Compare commits

..

38 Commits

Author SHA1 Message Date
Alex Tran
853a65aef1 Up version for release 2022-09-07 15:26:29 -05:00
Alex
566039b93f feat(web): add asset and album count info (#623)
* Get asset and album count

* Generate APIs

* Added asset count for each type

* Added api on the web

* Added info button for asset and album count to trigger getting info on hover

* Remove websocket event from photo page
2022-09-07 15:16:18 -05:00
bo0tzz
18a7ff8726 Remove empty translations (#620) 2022-09-07 14:41:44 -05:00
Thanh Pham
6ffdf167fe fix(web): detail panel overflow-x (#615) 2022-09-07 13:20:44 -05:00
Alex
92c4f0598b fix(mobile): search page crashes the app on some Android models (#610) 2022-09-07 06:45:26 -05:00
Alex
a337402124 fix(web): stop showing version announcement on first run of a new web instance (#609) 2022-09-07 06:38:29 -05:00
dependabot[bot]
209e6332b3 Bump actions/checkout from 2 to 3 (#604)
Bumps [actions/checkout](https://github.com/actions/checkout) from 2 to 3.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v2...v3)

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

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-09-07 06:08:44 -05:00
Jaime Baez
645bd8a109 Add web test setup (#597)
* Extract logic from Albums page

- move "albums" page logic to `albums-bloc`
- add types to AlbumCard custom events

* Implement some album-bloc unit-tests

- add libraries for testing
- add album factory
- changes in albums-bloc API

* Add rest of albums-bloc test

Cleanup and remove console logs

* Refactor `isShowContextMenu` writable to derived
2022-09-07 05:20:19 -05:00
Daniel Weaver
9a471d80f7 Update README.md (#599)
Adding a note to the installation section about reverse proxies being a cause for issues when uploading large files.
2022-09-06 16:05:38 -05:00
Alex Tran
de0c59efe7 Added mobile change log 37 2022-09-06 10:03:55 -05:00
Alex Tran
c19d26f4f3 Update some mobile UI with Material 3 theme 2022-09-06 09:37:04 -05:00
Alex Tran
2edfc75c8a Fixed sliverappbar icon color to conform with theming 2022-09-06 08:18:07 -05:00
Matthias Rupp
4c977d2c1f fix(mobile): cache related crash (#593) 2022-09-06 08:10:52 -05:00
Alex
1425f2ec78 Up server version 2022-09-05 23:44:42 -05:00
Alex
b081eda76f fix(server): change the createdAt and modifiedAt to the correct type in database (#591)
* Added migration files

* Remove type casting in sql query
2022-09-05 20:51:01 -05:00
Thanh Pham
7f6837c751 hotfix(server): skip exif extraction on duplicate file (#590)
* fix(server): skip exif extraction on duplicate file

* fix(server): typo

* chore(server): remvoe un-use code
2022-09-05 20:02:50 -05:00
Thanh Pham
a467936e73 feat(server): de-duplication (#557)
* feat(server): remove un-used deviceAssetId cols.

* feat(server): return 409 if asset is duplicated

* feat(server): replace old unique constaint

* feat(server): strip deviceId in file path

* feat(server): skip duplicate asset

* chore(server): revert changes

* fix(server): asset test spec

* fix(server): checksum generation for uploaded assets

* fix(server): make sure generation queue run after migraion

* feat(server): remove temp file

* chore(server): remove dead code
2022-09-05 14:45:38 -05:00
Alex Tran
2677ddccaa Up version for release 2022-09-05 14:32:05 -05:00
bo0tzz
564ace3ddf Use runtime env var for login page message to lower web container startup time (#577)
* Use runtime env var for loginPageMessage

* Rename VITE_LOGIN_PAGE_MESSAGE to PUBLIC_LOGIN_PAGE_MESSAGE in .env.example

* Move docker image `npm run build` step into Dockerfile

* Remove comment from web Dockerfile
2022-09-05 09:51:45 -05:00
Thanh Pham
a81ef7497c feat(server): support 3gpp format (#582)
* feat(server): support 3gpp format

* feat(web): add 3gp ext

* Support 3gp video format.

video/3gpp mimetype added to supported video format.

* feat(mobile): add tif ext

Co-authored-by: Alexandre Bouijoux <alexandre@bouijoux.fr>
2022-09-05 08:53:13 -05:00
Matthias Rupp
caa7b07398 Show all albums an asset appears in on the asset viewer page (#575)
* Add route to query albums for a specific asset

* Update API and add to detail-panel

* Fix tests

* Refactor API endpoint

* Added alt attribute to img tag

Co-authored-by: Alex <alex.tran1502@gmail.com>
2022-09-05 08:50:20 -05:00
Alex
6976a7241e Fixed upload asset to album in asset selection (#579)
* Fixed error uploading a file from album

* Fixed album selection mode show viewing asset stage

* Navigate back after uploading asset to album
2022-09-05 00:18:53 -05:00
Alex Tran
172eda3ce5 Fixed readme 2022-09-04 21:08:13 -05:00
Alex
552340add7 Feature - Implemented virtual scroll on web (#573)
This PR implemented a virtual scroll on the web, as seen in this article.

[Building the Google Photos Web UI](https://medium.com/google-design/google-photos-45b714dfbed1)
2022-09-04 08:34:39 -05:00
SirBogner
bd92dde117 Update localizely.yml (#574) 2022-09-04 08:27:05 -05:00
Alex
617c54ab81 [Localizely] Translations update (#576) 2022-09-04 08:26:11 -05:00
Thanh Pham
c76f7804ab feat(server): generate checksum for previous uploaded assets (#558)
* feat(server): generate checksum for previous uploaded assets

* fix(server): typo
2022-09-02 08:32:21 -05:00
Damian Gomez
0799aa2c72 Italia language for Mobile App (#559)
Co-authored-by: Damian Gomez <damian.gomez@elitedivision.it>
2022-09-01 08:48:13 -05:00
Thanh Pham
b80dca74ef feat(server): calculate sha1 checksum (#525)
* feat(server): override multer storage

* feat(server): calc sha1 of uploaded file

* feat(server): add checksum into asset

* chore(server): add package-lock for mkdirp package

* fix(server): free hash stream

* chore(server): rollback this changes, not refactor here

* refactor(server): re-arrange import statement

* fix(server): make sure hash done before callback

* refactor(server): replace varchar to char for checksum, reserve pixelChecksum for future

* refactor(server): remove pixelChecksum

* refactor(server): convert checksum from string to bytea

* feat(server): add index to checksum

* refactor(): rollback package.json changes

* feat(server): remove uploaded file when progress fail

* feat(server): calculate hash in sequence
2022-08-31 09:27:17 -05:00
Thanh Pham
f5f00e0f6c fix(web): file uploading error in album page (#550)
* feat(web): show upload error notification

* fix(web): album upload issue
2022-08-31 08:12:31 -05:00
Fynn Petersen-Frey
75d2d82d05 ask user to disable battery optimizations when turning on background backup (#554)
* ask user to disable battery optimizations when turning on background backup

* remove obsolete texts/translations

* add button link to dontkillmyapp
2022-08-31 08:08:40 -05:00
Fynn Petersen-Frey
5172242f88 fix: persist WiFi + charging settings of background backup (#553) 2022-08-30 09:09:19 -05:00
Matthias Rupp
25e68cf826 Better caching for mobile (#521)
* Use custom caches in all modules

* Cache Settings

* Fix wrong key

* Create custom cache repository based on hive

* Show cache usage in settings

* Show cache sizes

* Change settings ranges and default value

* Handle cache clear by operating system

* Resolve review comments
2022-08-29 22:44:43 -05:00
be bright
e527685ebf Added korean translation for mobile app (#549)
* Added korean translation for mobile app

* Added locale to info.plist

Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
2022-08-29 08:54:40 -05:00
Thanh Pham
e745cb5e4b fix(server): parse all img formats and enrich metadata (#547)
* fix(server): use file path instead buffer to reduce memory usage

fix undefined exif data

* fix(server): parse all img formats

* feat(server): enrich metadata

* Format oneliner condition

Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
2022-08-28 15:43:31 -05:00
Thanh Pham
dfaa4969da Server - Fixed - Use file path instead buffer to reduce memory usage on EXIF extraction (#545)
fix undefined exif data
2022-08-28 11:09:24 -05:00
Alex
f980a2f27a Add asset repository and refactor asset service (#540)
* build endpoint to get asset count by month

* Added asset repository

* Added create asset

* get asset by device ID

* Added test for existing methods

* Refactor additional endpoint

* Refactor database api to get curated locations and curated objects

* Refactor get search properties

* Fixed cookies parsing for websocket

* Added API to get asset count by time group

* Remove unused code
2022-08-26 22:53:37 -07:00
Alex
6b7c97c02a Added release notes 2022-08-26 13:43:05 -07:00
169 changed files with 14975 additions and 1113 deletions

View File

@@ -13,7 +13,7 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v2
uses: actions/checkout@v3
- name: Run Immich Server 2E2 Test
run: docker-compose -f ./docker/docker-compose.test.yml --env-file ./docker/.env.test up --abort-on-container-exit --exit-code-from immich-server-test
@@ -24,7 +24,7 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v2
uses: actions/checkout@v3
- name: Run tests
run: cd server && npm install && npm run test

View File

@@ -36,20 +36,22 @@
> ⚠️ WARNING: **NOT READY FOR PRODUCTION! DO NOT USE TO STORE YOUR ASSETS**. This project is under heavy development, there will be continuous functions, features and api changes.
| | Mobile | Web |
| Features | Mobile | Web |
| - | - | - |
| ☁️ Upload and view videos and photos | Yes | Yes
| 🔄 Auto backup when the app is opened | Yes | N/A
| ☑️ Selective album(s) for backup | Yes | N/A
| ⬇️ Download photos and videos to local device | Yes | Yes
| 👪 Multi-user support | Yes | Yes
| 🖼️ Album | Yes | Yes
| 🤝 Shared Albums | Yes | Yes
| 🚀 Quick navigation with draggable scrollbar | Yes | Yes
| 🗃️ Support RAW (HEIC, HEIF, DNG, Apple ProRaw) | Yes | Yes
| 🧭 Metadata view (EXIF, map) | Yes | Yes
| 🔎 Search by metadata, objects and image tags | Yes | No
| ⚙️ Administrative functions (user management) | N/A | Yes
| Upload and view videos and photos | Yes | Yes
| Auto backup when the app is opened | Yes | N/A
| Selective album(s) for backup | Yes | N/A
| Download photos and videos to local device | Yes | Yes
| Multi-user support | Yes | Yes
| Album | Yes | Yes
| Shared Albums | Yes | Yes
| Quick navigation with draggable scrollbar | Yes | Yes
| Support RAW (HEIC, HEIF, DNG, Apple ProRaw) | Yes | Yes
| Metadata view (EXIF, map) | Yes | Yes
| Search by metadata, objects and image tags | Yes | No
| Administrative functions (user management) | N/A | Yes
| Background backup | Android | N/A
| Virtual scroll | N/A | Yes
<br/>
@@ -95,6 +97,8 @@ There are several services that compose Immich:
# Installation
NOTE: When using a reverse proxy in front of Immich (such as NGINX), the reverse proxy might require extra configuration to allow large files to be uploaded (such as client_max_body_size in the case of NGINX).
## Testing One-step installation (not recommended for production)
> ⚠️ *This installation method is for evaluating Immich before futher customization to meet the users' needs.*

View File

@@ -61,6 +61,6 @@ MAPBOX_KEY=
####################################################################################
# Custom message on the login page, should be written in HTML form.
# For example VITE_LOGIN_PAGE_MESSAGE="This is a demo instance of Immich.<br><br>Email: <i>demo@demo.de</i><br>Password: <i>demo</i>"
# For example PUBLIC_LOGIN_PAGE_MESSAGE="This is a demo instance of Immich.<br><br>Email: <i>demo@demo.de</i><br>Password: <i>demo</i>"
VITE_LOGIN_PAGE_MESSAGE=
PUBLIC_LOGIN_PAGE_MESSAGE=

View File

@@ -9,8 +9,14 @@ upload:
locale_code: de-DE
- file: mobile/assets/i18n/fr-FR.json
locale_code: fr-FR
- file: mobile/assets/i18n/it-IT.json
locale_code: it-IT
- file: mobile/assets/i18n/nl-NL.json
locale_code: nl-NL
- file: mobile/assets/i18n/ko-KR.json
locale_code: ko-KR
- file: mobile/assets/i18n/da-DK.json
locale_code: da-DK
download:
files:
- file: mobile/assets/i18n/en-US.json
@@ -19,5 +25,11 @@ download:
locale_code: de-DE
- file: mobile/assets/i18n/fr-FR.json
locale_code: fr-FR
- file: mobile/assets/i18n/it-IT.json
locale_code: it-IT
- file: mobile/assets/i18n/nl-NL.json
locale_code: nl-NL
- file: mobile/assets/i18n/ko-KR.json
locale_code: ko-KR
- file: mobile/assets/i18n/da-DK.json
locale_code: da-DK

View File

@@ -51,7 +51,7 @@ android {
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId "app.alextran.immich"
minSdkVersion 21
minSdkVersion 23
targetSdkVersion flutter.targetSdkVersion
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName

View File

@@ -69,26 +69,8 @@ class BackgroundServicePlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
"isEnabled" -> {
result.success(BackupWorker.isEnabled(ctx))
}
"disableBatteryOptimizations" -> {
if(!BackupWorker.isIgnoringBatteryOptimizations(ctx)) {
val args = call.arguments<ArrayList<*>>()!!
val text = args.get(0) as String
Toast.makeText(ctx, text, Toast.LENGTH_LONG).show()
val intent = Intent(Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
intent.setData(Uri.parse("package:" + ctx.getPackageName()))
try {
ctx.startActivity(intent)
} catch(e: Exception) {
intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
try {
ctx.startActivity(intent)
} catch (e2: Exception) {
return result.success(false)
}
}
}
result.success(true)
"isIgnoringBatteryOptimizations" -> {
result.success(BackupWorker.isIgnoringBatteryOptimizations(ctx))
}
else -> result.notImplemented()
}

View File

@@ -30,8 +30,8 @@ platform :android do
task: 'bundle',
build_type: 'Release',
properties: {
"android.injected.version.code" => 36,
"android.injected.version.name" => "1.26.0",
"android.injected.version.code" => 38,
"android.injected.version.name" => "1.28.0",
}
)
upload_to_play_store(skip_upload_apk: true, skip_upload_images: true, skip_upload_screenshots: true, aab: '../build/app/outputs/bundle/release/app-release.aab')

View File

@@ -0,0 +1 @@
* Fixed rendering blank when failed to parse datetime on main timeline

View File

@@ -0,0 +1,2 @@
* Add cache setting and improve caching mechanism
* Persist WiFi + charging settings of background backup

View File

@@ -0,0 +1,2 @@
* Fixed remove empty translations
* Fixed search page crashes the app on some Android models

View File

@@ -5,17 +5,17 @@
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000224">
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000222">
</testcase>
<testcase classname="fastlane.lanes" name="1: bundleRelease" time="65.786484">
<testcase classname="fastlane.lanes" name="1: bundleRelease" time="55.311329">
</testcase>
<testcase classname="fastlane.lanes" name="2: upload_to_play_store" time="36.344276">
<testcase classname="fastlane.lanes" name="2: upload_to_play_store" time="30.070842">
</testcase>

View File

@@ -1,6 +1,9 @@
{
"album_info_card_backup_album_excluded": "AUSGESCHLOSSEN",
"album_info_card_backup_album_included": "EINGESCHLOSSEN",
"album_thumbnail_card_item": "1 Element",
"album_thumbnail_card_items": "{} Elemente",
"album_thumbnail_card_shared": " · Geteilt",
"album_viewer_appbar_share_delete": "Album löschen",
"album_viewer_appbar_share_err_delete": "Album konnte nicht gelöscht werden",
"album_viewer_appbar_share_err_leave": "Album konnte nicht verlassen werden",
@@ -46,9 +49,11 @@
"backup_err_only_album": "Das einzige Album kann nicht entfernt werden",
"backup_info_card_assets": "Elemente",
"control_bottom_app_bar_delete": "Löschen",
"create_shared_album_page_share": "Teilen",
"control_bottom_app_bar_share": "Teilen",
"create_album_page_untitled": "Unbenannt",
"create_shared_album_page_create": "Erstellen",
"create_shared_album_page_share_add_assets": "FOTOS HINZUFÜGEN",
"create_shared_album_page_share": "Teilen",
"create_shared_album_page_share_add_assets": "ELEMENTE HINZUFÜGEN",
"create_shared_album_page_share_select_photos": "Fotos auswählen",
"daily_title_text_date": "E, dd MMM",
"daily_title_text_date_year": "E, dd MMM, yyyy",
@@ -60,6 +65,8 @@
"exif_bottom_sheet_description": "Beschreibung hinzufügen...",
"exif_bottom_sheet_details": "DETAILS",
"exif_bottom_sheet_location": "STANDORT",
"library_page_albums": "Alben",
"library_page_new_album": "Neues Album",
"login_form_button_text": "Anmelden",
"login_form_email_hint": "deine@email.de",
"login_form_endpoint_hint": "http://deine-server-ip:port/api",
@@ -68,15 +75,15 @@
"login_form_err_invalid_email": "Ungültige E-Mail",
"login_form_err_leading_whitespace": "Führendes Leerzichen",
"login_form_err_trailing_whitespace": "Folgendes Leerzeichen",
"login_form_failed_login": "Fehler bei der Anmeldung, überprüfen Sie Server URL, E-Mail und Passwort",
"login_form_failed_login": "Error logging you in, check server url, email and password",
"login_form_label_email": "E-Mail",
"login_form_label_password": "Passwort",
"login_form_password_hint": "Passwort",
"login_form_password_hint": "password",
"login_form_save_login": "Angemeldet bleiben",
"monthly_title_text_date_format": "MMMM y",
"profile_drawer_client_server_up_to_date": "App und Server sind aktuell",
"profile_drawer_sign_out": "Abmelden",
"profile_drawer_settings": "Einstellungen",
"profile_drawer_sign_out": "Abmelden",
"search_bar_hint": "Durchsuche deine Fotos",
"search_page_no_objects": "Keine Objektinformationen verfügbar",
"search_page_no_places": "Keine Informationen über Orte verfügbar",
@@ -85,42 +92,35 @@
"search_result_page_new_search_hint": "Neue Suche",
"select_additional_user_for_sharing_page_suggestions": "Vorschläge",
"select_user_for_sharing_page_err_album": "Album konnte nicht erstellt werden",
"select_user_for_sharing_page_share_suggestions": "Vorschläge",
"select_user_for_sharing_page_share_suggestions": "Suggestions",
"setting_pages_app_bar_settings": "Einstellungen",
"share_add": "Hinzufügen",
"share_add_photos": "Fotos hinzufügen",
"share_add_title": "Titel hinzufügen",
"share_create_album": "Album erstellen",
"share_dialog_preparing": "Vorbereiten...",
"share_invite": "Zum Album einladen",
"sharing_page_album": "Geteilte Alben",
"sharing_page_description": "Erstelle ein geteiltes Album um Fotos und Videos mit Personen in deinem Netzwerk zu teilen.",
"sharing_page_empty_list": "LEERE LISTE",
"sharing_silver_appbar_create_shared_album": "Neues geteiltes Album",
"sharing_silver_appbar_share_partner": "Teile mit Partner",
"tab_controller_nav_library": "Bibliothek",
"tab_controller_nav_photos": "Fotos",
"tab_controller_nav_search": "Suche",
"tab_controller_nav_sharing": "Teilen",
"tab_controller_nav_library": "Bibliothek",
"theme_setting_dark_mode_switch": "Dunkler Modus",
"theme_setting_image_viewer_quality_subtitle": "Einstellen der Qualität des Detailbildbetrachters",
"theme_setting_image_viewer_quality_title": "Qualität des Bildbetrachters",
"theme_setting_system_theme_switch": "Automatisch (Systemeinstellung folgen)",
"theme_setting_theme_subtitle": "Wählen Sie die Themeneinstellung der App",
"theme_setting_theme_title": "Theme",
"theme_setting_three_stage_loading_subtitle": "Das dreistufige Ladeverfahren kann die Performance beim Laden verbessern, erhöht allerdings den Datenverbrauch deutlich",
"theme_setting_three_stage_loading_title": "Dreistufiges Laden aktivieren",
"version_announcement_overlay_ack": "Ich habe verstanden",
"version_announcement_overlay_release_notes": "Änderungsprotokoll",
"version_announcement_overlay_text_1": "Hallo mein Freund! Es gibt eine neue Version von",
"version_announcement_overlay_text_2": "Bitte nehm dir die Zeit und lese das ",
"version_announcement_overlay_text_3": " und achte darauf, dass deine docker-compose und .env Dateien aktuell sind, vor allem wenn du ein System für automatische Updates benutzt (z.B. Watchtower).",
"version_announcement_overlay_title": "Neue Server-Version verfügbar \uD83C\uDF89",
"album_thumbnail_card_item": "1 Element",
"album_thumbnail_card_items": "{} Elemente",
"album_thumbnail_card_shared": " · Geteilt",
"library_page_albums": "Alben",
"library_page_new_album": "Neues Album",
"create_album_page_untitled": "Unbenannt",
"share_dialog_preparing": "Vorbereiten...",
"control_bottom_app_bar_share": "Teilen",
"setting_pages_app_bar_settings": "Einstellungen",
"theme_setting_theme_title": "Theme",
"theme_setting_theme_subtitle": "Wählen Sie die Themeneinstellung der App",
"theme_setting_system_theme_switch": "Automatisch (Systemeinstellung folgen)",
"theme_setting_dark_mode_switch": "Dunkler Modus",
"theme_setting_image_viewer_quality_title": "Qualität des Bildbetrachters",
"theme_setting_image_viewer_quality_subtitle": "Einstellen der Qualität des Detailbildbetrachters",
"theme_setting_three_stage_loading_title": "Dreistufiges Laden aktivieren",
"theme_setting_three_stage_loading_subtitle": "Das dreistufige Ladeverfahren kann die Performance beim Laden verbessern, erhöht allerdings den Datenverbrauch deutlich"
}
"version_announcement_overlay_title": "Neue Server-Version verfügbar \uD83C\uDF89"
}

View File

@@ -1,6 +1,9 @@
{
"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",
@@ -9,6 +12,8 @@
"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.",
@@ -16,26 +21,29 @@
"backup_album_selection_page_selection_info": "Selection Info",
"backup_album_selection_page_total_assets": "Total unique assets",
"backup_all": "All",
"backup_background_service_default_notification": "Checking for new assets…",
"backup_background_service_disable_battery_optimizations": "Please disable battery optimization for Immich to enable background backup",
"backup_background_service_upload_failure_notification": "Failed to upload {}",
"backup_background_service_in_progress_notification": "Backing up your assets…",
"backup_background_service_current_upload_notification": "Uploading {}",
"backup_background_service_error_title": "Backup error",
"backup_background_service_connection_failed_message": "Failed to connect to the server. Retrying…",
"backup_background_service_backup_failed_message": "Failed to backup assets. Retrying…",
"backup_background_service_connection_failed_message": "Failed to connect to the server. Retrying…",
"backup_background_service_current_upload_notification": "Uploading {}",
"backup_background_service_default_notification": "Checking for new assets…",
"backup_background_service_error_title": "Backup error",
"backup_background_service_in_progress_notification": "Backing up your assets…",
"backup_background_service_upload_failure_notification": "Failed to upload {}",
"backup_controller_page_albums": "Backup Albums",
"backup_controller_page_background_battery_info_link": "Show me how",
"backup_controller_page_background_battery_info_message": "For the best background backup experience, please disable any battery optimizations restricting background activity for Immich.\n\nSince this is device-specific, please lookup the required information for your device manufacturer.",
"backup_controller_page_background_battery_info_ok": "OK",
"backup_controller_page_background_battery_info_title": "Battery optimizations",
"backup_controller_page_background_charging": "Only while charging",
"backup_controller_page_background_configure_error": "Failed to configure the background service",
"backup_controller_page_background_description": "Turn on the background service to automatically backup any new assets without needing to open the app",
"backup_controller_page_background_is_off": "Automatic background backup is off",
"backup_controller_page_background_is_on": "Automatic background backup is on",
"backup_controller_page_background_turn_off": "Turn off background service",
"backup_controller_page_background_turn_on": "Turn on background service",
"backup_controller_page_background_wifi": "Only on WiFi",
"backup_controller_page_backup": "Backup",
"backup_controller_page_backup_selected": "Selected: ",
"backup_controller_page_backup_sub": "Backed up photos and videos",
"backup_controller_page_background_description": "Turn on the background service to automatically backup any new assets without needing to open the app",
"backup_controller_page_background_wifi": "Only on WiFi",
"backup_controller_page_background_charging": "Only while charging",
"backup_controller_page_background_is_on": "Automatic background backup is on",
"backup_controller_page_background_is_off": "Automatic background backup is off",
"backup_controller_page_background_turn_on": "Turn on background service",
"backup_controller_page_background_turn_off": "Turn off background service",
"backup_controller_page_background_configure_error": "Failed to configure the background service",
"backup_controller_page_cancel": "Cancel",
"backup_controller_page_created": "Created on: {}",
"backup_controller_page_desc_backup": "Turn on backup to automatically upload new assets to the server.",
@@ -61,10 +69,25 @@
"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_delete": "Delete",
"create_shared_album_page_share": "Share",
"control_bottom_app_bar_share": "Share",
"create_album_page_untitled": "Untitled",
"create_shared_album_page_create": "Create",
"create_shared_album_page_share_add_assets": "ADD PHOTOS",
"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",
@@ -76,6 +99,8 @@
"exif_bottom_sheet_description": "Add Description...",
"exif_bottom_sheet_details": "DETAILS",
"exif_bottom_sheet_location": "LOCATION",
"library_page_albums": "Albums",
"library_page_new_album": "New album",
"login_form_button_text": "Login",
"login_form_email_hint": "youremail@email.com",
"login_form_endpoint_hint": "http://your-server-ip:port/api",
@@ -91,8 +116,8 @@
"login_form_save_login": "Stay logged in",
"monthly_title_text_date_format": "MMMM y",
"profile_drawer_client_server_up_to_date": "Client and Server are up-to-date",
"profile_drawer_sign_out": "Sign out",
"profile_drawer_settings": "Settings",
"profile_drawer_sign_out": "Sign Out",
"search_bar_hint": "Search your photos",
"search_page_no_objects": "No Objects Info Available",
"search_page_no_places": "No Places Info Available",
@@ -102,52 +127,43 @@
"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_notifications_notify_failures_grace_period": "Notify background backup failures: {}",
"setting_notifications_notify_hours": "{} hours",
"setting_notifications_notify_immediately": "immediately",
"setting_notifications_notify_minutes": "{} minutes",
"setting_notifications_notify_never": "never",
"setting_notifications_subtitle": "Adjust your notification preferences",
"setting_notifications_title": "Notifications",
"setting_pages_app_bar_settings": "Settings",
"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",
"tab_controller_nav_search": "Search",
"tab_controller_nav_sharing": "Sharing",
"tab_controller_nav_library": "Library",
"theme_setting_asset_list_storage_indicator_title": "Show storage indicator on asset tiles",
"theme_setting_asset_list_tiles_per_row_title": "Number of assets per row ({})",
"theme_setting_dark_mode_switch": "Dark mode",
"theme_setting_image_viewer_quality_subtitle": "Adjust the quality of the detail image viewer",
"theme_setting_image_viewer_quality_title": "Image viewer quality",
"theme_setting_system_theme_switch": "Automatic (Follow system setting)",
"theme_setting_theme_subtitle": "Choose the app's theme setting",
"theme_setting_theme_title": "Theme",
"theme_setting_three_stage_loading_subtitle": "Three-stage loading might increase the loading performance but causes significantly higher network load",
"theme_setting_three_stage_loading_title": "Enable three-stage loading",
"version_announcement_overlay_ack": "Acknowledge",
"version_announcement_overlay_release_notes": "release notes",
"version_announcement_overlay_text_1": "Hi friend, there is a new release of",
"version_announcement_overlay_text_2": "please take your time to visit the ",
"version_announcement_overlay_text_3": " and ensure your docker-compose and .env setup is up-to-date to prevent any misconfigurations, especially if you use WatchTower or any mechanism that handles updating your server application automatically.",
"version_announcement_overlay_title": "New Server Version Available \uD83C\uDF89",
"album_thumbnail_card_item": "1 item",
"album_thumbnail_card_items": "{} items",
"album_thumbnail_card_shared": " · Shared",
"library_page_albums": "Albums",
"library_page_new_album": "New album",
"create_album_page_untitled": "Untitled",
"share_dialog_preparing": "Preparing...",
"control_bottom_app_bar_share": "Share",
"setting_pages_app_bar_settings": "Settings",
"theme_setting_theme_title": "Theme",
"theme_setting_theme_subtitle": "Choose the app's theme setting",
"theme_setting_system_theme_switch": "Automatic (Follow system setting)",
"theme_setting_dark_mode_switch": "Dark mode",
"theme_setting_image_viewer_quality_title": "Image viewer quality",
"theme_setting_image_viewer_quality_subtitle": "Adjust the quality of the detail image viewer",
"theme_setting_three_stage_loading_title": "Enable three-stage loading",
"theme_setting_three_stage_loading_subtitle": "Three-stage loading might increase the loading performance but causes significantly higher network load",
"asset_list_settings_title": "Photo Grid",
"asset_list_settings_subtitle": "Photo grid layout settings",
"theme_setting_asset_list_storage_indicator_title": "Show storage indicator on asset tiles",
"theme_setting_asset_list_tiles_per_row_title": "Number of assets per row ({})",
"setting_notifications_title": "Notifications",
"setting_notifications_subtitle": "Adjust your notification preferences",
"setting_notifications_notify_failures_grace_period": "Notify background backup failures: {}",
"setting_notifications_notify_immediately": "immediately",
"setting_notifications_notify_minutes": "{} minutes",
"setting_notifications_notify_hours": "{} hours",
"setting_notifications_notify_never": "never"
}
"version_announcement_overlay_title": "New Server Version Available \uD83C\uDF89"
}

View File

@@ -21,12 +21,8 @@
"backup_controller_page_backup_selected": "Seleccionado:",
"backup_controller_page_backup_sub": "Copia de seguridad de fotos y vídeos",
"backup_controller_page_cancel": "Cancelar",
"backup_controller_page_created": "",
"backup_controller_page_desc_backup": "Active la copia de seguridad para cargar automáticamente los nuevos activos al servidor.",
"backup_controller_page_excluded": "Excluido:",
"backup_controller_page_failed": "",
"backup_controller_page_filename": "",
"backup_controller_page_id": "",
"backup_controller_page_info": "Información de la Copia de Seguridad",
"backup_controller_page_none_selected": "Ninguno seleccionado",
"backup_controller_page_remainder": "Remanente",
@@ -42,7 +38,6 @@
"backup_controller_page_total_sub": "Todas las fotos y vídeos únicos de los álbumes seleccionados",
"backup_controller_page_turn_off": "Apagar la copia de seguridad",
"backup_controller_page_turn_on": "Activar la copia de seguridad",
"backup_controller_page_uploading_file_info": "",
"backup_err_only_album": "No se puede eliminar el único álbum",
"backup_info_card_assets": "activos",
"control_bottom_app_bar_delete": "Eliminar",
@@ -67,7 +62,6 @@
"login_form_err_invalid_email": "Correo electrónico no válido",
"login_form_err_leading_whitespace": "Espacio en blanco inicial",
"login_form_err_trailing_whitespace": "Espacio en blanco al final",
"login_form_failed_login": "",
"login_form_label_email": "Correo",
"login_form_label_password": "Contraseña",
"login_form_password_hint": "contraseña",
@@ -76,14 +70,12 @@
"profile_drawer_client_server_up_to_date": "El Cliente y el Servidor están actualizados",
"profile_drawer_sign_out": "Cerrar Sesión",
"search_bar_hint": "Busca tus fotos",
"search_page_no_objects": "",
"search_page_no_places": "No hay información de lugares disponibles",
"search_page_places": "Lugares",
"search_page_things": "Cosas",
"search_result_page_new_search_hint": "Nueva Busqueda",
"select_additional_user_for_sharing_page_suggestions": "Sugerencias",
"select_user_for_sharing_page_err_album": "Fallo al crear el álbum",
"select_user_for_sharing_page_share_suggestions": "",
"share_add": "Añadir",
"share_add_photos": "Añadir fotos",
"share_add_title": "Añadir un título",

View File

@@ -49,9 +49,6 @@
"create_shared_album_page_share": "Jaa",
"create_shared_album_page_share_add_assets": "LISÄÄ KOHTEITA",
"create_shared_album_page_share_select_photos": "Valitse kuvat",
"daily_title_text_date": "",
"daily_title_text_date_year": "",
"date_format": "",
"delete_dialog_alert": "Nämä kohteet poistetaan pysyvästi Immich:stä ja laitteeltasi",
"delete_dialog_cancel": "Peruuta",
"delete_dialog_ok": "Poista",
@@ -72,7 +69,6 @@
"login_form_label_password": "Salasana",
"login_form_password_hint": "salasana",
"login_form_save_login": "Pysy kirjautuneena",
"monthly_title_text_date_format": "",
"profile_drawer_client_server_up_to_date": "Asiakassovellus ja palvelin ovat ajan tasalla",
"profile_drawer_sign_out": "Kirjaudu ulos",
"search_bar_hint": "Etsi kuvia",

View File

@@ -1,6 +1,9 @@
{
"album_info_card_backup_album_excluded": "EXCLU",
"album_info_card_backup_album_included": "INCLUS",
"album_thumbnail_card_item": "1 élément",
"album_thumbnail_card_items": "{} éléments",
"album_thumbnail_card_shared": " · Partagé",
"album_viewer_appbar_share_delete": "Supprimer l'album",
"album_viewer_appbar_share_err_delete": "Échec de la suppression de l'album",
"album_viewer_appbar_share_err_leave": "Impossible de quitter l'album",
@@ -46,6 +49,9 @@
"backup_err_only_album": "Impossible de retirer le seul album",
"backup_info_card_assets": "éléments",
"control_bottom_app_bar_delete": "Supprimer",
"control_bottom_app_bar_share": "Partager",
"create_album_page_untitled": "Sans titre",
"create_shared_album_page_create": "Créer",
"create_shared_album_page_share": "Partager",
"create_shared_album_page_share_add_assets": "AJOUTER DES ÉLÉMENTS",
"create_shared_album_page_share_select_photos": "Sélectionner les photos",
@@ -59,6 +65,8 @@
"exif_bottom_sheet_description": "Ajouter une description...",
"exif_bottom_sheet_details": "DÉTAILS",
"exif_bottom_sheet_location": "LOCALISATION",
"library_page_albums": "Albums",
"library_page_new_album": "Nouvel album",
"login_form_button_text": "Connexion",
"login_form_email_hint": "votreemail@email.com",
"login_form_endpoint_hint": "http://adresse-ip-serveur:port/api",
@@ -74,6 +82,7 @@
"login_form_save_login": "Rester connecté",
"monthly_title_text_date_format": "MMMM y",
"profile_drawer_client_server_up_to_date": "Le client et le serveur sont à jour",
"profile_drawer_settings": "Paramètres",
"profile_drawer_sign_out": "Se déconnecter",
"search_bar_hint": "Rechercher vos photos",
"search_page_no_objects": "Aucune information disponible sur les objets",
@@ -88,12 +97,14 @@
"share_add_photos": "Ajouter des photos",
"share_add_title": "Ajouter un titre",
"share_create_album": "Créer un album",
"share_dialog_preparing": "Préparation...",
"share_invite": "Inviter à l'album",
"sharing_page_album": "Albums partagés",
"sharing_page_description": "Créez des albums partagés pour partager des photos et des vidéos avec les personnes de votre réseau.",
"sharing_page_empty_list": "LISTE VIDE",
"sharing_silver_appbar_create_shared_album": "Créer un album partagé",
"sharing_silver_appbar_share_partner": "Partager avec un partenaire",
"tab_controller_nav_library": "Bibliothèque",
"tab_controller_nav_photos": "Photos",
"tab_controller_nav_search": "Recherche",
"tab_controller_nav_sharing": "Partage",

View File

@@ -1,28 +1,52 @@
{
"album_info_card_backup_album_excluded": "ESCLUSI",
"album_info_card_backup_album_included": "INCLUSI",
"album_thumbnail_card_item": "1 elemento ",
"album_thumbnail_card_items": "{} elementi",
"album_thumbnail_card_shared": "Condiviso",
"album_viewer_appbar_share_delete": "Elimina album ",
"album_viewer_appbar_share_err_delete": "Fallito nel cancellare l'album ",
"album_viewer_appbar_share_err_leave": "Fallito nel lasciare l'album ",
"album_viewer_appbar_share_err_delete": "Errore nel cancellare l'album ",
"album_viewer_appbar_share_err_leave": "Errore nel lasciare l'album ",
"album_viewer_appbar_share_err_remove": "Ci sono problemi nel rimuovere oggetti dall'album ",
"album_viewer_appbar_share_err_title": "Fallito nel cambiare titolo dell'album ",
"album_viewer_appbar_share_leave": "Lascia l'album",
"album_viewer_appbar_share_err_title": "Errore nel cambiare il titolo dell'album ",
"album_viewer_appbar_share_leave": "Lascia album",
"album_viewer_appbar_share_remove": "Rimuovere dall'album ",
"album_viewer_page_share_add_users": "Aggiungi utenti",
"backup_album_selection_page_albums_device": "Albums nel device ({})",
"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 ({})",
"backup_album_selection_page_albums_tap": "Tap per includere, doppio tap per escludere.",
"backup_album_selection_page_assets_scatter": "Stesse immagini e video possono trovarsi tra più album, così gli album possono essere inclusi o esclusi dal backup.",
"backup_album_selection_page_select_albums": "Seleziona gli album",
"backup_album_selection_page_selection_info": "Informazioni sulla selezione ",
"backup_album_selection_page_total_assets": "Numero totale di oggetti unici",
"backup_all": "Tutti",
"backup_controller_page_albums": "Backup album",
"backup_background_service_backup_failed_message": "Impossibile caricare contenuti. Nuovo tentativo…",
"backup_background_service_connection_failed_message": "Impossibile connettersi al server. Nuovo tentativo…",
"backup_background_service_current_upload_notification": "Caricamento {}",
"backup_background_service_default_notification": "Verifica di nuovi contenuti…",
"backup_background_service_error_title": "Errore di Backup",
"backup_background_service_in_progress_notification": "Backing dei tuoi contenuti…",
"backup_background_service_upload_failure_notification": "Impossibile caricare {}",
"backup_controller_page_albums": "Backup Album",
"backup_controller_page_background_battery_info_link": "Mostrami come",
"backup_controller_page_background_battery_info_message": "Per una migliore esperienza di backup, disabilita le ottimisazioni della batteria per l'app Immich.\n\nDal momento che è una funzionalità specifica del dispositivo, per favore consulta il manuale del produttore.",
"backup_controller_page_background_battery_info_ok": "OK",
"backup_controller_page_background_battery_info_title": "Ottimizzazioni batteria",
"backup_controller_page_background_charging": "Solo durante la ricarica",
"backup_controller_page_background_configure_error": "Impossibile configurare i servizi in background",
"backup_controller_page_background_description": "Abilita i servizi in background per sincronizzare tutti i nuovi contenuti senza la necessità di aprire l'app",
"backup_controller_page_background_is_off": "Backup automatico spento",
"backup_controller_page_background_is_on": "Backup automatico attivo",
"backup_controller_page_background_turn_off": "Disabilita servizi in background",
"backup_controller_page_background_turn_on": "Abilita servizi in background",
"backup_controller_page_background_wifi": "Solo su WiFi",
"backup_controller_page_backup": "Backup",
"backup_controller_page_backup_selected": "Selezionati:",
"backup_controller_page_backup_sub": "Photo e video salvati",
"backup_controller_page_backup_sub": "Foto e video caricati",
"backup_controller_page_cancel": "Cancella ",
"backup_controller_page_created": "Creato il: {}",
"backup_controller_page_desc_backup": "Attiva il backup automatico per eseguire upload sul server",
"backup_controller_page_desc_backup": "Attiva il backup per eseguire il caricamento automatico sul server",
"backup_controller_page_excluded": "Esclusi:",
"backup_controller_page_failed": "Falliti: ({})",
"backup_controller_page_filename": "Nome del file: {} [{}]",
@@ -30,39 +54,57 @@
"backup_controller_page_info": "Informazioni sul backup",
"backup_controller_page_none_selected": "Nessuna selezione",
"backup_controller_page_remainder": "Promemoria ",
"backup_controller_page_remainder_sub": "Photo e album selezionati che rimangono da salvare",
"backup_controller_page_remainder_sub": "Photo e album selezionati che rimangono da caricare",
"backup_controller_page_select": "Seleziona ",
"backup_controller_page_server_storage": "Spazio nel server",
"backup_controller_page_server_storage": "Spazio sul server",
"backup_controller_page_start_backup": "Inizia backup ",
"backup_controller_page_status_off": "Backup è disattivato ",
"backup_controller_page_status_on": "Backup è attivato",
"backup_controller_page_storage_format": "{} di {} usati",
"backup_controller_page_to_backup": "Album da salvare",
"backup_controller_page_to_backup": "Album da caricare",
"backup_controller_page_total": "Totale",
"backup_controller_page_total_sub": "Tutte le foto e i video unici salvati dagli album selezionati ",
"backup_controller_page_total_sub": "Tutte le foto e i video unici caricati dagli album selezionati ",
"backup_controller_page_turn_off": "Disattiva backup",
"backup_controller_page_turn_on": "Attiva backup ",
"backup_controller_page_uploading_file_info": "Info sul file caricato",
"backup_err_only_album": "Non è possibile rimuovere l'unico album",
"backup_info_card_assets": "Oggetti ",
"backup_info_card_assets": "oggetti ",
"cache_settings_album_thumbnails": "Anteprime pagine librerie ({} assets)",
"cache_settings_clear_cache_button": "Cancella cache",
"cache_settings_clear_cache_button_title": "Cancella cache app. Questo impatterà sulle prestazioni applicative fino a quando la cache non sarà rigenerata.",
"cache_settings_image_cache_size": "Dimensione cache foto ({} assets)",
"cache_settings_statistics_album": "Anteprime librerie",
"cache_settings_statistics_assets": "{} contenuti ({})",
"cache_settings_statistics_full": "Immagini complete",
"cache_settings_statistics_shared": "Anteprime album condivisi",
"cache_settings_statistics_thumbnail": "Anteprime",
"cache_settings_statistics_title": "Uso della cache",
"cache_settings_subtitle": "Controlla il comportamento della cache",
"cache_settings_thumbnail_size": "Dimensione cache anteprime ({} assets)",
"cache_settings_title": "Impostazioni della Cache",
"control_bottom_app_bar_delete": "Elimina",
"control_bottom_app_bar_share": "Condividi",
"create_album_page_untitled": "Senza titolo",
"create_shared_album_page_create": "Crea",
"create_shared_album_page_share": "Condividi",
"create_shared_album_page_share_add_assets": "AGGIUNGI OGGETTI",
"create_shared_album_page_share_select_photos": "Seleziona foto",
"daily_title_text_date": "E, dd MMM",
"daily_title_text_date_year": "E, dd MMM, yyyy",
"date_format": "E, d LLL, y • hh:mm",
"delete_dialog_alert": "Questi oggetti saranno cancellati permanentemente da Immich e dal tuo device",
"delete_dialog_alert": "Questi oggetti saranno cancellati definitivamente da Immich e dal tuo device",
"delete_dialog_cancel": "Annulla",
"delete_dialog_ok": "Elimina",
"delete_dialog_title": "Cancella in modo permanente ",
"delete_dialog_title": "Cancella definitivamente",
"exif_bottom_sheet_description": "Aggiungi una descrizione...",
"exif_bottom_sheet_details": "DETTAGLI",
"exif_bottom_sheet_location": "POSIZIONE",
"login_form_button_text": "Accedi",
"library_page_albums": "Album",
"library_page_new_album": "Nuovo Album",
"login_form_button_text": "Login",
"login_form_email_hint": "tuaemail@email.com",
"login_form_endpoint_hint": "http://tuo-ip-del-server:port/api",
"login_form_endpoint_url": "URL del Server Endpoint",
"login_form_endpoint_url": "Server Endpoint URL",
"login_form_err_http": "Per favore specificare http:// o https://",
"login_form_err_invalid_email": "Email non valida",
"login_form_err_leading_whitespace": "Spazio bianco all'inizio ",
@@ -74,33 +116,54 @@
"login_form_save_login": "Rimani connesso ",
"monthly_title_text_date_format": "MMMM y",
"profile_drawer_client_server_up_to_date": "Client e server sono aggiornati",
"profile_drawer_sign_out": "Esci",
"profile_drawer_settings": "Impostazioni ",
"profile_drawer_sign_out": "Logout",
"search_bar_hint": "Cerca le tue foto",
"search_page_no_objects": "Nessuna Informazione relativa all'Oggetto Disponibile",
"search_page_no_places": "Nessun informazione sulla posizione ",
"search_page_no_objects": "Nessuna informazione relativa all'oggetto disponibile",
"search_page_no_places": "Nessun informazione sul luogo disponibile",
"search_page_places": "Luoghi",
"search_page_things": "Oggetti",
"search_result_page_new_search_hint": "Nuova ricerca ",
"select_additional_user_for_sharing_page_suggestions": "Suggerimenti ",
"select_user_for_sharing_page_err_album": "Fallito nel creare l'album ",
"select_user_for_sharing_page_err_album": "Errore nel creare l'album ",
"select_user_for_sharing_page_share_suggestions": "Suggerimenti",
"setting_notifications_notify_failures_grace_period": "Notifica caricamenti falliti in background: {}",
"setting_notifications_notify_hours": "{} Ore",
"setting_notifications_notify_immediately": "Immediatamente",
"setting_notifications_notify_minutes": "{} Minuti",
"setting_notifications_notify_never": "Mai",
"setting_notifications_subtitle": "Cambia le impostazioni di notifica",
"setting_notifications_title": "Notifiche",
"setting_pages_app_bar_settings": "Impostazioni",
"share_add": "Aggiungi",
"share_add_photos": "Aggiungi foto",
"share_add_title": "Aggiungi un titolo ",
"share_create_album": "Crea album",
"share_invite": "Invitare all'album ",
"share_dialog_preparing": "Preparo…",
"share_invite": "Invitare nell'album ",
"sharing_page_album": "Album condivisi",
"sharing_page_description": "Crea un album condiviso per condividere foto e video con gente nel tuo network",
"sharing_page_description": "Crea un album condiviso per condividere foto e video con persone nel tuo network",
"sharing_page_empty_list": "LISTA VUOTA",
"sharing_silver_appbar_create_shared_album": "Crea album condiviso",
"sharing_silver_appbar_share_partner": "Condividi con il partner",
"tab_controller_nav_library": "Libreria",
"tab_controller_nav_photos": "Foto",
"tab_controller_nav_search": "Cerca",
"tab_controller_nav_sharing": "Condividi",
"theme_setting_asset_list_storage_indicator_title": "Mostra indicatore dello storage nei titoli dei contenuti",
"theme_setting_asset_list_tiles_per_row_title": "Numero di contenuti per riga ({})",
"theme_setting_dark_mode_switch": "Dark mode",
"theme_setting_image_viewer_quality_subtitle": "Cambia la qualità del dettaglio dell'immagine",
"theme_setting_image_viewer_quality_title": "Qualità immagine",
"theme_setting_system_theme_switch": "Automatico (Vai alle impostazioni di sistema)",
"theme_setting_theme_subtitle": "Scegli un'impostazione per il tema",
"theme_setting_theme_title": "Tema",
"theme_setting_three_stage_loading_subtitle": "Il caricamento in 3 stage aumenterà le performance di caricamento ma anche il consumo di banda",
"theme_setting_three_stage_loading_title": "Abilita il caricamento a tre stage",
"version_announcement_overlay_ack": "Riconosci ",
"version_announcement_overlay_release_notes": "le note di rilascio ",
"version_announcement_overlay_release_notes": "note di rilascio ",
"version_announcement_overlay_text_1": "Ciao amico, c'è una nuova versione di",
"version_announcement_overlay_text_2": "prova a controllare ",
"version_announcement_overlay_text_2": "per favore prenditi il tuo tempo per controllare il",
"version_announcement_overlay_text_3": "e verifica che il tuo docker-compose e il file .env siano aggiornati per impedire qualsiasi errore nella configurazione, specialmente se utilizzate WatchTower o altri strumenti per l'aggiornamento automatico delle immagini docker.",
"version_announcement_overlay_title": "Nuova versione di server disponibile! \uD83C\uDF89"
"version_announcement_overlay_title": "Nuova versione del server disponibile! \uD83C\uDF89"
}

View File

@@ -0,0 +1,152 @@
{
"album_info_card_backup_album_excluded": "제외됨",
"album_info_card_backup_album_included": "포함됨",
"album_viewer_appbar_share_delete": "앨범 삭제",
"album_viewer_appbar_share_err_delete": "앨범 삭제 실패",
"album_viewer_appbar_share_err_leave": "앨범에서 나가지 못했습니다",
"album_viewer_appbar_share_err_remove": "앨범에서 미디어를 제거하는 데 문제가 있습니다",
"album_viewer_appbar_share_err_title": "앨범 제목 변경 실패",
"album_viewer_appbar_share_leave": "앨범 나가기",
"album_viewer_appbar_share_remove": "앨범에서 제거",
"album_viewer_page_share_add_users": "사용자 추가",
"backup_album_selection_page_albums_device": "기기의 앨범({})",
"backup_album_selection_page_albums_tap": "포함하려면 탭하고 제외하려면 두 번 탭하세요",
"backup_album_selection_page_assets_scatter": "미디어파일은 여러 앨범에 분산될 수 있습니다. 따라서 백업 프로세스 중에 앨범에서 포함하거나 제외할 수 있습니다.",
"backup_album_selection_page_select_albums": "앨범 선택",
"backup_album_selection_page_selection_info": "선택 정보",
"backup_album_selection_page_total_assets": "총 미디어파일 수",
"backup_all": "모두",
"backup_background_service_default_notification": "새 미디어파일 확인중...",
"backup_background_service_upload_failure_notification": "{} 업로드 실패",
"backup_background_service_in_progress_notification": "미디어파일 백업 중...",
"backup_background_service_current_upload_notification": "{} 업로드 중",
"backup_background_service_error_title": "백업 오류",
"backup_background_service_connection_failed_message": "서버에 연결하지 못했습니다. 다시 시도하는 중...",
"backup_background_service_backup_failed_message": "미디어파일을 백업하지 못했습니다. 다시 시도하는 중...",
"backup_controller_page_albums": "백업대상",
"backup_controller_page_backup": "백업",
"backup_controller_page_backup_selected": "선택됨: ",
"backup_controller_page_backup_sub": "백업된 사진 및 비디오",
"backup_controller_page_background_description": "백그라운드 서비스를 켜서 앱을 열지 않고도 새 미디어파일을 자동으로 백업합니다.",
"backup_controller_page_background_wifi": "WiFi에서만",
"backup_controller_page_background_charging": "충전 중일 때만",
"backup_controller_page_background_is_on": "자동 백그라운드 백업이 켜져 있습니다",
"backup_controller_page_background_is_off": "자동 백그라운드 백업이 꺼져 있습니다",
"backup_controller_page_background_turn_on": "백그라운드 서비스 켜기",
"backup_controller_page_background_turn_off": "백그라운드 서비스 끄기",
"backup_controller_page_background_configure_error": "백그라운드 서비스를 구성하지 못했습니다",
"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_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": "미디어",
"control_bottom_app_bar_delete": "삭제",
"create_shared_album_page_share": "공유",
"create_shared_album_page_create": "만들기",
"create_shared_album_page_share_add_assets": "사진 추가",
"create_shared_album_page_share_select_photos": "사진 선택",
"daily_title_text_date": "E, M월 d일",
"daily_title_text_date_year": "E, M월 d일, yyyy",
"date_format": "yyyy년 M월 d일, EEEE • a h:mm",
"delete_dialog_alert": "이 항목은 Immich 및 휴대폰에서 영구적으로 삭제됩니다",
"delete_dialog_cancel": "취소",
"delete_dialog_ok": "삭제",
"delete_dialog_title": "영구적으로 삭제",
"exif_bottom_sheet_description": "설명 추가...",
"exif_bottom_sheet_details": "상세정보",
"exif_bottom_sheet_location": "위치",
"login_form_button_text": "로그인",
"login_form_email_hint": "youremail@email.com",
"login_form_endpoint_hint": "https://your-server-ip:port/api",
"login_form_endpoint_url": "서버 엔드포인트 URL",
"login_form_err_http": "엔드포인트는 http:// 또는 https://로 시작해야 합니다",
"login_form_err_invalid_email": "잘못된 이메일 형식입니다",
"login_form_err_leading_whitespace": "이메일 앞에 공백문자가 포함되어 있습니다",
"login_form_err_trailing_whitespace": "이메일 뒤에 공백문자가 포함되어 있습니다",
"login_form_failed_login": "로그인 오류, 서버 URL, 이메일 및 비밀번호를 확인하세요",
"login_form_label_email": "이메일",
"login_form_label_password": "비밀번호",
"login_form_password_hint": "비밀번호",
"login_form_save_login": "로그인상태 유지",
"monthly_title_text_date_format": "y년 M월",
"profile_drawer_client_server_up_to_date": "클라이언트와 서버가 최신 상태입니다",
"profile_drawer_sign_out": "로그아웃",
"profile_drawer_settings": "설정",
"search_bar_hint": "사진 검색",
"search_page_no_objects": "발견된 사물이\n없습니다",
"search_page_no_places": "발견된 장소가\n없습니다",
"search_page_places": "장소",
"search_page_things": "사물",
"search_result_page_new_search_hint": "새 검색",
"select_additional_user_for_sharing_page_suggestions": "초대 가능한 사용자 제안",
"select_user_for_sharing_page_err_album": "앨범 생성 실패",
"select_user_for_sharing_page_share_suggestions": "초대 가능한 사용자 제안",
"share_add": "추가",
"share_add_photos": "사진 추가",
"share_add_title": "새 앨범제목",
"share_create_album": "앨범 만들기",
"share_invite": "앨범에 초대",
"sharing_page_album": "공유앨범",
"sharing_page_description": "공유앨범을 만들어 다른 사용자들과 사진 및 비디오를 공유합니다.",
"sharing_page_empty_list": "공유앨범 없음",
"sharing_silver_appbar_create_shared_album": "공유앨범 만들기",
"sharing_silver_appbar_share_partner": "파트너와 공유",
"tab_controller_nav_photos": "사진",
"tab_controller_nav_search": "검색",
"tab_controller_nav_sharing": "공유",
"tab_controller_nav_library": "라이브러리",
"version_announcement_overlay_ack": "승인",
"version_announcement_overlay_release_notes": "릴리스 정보",
"version_announcement_overlay_text_1": "안녕하세요!",
"version_announcement_overlay_text_2": "앱에 새로운 업데이트가 있습니다!",
"version_announcement_overlay_text_3": "특히 WatchTower 또는 서버 응용 프로그램 자동 업데이트를 처리하는 메커니즘을 사용하는 경우 잘못된 구성을 방지하기 위해 docker-compose 및 .env 설정이 최신 상태인지 확인하세요.",
"version_announcement_overlay_title": "새 서버 버전 사용 가능 \uD83C\uDF89",
"album_thumbnail_card_item": "1개 항목",
"album_thumbnail_card_items": "{}개 항목",
"album_thumbnail_card_shared": " · 공유",
"library_page_albums": "앨범",
"library_page_new_album": "새 앨범",
"create_album_page_untitled": "제목없음",
"share_dialog_preparing": "준비중...",
"control_bottom_app_bar_share": "공유",
"setting_pages_app_bar_settings": "설정",
"theme_setting_theme_title": "테마",
"theme_setting_theme_subtitle": "앱테마 선택",
"theme_setting_system_theme_switch": "자동(시스템 설정에 따름)",
"theme_setting_dark_mode_switch": "다크모드",
"theme_setting_image_viewer_quality_title": "이미지 뷰어 품질",
"theme_setting_image_viewer_quality_subtitle": "디테일 이미지 뷰어 품질 조정",
"theme_setting_three_stage_loading_title": "3단계 로딩 활성화",
"theme_setting_three_stage_loading_subtitle": "이 기능은 로딩 성능을 향상시킬 수 있지만 훨씬 더 많은 데이터를 사용합니다.",
"asset_list_settings_title": "사진 배열",
"asset_list_settings_subtitle": "사진 배열 레이아웃 설정",
"theme_setting_asset_list_storage_indicator_title": "미디어 타일에 스토리지 싱크여부 표시",
"theme_setting_asset_list_tiles_per_row_title": "한 줄에 표시할 미디어 수 ({})",
"setting_notifications_title": "알림",
"setting_notifications_subtitle": "알림 기본 설정 조정",
"setting_notifications_notify_failures_grace_period": "백그라운드 백업 실패 알림: {}",
"setting_notifications_notify_immediately": "즉시",
"setting_notifications_notify_minutes": "{}분 뒤",
"setting_notifications_notify_hours": "{}시간 뒤",
"setting_notifications_notify_never": "알리지 않음"
}

View File

@@ -17,7 +17,6 @@
"backup_album_selection_page_total_assets": "Totaal unieke items",
"backup_all": "Alle",
"backup_background_service_default_notification": "Controleren op nieuw items…",
"backup_background_service_disable_battery_optimizations": "Schakel batterij optimalisatie uit voor Immich om achtergrond backup in te schakelen",
"backup_background_service_upload_failure_notification": "Fout bij upload {}",
"backup_background_service_in_progress_notification": "Backuppen van items…",
"backup_background_service_current_upload_notification": "Uploaden {}",

View File

@@ -17,7 +17,6 @@
"backup_album_selection_page_total_assets": "Total de recursos exclusivos",
"backup_all": "Todos",
"backup_background_service_default_notification": "Checking for new assets…",
"backup_background_service_disable_battery_optimizations": "Por favor, desabilite a otimização da bateria para Immich para habilitar o backup em segundo plano",
"backup_background_service_upload_failure_notification": "Falha ao carregar {}",
"backup_background_service_in_progress_notification": "Fazendo backup de seus ativos…",
"backup_background_service_current_upload_notification": "Enviando {}",

View File

@@ -21,6 +21,6 @@
<key>CFBundleVersion</key>
<string>1.0</string>
<key>MinimumOSVersion</key>
<string>9.0</string>
<string>11.0</string>
</dict>
</plist>

View File

@@ -84,7 +84,7 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/wakelock/ios"
SPEC CHECKSUMS:
Flutter: 50d75fe2f02b26cc09d224853bb45737f8b3214a
Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854
flutter_udid: 0848809dbed4c055175747ae6a45a8b4f6771e1c
fluttertoast: 16fbe6039d06a763f3533670197d01fc73459037
FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a

View File

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

View File

@@ -17,11 +17,11 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.26.0</string>
<string>1.27.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>51</string>
<string>52</string>
<key>LSRequiresIPhoneOS</key>
<true />
<key>MGLMapboxMetricsEnabledSettingShownInApp</key>
@@ -92,6 +92,7 @@
<string>it</string>
<string>fi</string>
<string>ja</string>
<string>ko</string>
<string>nl</string>
<string>pl</string>
<string>pt</string>

View File

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

View File

@@ -5,32 +5,32 @@
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000349">
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000269">
</testcase>
<testcase classname="fastlane.lanes" name="1: increment_version_number" time="0.650297">
<testcase classname="fastlane.lanes" name="1: increment_version_number" time="0.499192">
</testcase>
<testcase classname="fastlane.lanes" name="2: latest_testflight_build_number" time="7.757602">
<testcase classname="fastlane.lanes" name="2: latest_testflight_build_number" time="30.057077">
</testcase>
<testcase classname="fastlane.lanes" name="3: increment_build_number" time="0.421008">
<testcase classname="fastlane.lanes" name="3: increment_build_number" time="0.438506">
</testcase>
<testcase classname="fastlane.lanes" name="4: build_app" time="126.240949">
<testcase classname="fastlane.lanes" name="4: build_app" time="91.259106">
</testcase>
<testcase classname="fastlane.lanes" name="5: upload_to_testflight" time="68.206021">
<testcase classname="fastlane.lanes" name="5: upload_to_testflight" time="102.092139">
</testcase>

View File

@@ -22,4 +22,6 @@ const String userSettingInfoBox = "immichUserSettingInfoBox";
// Background backup Info
const String backgroundBackupInfoBox = "immichBackgroundBackupInfoBox"; // Box
const String backupFailedSince = "immichBackupFailedSince"; // Key 1
const String backupFailedSince = "immichBackupFailedSince"; // Key 1
const String backupRequireWifi = "immichBackupRequireWifi"; // Key 2
const String backupRequireCharging = "immichBackupRequireCharging"; // Key 3

View File

@@ -13,7 +13,8 @@ const List<Locale> locales = [
Locale('ja', 'JP'),
Locale('nl', 'NL'),
Locale('pl', 'PL'),
Locale('pt', 'PR')
Locale('pt', 'PR'),
Locale('ko', 'KR'),
];
const String translationsPath = 'assets/i18n';

View File

@@ -1,6 +1,7 @@
import 'package:auto_route/auto_route.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
@@ -8,6 +9,7 @@ import 'package:immich_mobile/constants/hive_box.dart';
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
import 'package:immich_mobile/modules/album/providers/asset_selection.provider.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/shared/services/cache.service.dart';
import 'package:immich_mobile/utils/image_url_builder.dart';
import 'package:openapi/api.dart';
@@ -15,17 +17,18 @@ class AlbumViewerThumbnail extends HookConsumerWidget {
final AssetResponseDto asset;
final List<AssetResponseDto> assetList;
final bool showStorageIndicator;
final BaseCacheManager? cacheManager;
const AlbumViewerThumbnail({
Key? key,
required this.asset,
required this.assetList,
this.cacheManager,
this.showStorageIndicator = true,
}) : super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
final cacheKey = useState(1);
var box = Hive.box(userInfoBox);
var thumbnailRequestUrl = getThumbnailUrl(asset);
var deviceId = ref.watch(authenticationProvider).deviceId;
@@ -123,7 +126,8 @@ class AlbumViewerThumbnail extends HookConsumerWidget {
return Container(
decoration: BoxDecoration(border: drawBorderColor()),
child: CachedNetworkImage(
cacheKey: "${asset.id}-${cacheKey.value}",
cacheManager: cacheManager,
cacheKey: asset.id,
width: 300,
height: 300,
memCacheHeight: 200,

View File

@@ -5,6 +5,8 @@ import 'package:hive_flutter/hive_flutter.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/hive_box.dart';
import 'package:immich_mobile/modules/album/providers/asset_selection.provider.dart';
import 'package:immich_mobile/shared/services/cache.service.dart';
import 'package:immich_mobile/utils/image_url_builder.dart';
import 'package:openapi/api.dart';
class SelectionThumbnailImage extends HookConsumerWidget {
@@ -15,15 +17,14 @@ class SelectionThumbnailImage extends HookConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final cacheKey = useState(1);
var box = Hive.box(userInfoBox);
var thumbnailRequestUrl =
'${box.get(serverEndpointKey)}/asset/thumbnail/${asset.id}';
var thumbnailRequestUrl = getThumbnailUrl(asset);
var selectedAsset =
ref.watch(assetSelectionProvider).selectedNewAssetsForAlbum;
var newAssetsForAlbum =
ref.watch(assetSelectionProvider).selectedAdditionalAssetsForAlbum;
var isAlbumExist = ref.watch(assetSelectionProvider).isAlbumExist;
final cacheService = ref.watch(cacheServiceProvider);
Widget _buildSelectionIcon(AssetResponseDto asset) {
var isSelected = selectedAsset.map((item) => item.id).contains(asset.id);
@@ -113,7 +114,8 @@ class SelectionThumbnailImage extends HookConsumerWidget {
Container(
decoration: BoxDecoration(border: drawBorderColor()),
child: CachedNetworkImage(
cacheKey: "${asset.id}-${cacheKey.value}",
cacheManager: cacheService.getCache(CacheType.thumbnail),
cacheKey: asset.id,
width: 150,
height: 150,
memCacheHeight: asset.type == AssetTypeEnum.IMAGE ? 150 : 150,

View File

@@ -4,6 +4,7 @@ import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/hive_box.dart';
import 'package:immich_mobile/shared/services/cache.service.dart';
import 'package:immich_mobile/utils/image_url_builder.dart';
import 'package:openapi/api.dart';
@@ -15,8 +16,7 @@ class SharedAlbumThumbnailImage extends HookConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final cacheKey = useState(1);
final cacheService = ref.watch(cacheServiceProvider);
var box = Hive.box(userInfoBox);
return GestureDetector(
@@ -26,7 +26,8 @@ class SharedAlbumThumbnailImage extends HookConsumerWidget {
child: Stack(
children: [
CachedNetworkImage(
cacheKey: "${asset.id}-${cacheKey.value}",
cacheManager: cacheService.getCache(CacheType.thumbnail),
cacheKey: asset.id,
width: 500,
height: 500,
memCacheHeight: 500,

View File

@@ -16,6 +16,7 @@ import 'package:immich_mobile/modules/album/ui/album_viewer_thumbnail.dart';
import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart';
import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/shared/services/cache.service.dart';
import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
import 'package:immich_mobile/shared/ui/immich_sliver_persistent_app_bar_delegate.dart';
import 'package:immich_mobile/shared/views/immich_loading_overlay.dart';
@@ -191,6 +192,7 @@ class AlbumViewerPage extends HookConsumerWidget {
final appSettingService = ref.watch(appSettingsServiceProvider);
final bool showStorageIndicator =
appSettingService.getSetting(AppSettingsEnum.storageIndicator);
final cacheService = ref.watch(cacheServiceProvider);
if (albumInfo.assets.isNotEmpty) {
return SliverPadding(
@@ -205,6 +207,7 @@ class AlbumViewerPage extends HookConsumerWidget {
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
return AlbumViewerThumbnail(
cacheManager: cacheService.getCache(CacheType.thumbnail),
asset: albumInfo.assets[index],
assetList: albumInfo.assets,
showStorageIndicator: showStorageIndicator,

View File

@@ -95,12 +95,12 @@ class SharingPage extends HookConsumerWidget {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Padding(
padding: EdgeInsets.only(left: 5.0, bottom: 5),
Padding(
padding: const EdgeInsets.only(left: 5.0, bottom: 5),
child: Icon(
Icons.offline_share_outlined,
size: 50,
// color: Theme.of(context).primaryColor,
color: Theme.of(context).primaryColor,
),
),
Padding(

View File

@@ -1,6 +1,7 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
import 'package:photo_view/photo_view.dart';
enum _RemoteImageStatus { empty, thumbnail, preview, full }
@@ -63,11 +64,13 @@ class _RemotePhotoViewState extends State<RemotePhotoView> {
widget.onLoadingCompleted();
}
CachedNetworkImageProvider _authorizedImageProvider(String url) {
CachedNetworkImageProvider _authorizedImageProvider(
String url, String cacheKey, BaseCacheManager? cacheManager) {
return CachedNetworkImageProvider(
url,
headers: {"Authorization": widget.authToken},
cacheKey: url,
cacheKey: cacheKey,
cacheManager: cacheManager,
);
}
@@ -101,8 +104,11 @@ class _RemotePhotoViewState extends State<RemotePhotoView> {
}
void _loadImages() {
CachedNetworkImageProvider thumbnailProvider =
_authorizedImageProvider(widget.thumbnailUrl);
CachedNetworkImageProvider thumbnailProvider = _authorizedImageProvider(
widget.thumbnailUrl,
widget.cacheKey,
widget.thumbnailCacheManager,
);
_imageProvider = thumbnailProvider;
thumbnailProvider.resolve(const ImageConfiguration()).addListener(
@@ -115,8 +121,11 @@ class _RemotePhotoViewState extends State<RemotePhotoView> {
);
if (widget.previewUrl != null) {
CachedNetworkImageProvider previewProvider =
_authorizedImageProvider(widget.previewUrl!);
CachedNetworkImageProvider previewProvider = _authorizedImageProvider(
widget.previewUrl!,
"${widget.cacheKey}_previewStage",
widget.previewCacheManager,
);
previewProvider.resolve(const ImageConfiguration()).addListener(
ImageStreamListener((ImageInfo imageInfo, _) {
_performStateTransition(_RemoteImageStatus.preview, previewProvider);
@@ -124,8 +133,11 @@ class _RemotePhotoViewState extends State<RemotePhotoView> {
);
}
CachedNetworkImageProvider fullProvider =
_authorizedImageProvider(widget.imageUrl);
CachedNetworkImageProvider fullProvider = _authorizedImageProvider(
widget.imageUrl,
"${widget.cacheKey}_fullStage",
widget.fullCacheManager,
);
fullProvider.resolve(const ImageConfiguration()).addListener(
ImageStreamListener((ImageInfo imageInfo, _) {
_performStateTransition(_RemoteImageStatus.full, fullProvider);
@@ -153,6 +165,10 @@ class RemotePhotoView extends StatefulWidget {
this.previewUrl,
required this.onLoadingCompleted,
required this.onLoadingStart,
this.thumbnailCacheManager,
this.previewCacheManager,
this.fullCacheManager,
required this.cacheKey,
}) : super(key: key);
final String thumbnailUrl;
@@ -161,6 +177,10 @@ class RemotePhotoView extends StatefulWidget {
final String? previewUrl;
final Function onLoadingCompleted;
final Function onLoadingStart;
final BaseCacheManager? thumbnailCacheManager;
final BaseCacheManager? previewCacheManager;
final BaseCacheManager? fullCacheManager;
final String cacheKey;
final void Function() onSwipeDown;
final void Function() onSwipeUp;

View File

@@ -1,5 +1,3 @@
import 'dart:developer';
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
@@ -12,7 +10,7 @@ class TopControlAppBar extends ConsumerWidget with PreferredSizeWidget {
required this.onMoreInfoPressed,
required this.onDownloadPressed,
required this.onSharePressed,
this.loading = false
this.loading = false,
}) : super(key: key);
final AssetResponseDto asset;
@@ -26,6 +24,8 @@ class TopControlAppBar extends ConsumerWidget with PreferredSizeWidget {
double iconSize = 18.0;
return AppBar(
// iconTheme: IconThemeData(color: Colors.grey[100]),
// actionsIconTheme: IconThemeData(color: Colors.grey[100]),
foregroundColor: Colors.grey[100],
toolbarHeight: 60,
backgroundColor: Colors.black,
@@ -33,37 +33,32 @@ class TopControlAppBar extends ConsumerWidget with PreferredSizeWidget {
onPressed: () {
AutoRouter.of(context).pop();
},
icon: const Icon(
icon: Icon(
Icons.arrow_back_ios_new_rounded,
size: 20.0,
color: Colors.grey[200],
),
),
actions: [
if (loading) Center(
child: Container(
margin: const EdgeInsets.symmetric(horizontal: 15.0),
width: iconSize,
height: iconSize,
child: const CircularProgressIndicator(strokeWidth: 2.0),
if (loading)
Center(
child: Container(
margin: const EdgeInsets.symmetric(horizontal: 15.0),
width: iconSize,
height: iconSize,
child: const CircularProgressIndicator(strokeWidth: 2.0),
),
),
) ,
IconButton(
iconSize: iconSize,
splashRadius: iconSize,
onPressed: () {
onDownloadPressed();
},
icon: const Icon(Icons.cloud_download_rounded),
),
IconButton(
iconSize: iconSize,
splashRadius: iconSize,
onPressed: () {
log("favorite");
},
icon: asset.isFavorite
? const Icon(Icons.favorite_rounded)
: const Icon(Icons.favorite_border_rounded),
icon: Icon(
Icons.cloud_download_rounded,
color: Colors.grey[200],
),
),
IconButton(
iconSize: iconSize,
@@ -71,7 +66,10 @@ class TopControlAppBar extends ConsumerWidget with PreferredSizeWidget {
onPressed: () {
onSharePressed();
},
icon: const Icon(Icons.share),
icon: Icon(
Icons.share,
color: Colors.grey[200],
),
),
IconButton(
iconSize: iconSize,
@@ -79,7 +77,10 @@ class TopControlAppBar extends ConsumerWidget with PreferredSizeWidget {
onPressed: () {
onMoreInfoPressed();
},
icon: const Icon(Icons.more_horiz_rounded),
icon: Icon(
Icons.more_horiz_rounded,
color: Colors.grey[200],
),
)
],
);

View File

@@ -8,6 +8,7 @@ import 'package:immich_mobile/modules/asset_viewer/ui/download_loading_indicator
import 'package:immich_mobile/modules/asset_viewer/ui/exif_bottom_sheet.dart';
import 'package:immich_mobile/modules/asset_viewer/ui/remote_photo_view.dart';
import 'package:immich_mobile/modules/home/services/asset.service.dart';
import 'package:immich_mobile/shared/services/cache.service.dart';
import 'package:immich_mobile/utils/image_url_builder.dart';
import 'package:openapi/api.dart';
@@ -40,6 +41,7 @@ class ImageViewerPage extends HookConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
final downloadAssetStatus =
ref.watch(imageViewerStateProvider).downloadAssetStatus;
final cacheService = ref.watch(cacheServiceProvider);
getAssetExif() async {
assetDetail =
@@ -73,6 +75,7 @@ class ImageViewerPage extends HookConsumerWidget {
tag: heroTag,
child: RemotePhotoView(
thumbnailUrl: getThumbnailUrl(asset),
cacheKey: asset.id,
imageUrl: getImageUrl(asset),
previewUrl: threeStageLoading
? getThumbnailUrl(asset, type: ThumbnailFormat.JPEG)
@@ -84,6 +87,12 @@ class ImageViewerPage extends HookConsumerWidget {
onSwipeUp: () => showInfo(),
onLoadingCompleted: onLoadingCompleted,
onLoadingStart: onLoadingStart,
thumbnailCacheManager:
cacheService.getCache(CacheType.thumbnail),
previewCacheManager:
cacheService.getCache(CacheType.imageViewerPreview),
fullCacheManager:
cacheService.getCache(CacheType.imageViewerFull),
),
),
),

View File

@@ -122,8 +122,8 @@ class BackgroundService {
}
}
/// Opens an activity to let the user disable battery optimizations for Immich
Future<bool> disableBatteryOptimizations() async {
/// Returns `true` if battery optimizations are disabled
Future<bool> isIgnoringBatteryOptimizations() async {
if (!Platform.isAndroid) {
return true;
}
@@ -131,12 +131,8 @@ class BackgroundService {
if (!_isForegroundInitialized) {
await _initialize();
}
final String message =
"backup_background_service_disable_battery_optimizations".tr();
return await _foregroundChannel.invokeMethod(
'disableBatteryOptimizations',
message,
);
return await _foregroundChannel
.invokeMethod('isIgnoringBatteryOptimizations');
} catch (error) {
return false;
}

View File

@@ -117,6 +117,7 @@ class BackupNotifier extends StateNotifier<BackUpState> {
bool? requireWifi,
bool? requireCharging,
required void Function(String msg) onError,
required void Function() onBatteryInfo,
}) async {
assert(enabled != null || requireWifi != null || requireCharging != null);
if (Platform.isAndroid) {
@@ -131,14 +132,21 @@ class BackupNotifier extends StateNotifier<BackUpState> {
if (state.backgroundBackup) {
if (!wasEnabled) {
await _backgroundService.disableBatteryOptimizations();
if (!await _backgroundService.isIgnoringBatteryOptimizations()) {
onBatteryInfo();
}
}
final bool success = await _backgroundService.stopService() &&
await _backgroundService.startService(
requireUnmetered: state.backupRequireWifi,
requireCharging: state.backupRequireCharging,
);
if (!success) {
if (success) {
await Hive.box(backgroundBackupInfoBox)
.put(backupRequireWifi, state.backupRequireWifi);
await Hive.box(backgroundBackupInfoBox)
.put(backupRequireCharging, state.backupRequireCharging);
} else {
state = state.copyWith(
backgroundBackup: wasEnabled,
backupRequireWifi: wasWifi,
@@ -549,10 +557,13 @@ class BackupNotifier extends StateNotifier<BackUpState> {
albums.lastExcludedBackupTime,
);
}
final Box backgroundBox = await Hive.openBox(backgroundBackupInfoBox);
state = state.copyWith(
backupProgress: previous,
selectedBackupAlbums: selectedAlbums,
excludedBackupAlbums: excludedAlbums,
backupRequireWifi: backgroundBox.get(backupRequireWifi),
backupRequireCharging: backgroundBox.get(backupRequireCharging),
);
}
return _resumeBackup();
@@ -590,6 +601,13 @@ class BackupNotifier extends StateNotifier<BackUpState> {
} catch (error) {
debugPrint("[_notifyBackgroundServiceCanRun] failed to close box");
}
try {
if (Hive.isBoxOpen(backgroundBackupInfoBox)) {
await Hive.box(backgroundBackupInfoBox).close();
}
} catch (error) {
debugPrint("[_notifyBackgroundServiceCanRun] failed to close box");
}
_backgroundService.releaseLock();
}
}

View File

@@ -14,6 +14,7 @@ import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/shared/providers/websocket.provider.dart';
import 'package:immich_mobile/modules/backup/ui/backup_info_card.dart';
import 'package:percent_indicator/linear_percent_indicator.dart';
import 'package:url_launcher/url_launcher.dart';
class BackupControllerPage extends HookConsumerWidget {
const BackupControllerPage({Key? key}) : super(key: key);
@@ -156,6 +157,46 @@ class BackupControllerPage extends HookConsumerWidget {
ScaffoldMessenger.of(context).showSnackBar(snackBar);
}
void _showBatteryOptimizationInfoToUser() {
final buttonTextColor = Theme.of(context).primaryColor;
showDialog<void>(
context: context,
barrierDismissible: false,
builder: (BuildContext context) {
return AlertDialog(
title: const Text(
'backup_controller_page_background_battery_info_title',
).tr(),
content: SingleChildScrollView(
child: const Text(
'backup_controller_page_background_battery_info_message',
).tr(),
),
actions: [
TextButton(
onPressed: () => launchUrl(
Uri.parse('https://dontkillmyapp.com'),
mode: LaunchMode.externalApplication),
child: Text(
"backup_controller_page_background_battery_info_link",
style: TextStyle(color: buttonTextColor),
).tr(),
),
TextButton(
child: Text(
'backup_controller_page_background_battery_info_ok',
style: TextStyle(color: buttonTextColor),
).tr(),
onPressed: () {
Navigator.of(context).pop();
},
),
],
);
},
);
}
ListTile _buildBackgroundBackupController() {
final bool isBackgroundEnabled = backupState.backgroundBackup;
final bool isWifiRequired = backupState.backupRequireWifi;
@@ -197,6 +238,7 @@ class BackupControllerPage extends HookConsumerWidget {
.configureBackgroundBackup(
requireWifi: isChecked,
onError: _showErrorToUser,
onBatteryInfo: _showBatteryOptimizationInfoToUser,
)
: null,
),
@@ -217,6 +259,7 @@ class BackupControllerPage extends HookConsumerWidget {
.configureBackgroundBackup(
requireCharging: isChecked,
onError: _showErrorToUser,
onBatteryInfo: _showBatteryOptimizationInfoToUser,
)
: null,
),
@@ -225,6 +268,7 @@ class BackupControllerPage extends HookConsumerWidget {
ref.read(backupProvider.notifier).configureBackgroundBackup(
enabled: !isBackgroundEnabled,
onError: _showErrorToUser,
onBatteryInfo: _showBatteryOptimizationInfoToUser,
),
child: Text(
isBackgroundEnabled

View File

@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/home/ui/thumbnail_image.dart';
import 'package:openapi/api.dart';
@@ -9,11 +10,13 @@ class ImageGrid extends ConsumerWidget {
final List<AssetResponseDto> sortedAssetGroup;
final int tilesPerRow;
final bool showStorageIndicator;
final BaseCacheManager? cacheManager;
ImageGrid({
Key? key,
required this.assetGroup,
required this.sortedAssetGroup,
this.cacheManager,
this.tilesPerRow = 4,
this.showStorageIndicator = true,
}) : super(key: key);
@@ -36,6 +39,7 @@ class ImageGrid extends ConsumerWidget {
child: Stack(
children: [
ThumbnailImage(
cacheManager: cacheManager,
asset: assetGroup[index],
assetList: sortedAssetGroup,
showStorageIndicator: showStorageIndicator,

View File

@@ -42,9 +42,10 @@ class ImmichSliverAppBar extends ConsumerWidget {
top: 5,
child: IconButton(
splashRadius: 25,
icon: const Icon(
icon: Icon(
Icons.face_outlined,
size: 30,
color: Theme.of(context).primaryColor,
),
onPressed: () {
Scaffold.of(context).openDrawer();
@@ -109,7 +110,10 @@ class ImmichSliverAppBar extends ConsumerWidget {
splashRadius: 25,
iconSize: 30,
icon: isEnableAutoBackup
? const Icon(Icons.backup_rounded)
? Icon(
Icons.backup_rounded,
color: Theme.of(context).primaryColor,
)
: Badge(
padding: const EdgeInsets.all(4),
elevation: 3,
@@ -120,7 +124,10 @@ class ImmichSliverAppBar extends ConsumerWidget {
size: 8,
color: Colors.indigo,
),
child: const Icon(Icons.backup_rounded),
child: Icon(
Icons.backup_rounded,
color: Theme.of(context).primaryColor,
),
),
onPressed: () async {
var onPop = await AutoRouter.of(context)

View File

@@ -2,6 +2,7 @@ import 'package:auto_route/auto_route.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
@@ -16,18 +17,18 @@ class ThumbnailImage extends HookConsumerWidget {
final AssetResponseDto asset;
final List<AssetResponseDto> assetList;
final bool showStorageIndicator;
final BaseCacheManager? cacheManager;
const ThumbnailImage(
{Key? key,
required this.asset,
required this.assetList,
this.showStorageIndicator = true})
: super(key: key);
const ThumbnailImage({
Key? key,
required this.asset,
required this.assetList,
this.cacheManager,
this.showStorageIndicator = true,
}) : super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
final cacheKey = useState(1);
var box = Hive.box(userInfoBox);
var thumbnailRequestUrl = getThumbnailUrl(asset);
var selectedAsset = ref.watch(homePageStateProvider).selectedItems;
@@ -94,7 +95,8 @@ class ThumbnailImage extends HookConsumerWidget {
: const Border(),
),
child: CachedNetworkImage(
cacheKey: "${asset.id}-${cacheKey.value}",
cacheKey: asset.id,
cacheManager: cacheManager,
width: 300,
height: 300,
memCacheHeight: asset.type == AssetTypeEnum.IMAGE ? 250 : 400,
@@ -128,17 +130,18 @@ class ThumbnailImage extends HookConsumerWidget {
child: _buildSelectionIcon(asset),
),
),
if (showStorageIndicator) Positioned(
right: 10,
bottom: 5,
child: Icon(
(deviceId != asset.deviceId)
? Icons.cloud_done_outlined
: Icons.photo_library_rounded,
color: Colors.white,
size: 18,
),
)
if (showStorageIndicator)
Positioned(
right: 10,
bottom: 5,
child: Icon(
(deviceId != asset.deviceId)
? Icons.cloud_done_outlined
: Icons.photo_library_rounded,
color: Colors.white,
size: 18,
),
)
],
),
),

View File

@@ -16,6 +16,7 @@ import 'package:immich_mobile/modules/settings/services/app_settings.service.dar
import 'package:immich_mobile/shared/providers/asset.provider.dart';
import 'package:immich_mobile/shared/providers/server_info.provider.dart';
import 'package:immich_mobile/shared/providers/websocket.provider.dart';
import 'package:immich_mobile/shared/services/cache.service.dart';
import 'package:openapi/api.dart';
class HomePage extends HookConsumerWidget {
@@ -24,6 +25,7 @@ class HomePage extends HookConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final appSettingService = ref.watch(appSettingsServiceProvider);
final cacheService = ref.watch(cacheServiceProvider);
ScrollController scrollController = useScrollController();
var assetGroupByDateTime = ref.watch(assetGroupByDateTimeProvider);
@@ -89,6 +91,7 @@ class HomePage extends HookConsumerWidget {
imageGridGroup.add(
ImageGrid(
cacheManager: cacheService.getCache(CacheType.thumbnail),
assetGroup: immichAssetList,
sortedAssetGroup: sortedAssetList,
tilesPerRow:

View File

@@ -7,7 +7,10 @@ enum AppSettingsEnum<T> {
tilesPerRow<int>("tilesPerRow", 4),
uploadErrorNotificationGracePeriod<int>(
"uploadErrorNotificationGracePeriod", 2),
storageIndicator<bool>("storageIndicator", true);
storageIndicator<bool>("storageIndicator", true),
thumbnailCacheSize<int>("thumbnailCacheSize", 10000),
imageCacheSize<int>("imageCacheSize", 350),
albumThumbnailCacheSize<int>("albumThumbnailCacheSize", 200);
const AppSettingsEnum(this.hiveKey, this.defaultValue);

View File

@@ -0,0 +1,151 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
import 'package:immich_mobile/modules/settings/ui/cache_settings/cache_settings_slider_pref.dart';
import 'package:immich_mobile/shared/services/cache.service.dart';
import 'package:immich_mobile/utils/bytes_units.dart';
class CacheSettings extends HookConsumerWidget {
const CacheSettings({
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
final CacheService cacheService = ref.watch(cacheServiceProvider);
final clearCacheState = useState(false);
Future<void> clearCache() async {
await cacheService.emptyAllCaches();
clearCacheState.value = true;
}
Widget cacheStatisticsRow(String name, CacheType type) {
final cacheSize = useState(0);
final cacheAssets = useState(0);
if (!clearCacheState.value) {
final repo = cacheService.getCacheRepo(type);
repo.open().then((_) {
cacheSize.value = repo.getCacheSize();
cacheAssets.value = repo.getNumberOfCachedObjects();
});
} else {
cacheSize.value = 0;
cacheAssets.value = 0;
}
return Container(
margin: const EdgeInsets.only(left: 20, bottom: 10),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
name,
style: const TextStyle(
fontWeight: FontWeight.bold,
),
),
const Text(
"cache_settings_statistics_assets",
style: TextStyle(color: Colors.grey),
).tr(
args: ["${cacheAssets.value}", formatBytes(cacheSize.value)],
),
],
),
);
}
return ExpansionTile(
expandedCrossAxisAlignment: CrossAxisAlignment.start,
textColor: Theme.of(context).primaryColor,
title: const Text(
'cache_settings_title',
style: TextStyle(
fontWeight: FontWeight.bold,
),
).tr(),
subtitle: const Text(
'cache_settings_subtitle',
style: TextStyle(
fontSize: 13,
),
).tr(),
children: [
const CacheSettingsSliderPref(
setting: AppSettingsEnum.thumbnailCacheSize,
translationKey: "cache_settings_thumbnail_size",
min: 1000,
max: 20000,
divisions: 19,
),
const CacheSettingsSliderPref(
setting: AppSettingsEnum.imageCacheSize,
translationKey: "cache_settings_image_cache_size",
min: 0,
max: 1000,
divisions: 20,
),
const CacheSettingsSliderPref(
setting: AppSettingsEnum.albumThumbnailCacheSize,
translationKey: "cache_settings_album_thumbnails",
min: 0,
max: 1000,
divisions: 20,
),
ListTile(
title: const Text(
"cache_settings_statistics_title",
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
),
).tr(),
),
cacheStatisticsRow(
"cache_settings_statistics_thumbnail".tr(),
CacheType.thumbnail,
),
cacheStatisticsRow(
"cache_settings_statistics_album".tr(),
CacheType.albumThumbnail,
),
cacheStatisticsRow(
"cache_settings_statistics_shared".tr(),
CacheType.sharedAlbumThumbnail,
),
cacheStatisticsRow(
"cache_settings_statistics_full".tr(),
CacheType.imageViewerFull,
),
ListTile(
title: const Text(
"cache_settings_clear_cache_button_title",
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
),
).tr(),
),
Container(
alignment: Alignment.center,
child: ElevatedButton(
onPressed: clearCache,
child: const Text(
"cache_settings_clear_cache_button",
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 12,
),
).tr(),
),
)
],
);
}
}

View File

@@ -0,0 +1,63 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart';
import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
class CacheSettingsSliderPref extends HookConsumerWidget {
final AppSettingsEnum<int> setting;
final String translationKey;
final int min;
final int max;
final int divisions;
const CacheSettingsSliderPref({
Key? key,
required this.setting,
required this.translationKey,
required this.min,
required this.max,
required this.divisions,
}) : super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
final appSettingService = ref.watch(appSettingsServiceProvider);
final itemsValue = useState(appSettingService.getSetting<int>(setting));
void sliderChanged(double value) {
itemsValue.value = value.toInt();
}
void sliderChangedEnd(double value) {
appSettingService.setSetting(setting, value.toInt());
}
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ListTile(
title: Text(
translationKey,
style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
),
).tr(args: ["${itemsValue.value.toInt()}"]),
),
Slider(
onChangeEnd: sliderChangedEnd,
onChanged: sliderChanged,
value: itemsValue.value.toDouble(),
min: min.toDouble(),
max: max.toDouble(),
divisions: divisions,
label: "${itemsValue.value.toInt()}",
activeColor: Theme.of(context).primaryColor,
),
],
);
}
}

View File

@@ -4,6 +4,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/settings/ui/asset_list_settings/asset_list_settings.dart';
import 'package:immich_mobile/modules/settings/ui/cache_settings/cache_settings.dart';
import 'package:immich_mobile/modules/settings/ui/image_viewer_quality_setting/image_viewer_quality_setting.dart';
import 'package:immich_mobile/modules/settings/ui/notification_setting/notification_setting.dart';
import 'package:immich_mobile/modules/settings/ui/theme_setting/theme_setting.dart';
@@ -41,6 +42,7 @@ class SettingsPage extends HookConsumerWidget {
const ImageViewerQualitySetting(),
const ThemeSetting(),
const AssetListSettings(),
const CacheSettings(),
if (Platform.isAndroid) const NotificationSetting(),
],
).toList(),

View File

@@ -1,21 +1,79 @@
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
import 'package:hooks_riverpod/hooks_riverpod.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/utils/immich_cache_info_repository.dart';
enum CacheType {
// Shared cache for asset thumbnails in various modules
thumbnail,
imageViewerPreview,
imageViewerFull,
albumThumbnail,
sharedAlbumThumbnail;
}
final cacheServiceProvider = Provider((_) => CacheService());
final cacheServiceProvider = Provider(
(ref) => CacheService(ref.watch(appSettingsServiceProvider)),
);
class CacheService {
final AppSettingsService _settingsService;
final _cacheRepositoryInstances = <CacheType, ImmichCacheRepository>{};
CacheService(this._settingsService);
BaseCacheManager getCache(CacheType type) {
return _getDefaultCache(type.name);
return _getDefaultCache(
type.name,
_getCacheSize(type) + 1,
getCacheRepo(type),
);
}
BaseCacheManager _getDefaultCache(String cacheName) {
return CacheManager(Config(cacheName));
ImmichCacheRepository getCacheRepo(CacheType type) {
if (!_cacheRepositoryInstances.containsKey(type)) {
final repo = ImmichCacheInfoRepository(
"cache_${type.name}",
"cacheKeys_${type.name}",
);
_cacheRepositoryInstances[type] = repo;
}
return _cacheRepositoryInstances[type]!;
}
}
Future<void> emptyAllCaches() async {
for (var type in CacheType.values) {
await getCache(type).emptyCache();
}
}
int _getCacheSize(CacheType type) {
switch (type) {
case CacheType.thumbnail:
return _settingsService.getSetting(AppSettingsEnum.thumbnailCacheSize);
case CacheType.imageViewerPreview:
case CacheType.imageViewerFull:
return _settingsService.getSetting(AppSettingsEnum.imageCacheSize);
case CacheType.sharedAlbumThumbnail:
case CacheType.albumThumbnail:
return _settingsService
.getSetting(AppSettingsEnum.albumThumbnailCacheSize);
default:
return 200;
}
}
BaseCacheManager _getDefaultCache(
String cacheName, int size, CacheInfoRepository repo) {
return CacheManager(
Config(
cacheName,
maxNrOfCacheObjects: size,
repo: repo,
),
);
}
}

View File

@@ -0,0 +1,15 @@
String formatBytes(int bytes) {
if (bytes < 1000) {
return "$bytes B";
} else if (bytes < 1000000) {
final kb = (bytes / 1000).toStringAsFixed(1);
return "$kb kB";
} else if (bytes < 1000000000) {
final mb = (bytes / 1000000).toStringAsFixed(1);
return "$mb MB";
} else {
final gb = (bytes / 1000000000).toStringAsFixed(1);
return "$gb GB";
}
}

View File

@@ -17,6 +17,9 @@ class FileHelper {
case 'png':
return {"type": "image", "subType": "png"};
case 'tif':
return {"type": "image", "subType": "tiff"};
case 'mov':
return {"type": "video", "subType": "quicktime"};
@@ -38,6 +41,9 @@ class FileHelper {
case 'webp':
return {"type": "image", "subType": "webp"};
case '3gp':
return {"type": "video", "subType": "3gpp"};
default:
return {"type": "unsupport", "subType": "unsupport"};
}

View File

@@ -0,0 +1,204 @@
import 'dart:io';
import 'dart:math';
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
import 'package:flutter_cache_manager/src/storage/cache_object.dart';
import 'package:hive/hive.dart';
import 'package:path/path.dart' as p;
import 'package:path_provider/path_provider.dart';
// Implementation of a CacheInfoRepository based on Hive
abstract class ImmichCacheRepository extends CacheInfoRepository {
int getNumberOfCachedObjects();
int getCacheSize();
}
class ImmichCacheInfoRepository extends ImmichCacheRepository {
final String hiveBoxName;
final String keyLookupHiveBoxName;
// To circumvent some of the limitations of a non-relational key-value database,
// we use two hive boxes per cache.
// [cacheObjectLookupBox] maps ids to cache objects.
// [keyLookupHiveBox] maps keys to ids.
// The lookup of a cache object by key therefore involves two steps:
// id = keyLookupHiveBox[key]
// object = cacheObjectLookupBox[id]
late Box<Map<dynamic, dynamic>> cacheObjectLookupBox;
late Box<int> keyLookupHiveBox;
ImmichCacheInfoRepository(this.hiveBoxName, this.keyLookupHiveBoxName);
@override
Future<bool> close() async {
await cacheObjectLookupBox.close();
return true;
}
@override
Future<int> delete(int id) async {
if (cacheObjectLookupBox.containsKey(id)) {
await cacheObjectLookupBox.delete(id);
return 1;
}
return 0;
}
@override
Future<int> deleteAll(Iterable<int> ids) async {
int deleted = 0;
for (var id in ids) {
if (cacheObjectLookupBox.containsKey(id)) {
deleted++;
await cacheObjectLookupBox.delete(id);
}
}
return deleted;
}
@override
Future<void> deleteDataFile() async {
await cacheObjectLookupBox.clear();
await keyLookupHiveBox.clear();
}
@override
Future<bool> exists() async {
return cacheObjectLookupBox.isNotEmpty && keyLookupHiveBox.isNotEmpty;
}
@override
Future<CacheObject?> get(String key) async {
if (!keyLookupHiveBox.containsKey(key)) {
return null;
}
int id = keyLookupHiveBox.get(key)!;
if (!cacheObjectLookupBox.containsKey(id)) {
keyLookupHiveBox.delete(key);
return null;
}
return _deserialize(cacheObjectLookupBox.get(id)!);
}
@override
Future<List<CacheObject>> getAllObjects() async {
return cacheObjectLookupBox.values.map(_deserialize).toList();
}
@override
Future<List<CacheObject>> getObjectsOverCapacity(int capacity) async {
if (cacheObjectLookupBox.length <= capacity) {
return List.empty();
}
var values = cacheObjectLookupBox.values.map(_deserialize).toList();
values.sort((CacheObject a, CacheObject b) {
final aTouched = a.touched ?? DateTime.fromMicrosecondsSinceEpoch(0);
final bTouched = b.touched ?? DateTime.fromMicrosecondsSinceEpoch(0);
return aTouched.compareTo(bTouched);
});
return values.skip(capacity).take(10).toList();
}
@override
Future<List<CacheObject>> getOldObjects(Duration maxAge) async {
return cacheObjectLookupBox.values
.map(_deserialize)
.where((CacheObject element) {
DateTime touched =
element.touched ?? DateTime.fromMicrosecondsSinceEpoch(0);
return touched.isBefore(DateTime.now().subtract(maxAge));
}).toList();
}
@override
Future<CacheObject> insert(CacheObject cacheObject,
{bool setTouchedToNow = true}) async {
int newId = keyLookupHiveBox.length == 0
? 0
: keyLookupHiveBox.values.reduce(max) + 1;
cacheObject = cacheObject.copyWith(id: newId);
keyLookupHiveBox.put(cacheObject.key, newId);
cacheObjectLookupBox.put(newId, cacheObject.toMap());
return cacheObject;
}
@override
Future<bool> open() async {
cacheObjectLookupBox = await Hive.openBox(hiveBoxName);
keyLookupHiveBox = await Hive.openBox(keyLookupHiveBoxName);
// The cache might have cleared by the operating system.
// This could create inconsistencies between the file system cache and database.
// To check whether the cache was cleared, a file within the cache directory
// is created for each database. If the file is absent, the cache was cleared and therefore
// the database has to be cleared as well.
if (!await _checkAndCreateAnchorFile()) {
await cacheObjectLookupBox.clear();
await keyLookupHiveBox.clear();
}
return cacheObjectLookupBox.isOpen;
}
@override
Future<int> update(CacheObject cacheObject,
{bool setTouchedToNow = true}) async {
if (cacheObject.id != null) {
cacheObjectLookupBox.put(cacheObject.id, cacheObject.toMap());
return 1;
}
return 0;
}
@override
Future updateOrInsert(CacheObject cacheObject) {
if (cacheObject.id == null) {
return insert(cacheObject);
} else {
return update(cacheObject);
}
}
@override
int getNumberOfCachedObjects() {
return cacheObjectLookupBox.length;
}
@override
int getCacheSize() {
final cacheElementsWithSize =
cacheObjectLookupBox.values.map(_deserialize).map((e) => e.length ?? 0);
if (cacheElementsWithSize.isEmpty) {
return 0;
}
return cacheElementsWithSize.reduce((value, element) => value + element);
}
CacheObject _deserialize(Map serData) {
Map<String, dynamic> converted = {};
serData.forEach((key, value) {
converted[key.toString()] = value;
});
return CacheObject.fromMap(converted);
}
Future<bool> _checkAndCreateAnchorFile() async {
final tmpDir = await getTemporaryDirectory();
final cacheFile = File(p.join(tmpDir.path, "$hiveBoxName.tmp"));
if (await cacheFile.exists()) {
return true;
}
await cacheFile.create();
return false;
}
}

View File

@@ -6,8 +6,12 @@ doc/AddAssetsDto.md
doc/AddUsersDto.md
doc/AdminSignupResponseDto.md
doc/AlbumApi.md
doc/AlbumCountResponseDto.md
doc/AlbumResponseDto.md
doc/AssetApi.md
doc/AssetCountByTimeBucket.md
doc/AssetCountByTimeBucketResponseDto.md
doc/AssetCountByUserIdResponseDto.md
doc/AssetFileUploadResponseDto.md
doc/AssetResponseDto.md
doc/AssetTypeEnum.md
@@ -27,6 +31,8 @@ doc/DeviceInfoApi.md
doc/DeviceInfoResponseDto.md
doc/DeviceTypeEnum.md
doc/ExifResponseDto.md
doc/GetAssetByTimeBucketDto.md
doc/GetAssetCountByTimeBucketDto.md
doc/LoginCredentialDto.md
doc/LoginResponseDto.md
doc/LogoutResponseDto.md
@@ -39,6 +45,7 @@ doc/ServerVersionReponseDto.md
doc/SignUpDto.md
doc/SmartInfoResponseDto.md
doc/ThumbnailFormat.md
doc/TimeGroupEnum.md
doc/UpdateAlbumDto.md
doc/UpdateDeviceInfoDto.md
doc/UpdateUserDto.md
@@ -65,7 +72,11 @@ lib/auth/oauth.dart
lib/model/add_assets_dto.dart
lib/model/add_users_dto.dart
lib/model/admin_signup_response_dto.dart
lib/model/album_count_response_dto.dart
lib/model/album_response_dto.dart
lib/model/asset_count_by_time_bucket.dart
lib/model/asset_count_by_time_bucket_response_dto.dart
lib/model/asset_count_by_user_id_response_dto.dart
lib/model/asset_file_upload_response_dto.dart
lib/model/asset_response_dto.dart
lib/model/asset_type_enum.dart
@@ -83,6 +94,8 @@ lib/model/delete_asset_status.dart
lib/model/device_info_response_dto.dart
lib/model/device_type_enum.dart
lib/model/exif_response_dto.dart
lib/model/get_asset_by_time_bucket_dto.dart
lib/model/get_asset_count_by_time_bucket_dto.dart
lib/model/login_credential_dto.dart
lib/model/login_response_dto.dart
lib/model/logout_response_dto.dart
@@ -94,6 +107,7 @@ lib/model/server_version_reponse_dto.dart
lib/model/sign_up_dto.dart
lib/model/smart_info_response_dto.dart
lib/model/thumbnail_format.dart
lib/model/time_group_enum.dart
lib/model/update_album_dto.dart
lib/model/update_device_info_dto.dart
lib/model/update_user_dto.dart
@@ -101,3 +115,4 @@ lib/model/user_count_response_dto.dart
lib/model/user_response_dto.dart
lib/model/validate_access_token_response_dto.dart
pubspec.yaml
test/asset_count_by_user_id_response_dto_test.dart

View File

@@ -69,6 +69,7 @@ Class | Method | HTTP request | Description
*AlbumApi* | [**addUsersToAlbum**](doc//AlbumApi.md#adduserstoalbum) | **PUT** /album/{albumId}/users |
*AlbumApi* | [**createAlbum**](doc//AlbumApi.md#createalbum) | **POST** /album |
*AlbumApi* | [**deleteAlbum**](doc//AlbumApi.md#deletealbum) | **DELETE** /album/{albumId} |
*AlbumApi* | [**getAlbumCountByUserId**](doc//AlbumApi.md#getalbumcountbyuserid) | **GET** /album/count-by-user-id |
*AlbumApi* | [**getAlbumInfo**](doc//AlbumApi.md#getalbuminfo) | **GET** /album/{albumId} |
*AlbumApi* | [**getAllAlbums**](doc//AlbumApi.md#getallalbums) | **GET** /album |
*AlbumApi* | [**removeAssetFromAlbum**](doc//AlbumApi.md#removeassetfromalbum) | **DELETE** /album/{albumId}/assets |
@@ -79,10 +80,13 @@ Class | Method | HTTP request | Description
*AssetApi* | [**downloadFile**](doc//AssetApi.md#downloadfile) | **GET** /asset/download |
*AssetApi* | [**getAllAssets**](doc//AssetApi.md#getallassets) | **GET** /asset |
*AssetApi* | [**getAssetById**](doc//AssetApi.md#getassetbyid) | **GET** /asset/assetById/{assetId} |
*AssetApi* | [**getAssetSearchTerms**](doc//AssetApi.md#getassetsearchterms) | **GET** /asset/searchTerm |
*AssetApi* | [**getAssetByTimeBucket**](doc//AssetApi.md#getassetbytimebucket) | **POST** /asset/time-bucket |
*AssetApi* | [**getAssetCountByTimeBucket**](doc//AssetApi.md#getassetcountbytimebucket) | **POST** /asset/count-by-time-bucket |
*AssetApi* | [**getAssetCountByUserId**](doc//AssetApi.md#getassetcountbyuserid) | **GET** /asset/count-by-user-id |
*AssetApi* | [**getAssetSearchTerms**](doc//AssetApi.md#getassetsearchterms) | **GET** /asset/search-terms |
*AssetApi* | [**getAssetThumbnail**](doc//AssetApi.md#getassetthumbnail) | **GET** /asset/thumbnail/{assetId} |
*AssetApi* | [**getCuratedLocations**](doc//AssetApi.md#getcuratedlocations) | **GET** /asset/allLocation |
*AssetApi* | [**getCuratedObjects**](doc//AssetApi.md#getcuratedobjects) | **GET** /asset/allObjects |
*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* | [**searchAsset**](doc//AssetApi.md#searchasset) | **POST** /asset/search |
*AssetApi* | [**serveFile**](doc//AssetApi.md#servefile) | **GET** /asset/file |
@@ -111,7 +115,11 @@ Class | Method | HTTP request | Description
- [AddAssetsDto](doc//AddAssetsDto.md)
- [AddUsersDto](doc//AddUsersDto.md)
- [AdminSignupResponseDto](doc//AdminSignupResponseDto.md)
- [AlbumCountResponseDto](doc//AlbumCountResponseDto.md)
- [AlbumResponseDto](doc//AlbumResponseDto.md)
- [AssetCountByTimeBucket](doc//AssetCountByTimeBucket.md)
- [AssetCountByTimeBucketResponseDto](doc//AssetCountByTimeBucketResponseDto.md)
- [AssetCountByUserIdResponseDto](doc//AssetCountByUserIdResponseDto.md)
- [AssetFileUploadResponseDto](doc//AssetFileUploadResponseDto.md)
- [AssetResponseDto](doc//AssetResponseDto.md)
- [AssetTypeEnum](doc//AssetTypeEnum.md)
@@ -129,6 +137,8 @@ Class | Method | HTTP request | Description
- [DeviceInfoResponseDto](doc//DeviceInfoResponseDto.md)
- [DeviceTypeEnum](doc//DeviceTypeEnum.md)
- [ExifResponseDto](doc//ExifResponseDto.md)
- [GetAssetByTimeBucketDto](doc//GetAssetByTimeBucketDto.md)
- [GetAssetCountByTimeBucketDto](doc//GetAssetCountByTimeBucketDto.md)
- [LoginCredentialDto](doc//LoginCredentialDto.md)
- [LoginResponseDto](doc//LoginResponseDto.md)
- [LogoutResponseDto](doc//LogoutResponseDto.md)
@@ -140,6 +150,7 @@ Class | Method | HTTP request | Description
- [SignUpDto](doc//SignUpDto.md)
- [SmartInfoResponseDto](doc//SmartInfoResponseDto.md)
- [ThumbnailFormat](doc//ThumbnailFormat.md)
- [TimeGroupEnum](doc//TimeGroupEnum.md)
- [UpdateAlbumDto](doc//UpdateAlbumDto.md)
- [UpdateDeviceInfoDto](doc//UpdateDeviceInfoDto.md)
- [UpdateUserDto](doc//UpdateUserDto.md)

View File

@@ -13,6 +13,7 @@ Method | HTTP request | Description
[**addUsersToAlbum**](AlbumApi.md#adduserstoalbum) | **PUT** /album/{albumId}/users |
[**createAlbum**](AlbumApi.md#createalbum) | **POST** /album |
[**deleteAlbum**](AlbumApi.md#deletealbum) | **DELETE** /album/{albumId} |
[**getAlbumCountByUserId**](AlbumApi.md#getalbumcountbyuserid) | **GET** /album/count-by-user-id |
[**getAlbumInfo**](AlbumApi.md#getalbuminfo) | **GET** /album/{albumId} |
[**getAllAlbums**](AlbumApi.md#getallalbums) | **GET** /album |
[**removeAssetFromAlbum**](AlbumApi.md#removeassetfromalbum) | **DELETE** /album/{albumId}/assets |
@@ -211,6 +212,49 @@ void (empty response body)
[[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)
# **getAlbumCountByUserId**
> AlbumCountResponseDto getAlbumCountByUserId()
### 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 = AlbumApi();
try {
final result = api_instance.getAlbumCountByUserId();
print(result);
} catch (e) {
print('Exception when calling AlbumApi->getAlbumCountByUserId: $e\n');
}
```
### Parameters
This endpoint does not need any parameter.
### Return type
[**AlbumCountResponseDto**](AlbumCountResponseDto.md)
### Authorization
[bearer](../README.md#bearer)
### HTTP request headers
- **Content-Type**: Not defined
- **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)
# **getAlbumInfo**
> AlbumResponseDto getAlbumInfo(albumId)
@@ -259,7 +303,7 @@ 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)
# **getAllAlbums**
> List<AlbumResponseDto> getAllAlbums(shared)
> List<AlbumResponseDto> getAllAlbums(shared, assetId)
@@ -275,9 +319,10 @@ import 'package:openapi/api.dart';
final api_instance = AlbumApi();
final shared = true; // bool |
final assetId = assetId_example; // String | Only returns albums that contain the asset Ignores the shared parameter undefined: get all albums
try {
final result = api_instance.getAllAlbums(shared);
final result = api_instance.getAllAlbums(shared, assetId);
print(result);
} catch (e) {
print('Exception when calling AlbumApi->getAllAlbums: $e\n');
@@ -289,6 +334,7 @@ try {
Name | Type | Description | Notes
------------- | ------------- | ------------- | -------------
**shared** | **bool**| | [optional]
**assetId** | **String**| Only returns albums that contain the asset Ignores the shared parameter undefined: get all albums | [optional]
### Return type

View File

@@ -0,0 +1,17 @@
# openapi.model.AlbumCountResponseDto
## Load the model package
```dart
import 'package:openapi/api.dart';
```
## Properties
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**owned** | **int** | |
**shared** | **int** | |
**sharing** | **int** | |
[[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,10 +14,13 @@ Method | HTTP request | Description
[**downloadFile**](AssetApi.md#downloadfile) | **GET** /asset/download |
[**getAllAssets**](AssetApi.md#getallassets) | **GET** /asset |
[**getAssetById**](AssetApi.md#getassetbyid) | **GET** /asset/assetById/{assetId} |
[**getAssetSearchTerms**](AssetApi.md#getassetsearchterms) | **GET** /asset/searchTerm |
[**getAssetByTimeBucket**](AssetApi.md#getassetbytimebucket) | **POST** /asset/time-bucket |
[**getAssetCountByTimeBucket**](AssetApi.md#getassetcountbytimebucket) | **POST** /asset/count-by-time-bucket |
[**getAssetCountByUserId**](AssetApi.md#getassetcountbyuserid) | **GET** /asset/count-by-user-id |
[**getAssetSearchTerms**](AssetApi.md#getassetsearchterms) | **GET** /asset/search-terms |
[**getAssetThumbnail**](AssetApi.md#getassetthumbnail) | **GET** /asset/thumbnail/{assetId} |
[**getCuratedLocations**](AssetApi.md#getcuratedlocations) | **GET** /asset/allLocation |
[**getCuratedObjects**](AssetApi.md#getcuratedobjects) | **GET** /asset/allObjects |
[**getCuratedLocations**](AssetApi.md#getcuratedlocations) | **GET** /asset/curated-locations |
[**getCuratedObjects**](AssetApi.md#getcuratedobjects) | **GET** /asset/curated-objects |
[**getUserAssetsByDeviceId**](AssetApi.md#getuserassetsbydeviceid) | **GET** /asset/{deviceId} |
[**searchAsset**](AssetApi.md#searchasset) | **POST** /asset/search |
[**serveFile**](AssetApi.md#servefile) | **GET** /asset/file |
@@ -267,6 +270,143 @@ 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)
# **getAssetByTimeBucket**
> List<AssetResponseDto> getAssetByTimeBucket(getAssetByTimeBucketDto)
### 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 getAssetByTimeBucketDto = GetAssetByTimeBucketDto(); // GetAssetByTimeBucketDto |
try {
final result = api_instance.getAssetByTimeBucket(getAssetByTimeBucketDto);
print(result);
} catch (e) {
print('Exception when calling AssetApi->getAssetByTimeBucket: $e\n');
}
```
### Parameters
Name | Type | Description | Notes
------------- | ------------- | ------------- | -------------
**getAssetByTimeBucketDto** | [**GetAssetByTimeBucketDto**](GetAssetByTimeBucketDto.md)| |
### Return type
[**List<AssetResponseDto>**](AssetResponseDto.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)
# **getAssetCountByTimeBucket**
> AssetCountByTimeBucketResponseDto getAssetCountByTimeBucket(getAssetCountByTimeBucketDto)
### 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 getAssetCountByTimeBucketDto = GetAssetCountByTimeBucketDto(); // GetAssetCountByTimeBucketDto |
try {
final result = api_instance.getAssetCountByTimeBucket(getAssetCountByTimeBucketDto);
print(result);
} catch (e) {
print('Exception when calling AssetApi->getAssetCountByTimeBucket: $e\n');
}
```
### Parameters
Name | Type | Description | Notes
------------- | ------------- | ------------- | -------------
**getAssetCountByTimeBucketDto** | [**GetAssetCountByTimeBucketDto**](GetAssetCountByTimeBucketDto.md)| |
### Return type
[**AssetCountByTimeBucketResponseDto**](AssetCountByTimeBucketResponseDto.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)
# **getAssetCountByUserId**
> AssetCountByUserIdResponseDto getAssetCountByUserId()
### 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();
try {
final result = api_instance.getAssetCountByUserId();
print(result);
} catch (e) {
print('Exception when calling AssetApi->getAssetCountByUserId: $e\n');
}
```
### Parameters
This endpoint does not need any parameter.
### Return type
[**AssetCountByUserIdResponseDto**](AssetCountByUserIdResponseDto.md)
### Authorization
[bearer](../README.md#bearer)
### HTTP request headers
- **Content-Type**: Not defined
- **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)
# **getAssetSearchTerms**
> List<String> getAssetSearchTerms()

View File

@@ -0,0 +1,16 @@
# openapi.model.AssetCountByTimeBucket
## Load the model package
```dart
import 'package:openapi/api.dart';
```
## Properties
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**timeBucket** | **String** | |
**count** | **int** | |
[[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

@@ -0,0 +1,16 @@
# openapi.model.AssetCountByTimeBucketResponseDto
## Load the model package
```dart
import 'package:openapi/api.dart';
```
## Properties
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**totalCount** | **int** | |
**buckets** | [**List<AssetCountByTimeBucket>**](AssetCountByTimeBucket.md) | | [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

@@ -0,0 +1,16 @@
# openapi.model.AssetCountByTimeGroupDto
## Load the model package
```dart
import 'package:openapi/api.dart';
```
## Properties
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**timeGroup** | **String** | |
**count** | **int** | |
[[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

@@ -0,0 +1,16 @@
# openapi.model.AssetCountByTimeGroupResponseDto
## Load the model package
```dart
import 'package:openapi/api.dart';
```
## Properties
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**count** | **int** | |
**buckets** | [**List<AssetCountByTimeBucketResponseDto>**](AssetCountByTimeBucketResponseDto.md) | | [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

@@ -0,0 +1,16 @@
# openapi.model.AssetCountByUserIdResponseDto
## Load the model package
```dart
import 'package:openapi/api.dart';
```
## Properties
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**photos** | **int** | |
**videos** | **int** | |
[[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

@@ -0,0 +1,15 @@
# openapi.model.GetAssetByTimeBucketDto
## Load the model package
```dart
import 'package:openapi/api.dart';
```
## Properties
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**timeBucket** | **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

@@ -0,0 +1,15 @@
# openapi.model.GetAssetCountByTimeBucketDto
## Load the model package
```dart
import 'package:openapi/api.dart';
```
## Properties
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**timeGroup** | [**TimeGroupEnum**](TimeGroupEnum.md) | |
[[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

@@ -0,0 +1,15 @@
# openapi.model.GetAssetCountByTimeGroupDto
## Load the model package
```dart
import 'package:openapi/api.dart';
```
## Properties
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**timeGroup** | [**TimeGroupEnum**](TimeGroupEnum.md) | |
[[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

@@ -0,0 +1,14 @@
# openapi.model.TimeBucketEnum
## Load the model package
```dart
import 'package:openapi/api.dart';
```
## Properties
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
[[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

@@ -0,0 +1,14 @@
# openapi.model.TimeGroupEnum
## Load the model package
```dart
import 'package:openapi/api.dart';
```
## Properties
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
[[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

@@ -37,7 +37,11 @@ part 'api/user_api.dart';
part 'model/add_assets_dto.dart';
part 'model/add_users_dto.dart';
part 'model/admin_signup_response_dto.dart';
part 'model/album_count_response_dto.dart';
part 'model/album_response_dto.dart';
part 'model/asset_count_by_time_bucket.dart';
part 'model/asset_count_by_time_bucket_response_dto.dart';
part 'model/asset_count_by_user_id_response_dto.dart';
part 'model/asset_file_upload_response_dto.dart';
part 'model/asset_response_dto.dart';
part 'model/asset_type_enum.dart';
@@ -55,6 +59,8 @@ part 'model/delete_asset_status.dart';
part 'model/device_info_response_dto.dart';
part 'model/device_type_enum.dart';
part 'model/exif_response_dto.dart';
part 'model/get_asset_by_time_bucket_dto.dart';
part 'model/get_asset_count_by_time_bucket_dto.dart';
part 'model/login_credential_dto.dart';
part 'model/login_response_dto.dart';
part 'model/logout_response_dto.dart';
@@ -66,6 +72,7 @@ part 'model/server_version_reponse_dto.dart';
part 'model/sign_up_dto.dart';
part 'model/smart_info_response_dto.dart';
part 'model/thumbnail_format.dart';
part 'model/time_group_enum.dart';
part 'model/update_album_dto.dart';
part 'model/update_device_info_dto.dart';
part 'model/update_user_dto.dart';

View File

@@ -207,6 +207,47 @@ class AlbumApi {
}
}
/// Performs an HTTP 'GET /album/count-by-user-id' operation and returns the [Response].
Future<Response> getAlbumCountByUserIdWithHttpInfo() async {
// ignore: prefer_const_declarations
final path = r'/album/count-by-user-id';
// ignore: prefer_final_locals
Object? postBody;
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
const contentTypes = <String>[];
return apiClient.invokeAPI(
path,
'GET',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
);
}
Future<AlbumCountResponseDto?> getAlbumCountByUserId() async {
final response = await getAlbumCountByUserIdWithHttpInfo();
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), 'AlbumCountResponseDto',) as AlbumCountResponseDto;
}
return null;
}
/// Performs an HTTP 'GET /album/{albumId}' operation and returns the [Response].
/// Parameters:
///
@@ -259,7 +300,10 @@ class AlbumApi {
/// Parameters:
///
/// * [bool] shared:
Future<Response> getAllAlbumsWithHttpInfo({ bool? shared, }) async {
///
/// * [String] assetId:
/// Only returns albums that contain the asset Ignores the shared parameter undefined: get all albums
Future<Response> getAllAlbumsWithHttpInfo({ bool? shared, String? assetId, }) async {
// ignore: prefer_const_declarations
final path = r'/album';
@@ -273,6 +317,9 @@ class AlbumApi {
if (shared != null) {
queryParams.addAll(_queryParams('', 'shared', shared));
}
if (assetId != null) {
queryParams.addAll(_queryParams('', 'assetId', assetId));
}
const contentTypes = <String>[];
@@ -291,8 +338,11 @@ class AlbumApi {
/// Parameters:
///
/// * [bool] shared:
Future<List<AlbumResponseDto>?> getAllAlbums({ bool? shared, }) async {
final response = await getAllAlbumsWithHttpInfo( shared: shared, );
///
/// * [String] assetId:
/// Only returns albums that contain the asset Ignores the shared parameter undefined: get all albums
Future<List<AlbumResponseDto>?> getAllAlbums({ bool? shared, String? assetId, }) async {
final response = await getAllAlbumsWithHttpInfo( shared: shared, assetId: assetId, );
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
}

View File

@@ -298,10 +298,148 @@ class AssetApi {
return null;
}
/// Performs an HTTP 'GET /asset/searchTerm' operation and returns the [Response].
/// Performs an HTTP 'POST /asset/time-bucket' operation and returns the [Response].
/// Parameters:
///
/// * [GetAssetByTimeBucketDto] getAssetByTimeBucketDto (required):
Future<Response> getAssetByTimeBucketWithHttpInfo(GetAssetByTimeBucketDto getAssetByTimeBucketDto,) async {
// ignore: prefer_const_declarations
final path = r'/asset/time-bucket';
// ignore: prefer_final_locals
Object? postBody = getAssetByTimeBucketDto;
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
const contentTypes = <String>['application/json'];
return apiClient.invokeAPI(
path,
'POST',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
);
}
/// Parameters:
///
/// * [GetAssetByTimeBucketDto] getAssetByTimeBucketDto (required):
Future<List<AssetResponseDto>?> getAssetByTimeBucket(GetAssetByTimeBucketDto getAssetByTimeBucketDto,) async {
final response = await getAssetByTimeBucketWithHttpInfo(getAssetByTimeBucketDto,);
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) {
final responseBody = await _decodeBodyBytes(response);
return (await apiClient.deserializeAsync(responseBody, 'List<AssetResponseDto>') as List)
.cast<AssetResponseDto>()
.toList();
}
return null;
}
/// Performs an HTTP 'POST /asset/count-by-time-bucket' operation and returns the [Response].
/// Parameters:
///
/// * [GetAssetCountByTimeBucketDto] getAssetCountByTimeBucketDto (required):
Future<Response> getAssetCountByTimeBucketWithHttpInfo(GetAssetCountByTimeBucketDto getAssetCountByTimeBucketDto,) async {
// ignore: prefer_const_declarations
final path = r'/asset/count-by-time-bucket';
// ignore: prefer_final_locals
Object? postBody = getAssetCountByTimeBucketDto;
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
const contentTypes = <String>['application/json'];
return apiClient.invokeAPI(
path,
'POST',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
);
}
/// Parameters:
///
/// * [GetAssetCountByTimeBucketDto] getAssetCountByTimeBucketDto (required):
Future<AssetCountByTimeBucketResponseDto?> getAssetCountByTimeBucket(GetAssetCountByTimeBucketDto getAssetCountByTimeBucketDto,) async {
final response = await getAssetCountByTimeBucketWithHttpInfo(getAssetCountByTimeBucketDto,);
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), 'AssetCountByTimeBucketResponseDto',) as AssetCountByTimeBucketResponseDto;
}
return null;
}
/// Performs an HTTP 'GET /asset/count-by-user-id' operation and returns the [Response].
Future<Response> getAssetCountByUserIdWithHttpInfo() async {
// ignore: prefer_const_declarations
final path = r'/asset/count-by-user-id';
// ignore: prefer_final_locals
Object? postBody;
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
const contentTypes = <String>[];
return apiClient.invokeAPI(
path,
'GET',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
);
}
Future<AssetCountByUserIdResponseDto?> getAssetCountByUserId() async {
final response = await getAssetCountByUserIdWithHttpInfo();
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), 'AssetCountByUserIdResponseDto',) as AssetCountByUserIdResponseDto;
}
return null;
}
/// Performs an HTTP 'GET /asset/search-terms' operation and returns the [Response].
Future<Response> getAssetSearchTermsWithHttpInfo() async {
// ignore: prefer_const_declarations
final path = r'/asset/searchTerm';
final path = r'/asset/search-terms';
// ignore: prefer_final_locals
Object? postBody;
@@ -398,10 +536,10 @@ class AssetApi {
return null;
}
/// Performs an HTTP 'GET /asset/allLocation' operation and returns the [Response].
/// Performs an HTTP 'GET /asset/curated-locations' operation and returns the [Response].
Future<Response> getCuratedLocationsWithHttpInfo() async {
// ignore: prefer_const_declarations
final path = r'/asset/allLocation';
final path = r'/asset/curated-locations';
// ignore: prefer_final_locals
Object? postBody;
@@ -442,10 +580,10 @@ class AssetApi {
return null;
}
/// Performs an HTTP 'GET /asset/allObjects' operation and returns the [Response].
/// Performs an HTTP 'GET /asset/curated-objects' operation and returns the [Response].
Future<Response> getCuratedObjectsWithHttpInfo() async {
// ignore: prefer_const_declarations
final path = r'/asset/allObjects';
final path = r'/asset/curated-objects';
// ignore: prefer_final_locals
Object? postBody;

View File

@@ -198,8 +198,16 @@ class ApiClient {
return AddUsersDto.fromJson(value);
case 'AdminSignupResponseDto':
return AdminSignupResponseDto.fromJson(value);
case 'AlbumCountResponseDto':
return AlbumCountResponseDto.fromJson(value);
case 'AlbumResponseDto':
return AlbumResponseDto.fromJson(value);
case 'AssetCountByTimeBucket':
return AssetCountByTimeBucket.fromJson(value);
case 'AssetCountByTimeBucketResponseDto':
return AssetCountByTimeBucketResponseDto.fromJson(value);
case 'AssetCountByUserIdResponseDto':
return AssetCountByUserIdResponseDto.fromJson(value);
case 'AssetFileUploadResponseDto':
return AssetFileUploadResponseDto.fromJson(value);
case 'AssetResponseDto':
@@ -234,6 +242,10 @@ class ApiClient {
return DeviceTypeEnumTypeTransformer().decode(value);
case 'ExifResponseDto':
return ExifResponseDto.fromJson(value);
case 'GetAssetByTimeBucketDto':
return GetAssetByTimeBucketDto.fromJson(value);
case 'GetAssetCountByTimeBucketDto':
return GetAssetCountByTimeBucketDto.fromJson(value);
case 'LoginCredentialDto':
return LoginCredentialDto.fromJson(value);
case 'LoginResponseDto':
@@ -256,6 +268,8 @@ class ApiClient {
return SmartInfoResponseDto.fromJson(value);
case 'ThumbnailFormat':
return ThumbnailFormatTypeTransformer().decode(value);
case 'TimeGroupEnum':
return TimeGroupEnumTypeTransformer().decode(value);
case 'UpdateAlbumDto':
return UpdateAlbumDto.fromJson(value);
case 'UpdateDeviceInfoDto':

View File

@@ -67,6 +67,9 @@ String parameterToString(dynamic value) {
if (value is ThumbnailFormat) {
return ThumbnailFormatTypeTransformer().encode(value).toString();
}
if (value is TimeGroupEnum) {
return TimeGroupEnumTypeTransformer().encode(value).toString();
}
return value.toString();
}

View File

@@ -0,0 +1,127 @@
//
// 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 AlbumCountResponseDto {
/// Returns a new [AlbumCountResponseDto] instance.
AlbumCountResponseDto({
required this.owned,
required this.shared,
required this.sharing,
});
int owned;
int shared;
int sharing;
@override
bool operator ==(Object other) => identical(this, other) || other is AlbumCountResponseDto &&
other.owned == owned &&
other.shared == shared &&
other.sharing == sharing;
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(owned.hashCode) +
(shared.hashCode) +
(sharing.hashCode);
@override
String toString() => 'AlbumCountResponseDto[owned=$owned, shared=$shared, sharing=$sharing]';
Map<String, dynamic> toJson() {
final _json = <String, dynamic>{};
_json[r'owned'] = owned;
_json[r'shared'] = shared;
_json[r'sharing'] = sharing;
return _json;
}
/// Returns a new [AlbumCountResponseDto] instance and imports its values from
/// [value] if it's a [Map], null otherwise.
// ignore: prefer_constructors_over_static_methods
static AlbumCountResponseDto? 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 "AlbumCountResponseDto[$key]" is missing from JSON.');
assert(json[key] != null, 'Required key "AlbumCountResponseDto[$key]" has a null value in JSON.');
});
return true;
}());
return AlbumCountResponseDto(
owned: mapValueOfType<int>(json, r'owned')!,
shared: mapValueOfType<int>(json, r'shared')!,
sharing: mapValueOfType<int>(json, r'sharing')!,
);
}
return null;
}
static List<AlbumCountResponseDto>? listFromJson(dynamic json, {bool growable = false,}) {
final result = <AlbumCountResponseDto>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = AlbumCountResponseDto.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
static Map<String, AlbumCountResponseDto> mapFromJson(dynamic json) {
final map = <String, AlbumCountResponseDto>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) {
final value = AlbumCountResponseDto.fromJson(entry.value);
if (value != null) {
map[entry.key] = value;
}
}
}
return map;
}
// maps a json object with a list of AlbumCountResponseDto-objects as value to a dart map
static Map<String, List<AlbumCountResponseDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<AlbumCountResponseDto>>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) {
final value = AlbumCountResponseDto.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>{
'owned',
'shared',
'sharing',
};
}

View File

@@ -0,0 +1,119 @@
//
// 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 AssetCountByTimeBucket {
/// Returns a new [AssetCountByTimeBucket] instance.
AssetCountByTimeBucket({
required this.timeBucket,
required this.count,
});
String timeBucket;
int count;
@override
bool operator ==(Object other) => identical(this, other) || other is AssetCountByTimeBucket &&
other.timeBucket == timeBucket &&
other.count == count;
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(timeBucket.hashCode) +
(count.hashCode);
@override
String toString() => 'AssetCountByTimeBucket[timeBucket=$timeBucket, count=$count]';
Map<String, dynamic> toJson() {
final _json = <String, dynamic>{};
_json[r'timeBucket'] = timeBucket;
_json[r'count'] = count;
return _json;
}
/// Returns a new [AssetCountByTimeBucket] instance and imports its values from
/// [value] if it's a [Map], null otherwise.
// ignore: prefer_constructors_over_static_methods
static AssetCountByTimeBucket? 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 "AssetCountByTimeBucket[$key]" is missing from JSON.');
assert(json[key] != null, 'Required key "AssetCountByTimeBucket[$key]" has a null value in JSON.');
});
return true;
}());
return AssetCountByTimeBucket(
timeBucket: mapValueOfType<String>(json, r'timeBucket')!,
count: mapValueOfType<int>(json, r'count')!,
);
}
return null;
}
static List<AssetCountByTimeBucket>? listFromJson(dynamic json, {bool growable = false,}) {
final result = <AssetCountByTimeBucket>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = AssetCountByTimeBucket.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
static Map<String, AssetCountByTimeBucket> mapFromJson(dynamic json) {
final map = <String, AssetCountByTimeBucket>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) {
final value = AssetCountByTimeBucket.fromJson(entry.value);
if (value != null) {
map[entry.key] = value;
}
}
}
return map;
}
// maps a json object with a list of AssetCountByTimeBucket-objects as value to a dart map
static Map<String, List<AssetCountByTimeBucket>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<AssetCountByTimeBucket>>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) {
final value = AssetCountByTimeBucket.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>{
'timeBucket',
'count',
};
}

View File

@@ -0,0 +1,119 @@
//
// 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 AssetCountByTimeBucketResponseDto {
/// Returns a new [AssetCountByTimeBucketResponseDto] instance.
AssetCountByTimeBucketResponseDto({
required this.totalCount,
this.buckets = const [],
});
int totalCount;
List<AssetCountByTimeBucket> buckets;
@override
bool operator ==(Object other) => identical(this, other) || other is AssetCountByTimeBucketResponseDto &&
other.totalCount == totalCount &&
other.buckets == buckets;
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(totalCount.hashCode) +
(buckets.hashCode);
@override
String toString() => 'AssetCountByTimeBucketResponseDto[totalCount=$totalCount, buckets=$buckets]';
Map<String, dynamic> toJson() {
final _json = <String, dynamic>{};
_json[r'totalCount'] = totalCount;
_json[r'buckets'] = buckets;
return _json;
}
/// Returns a new [AssetCountByTimeBucketResponseDto] instance and imports its values from
/// [value] if it's a [Map], null otherwise.
// ignore: prefer_constructors_over_static_methods
static AssetCountByTimeBucketResponseDto? 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 "AssetCountByTimeBucketResponseDto[$key]" is missing from JSON.');
assert(json[key] != null, 'Required key "AssetCountByTimeBucketResponseDto[$key]" has a null value in JSON.');
});
return true;
}());
return AssetCountByTimeBucketResponseDto(
totalCount: mapValueOfType<int>(json, r'totalCount')!,
buckets: AssetCountByTimeBucket.listFromJson(json[r'buckets'])!,
);
}
return null;
}
static List<AssetCountByTimeBucketResponseDto>? listFromJson(dynamic json, {bool growable = false,}) {
final result = <AssetCountByTimeBucketResponseDto>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = AssetCountByTimeBucketResponseDto.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
static Map<String, AssetCountByTimeBucketResponseDto> mapFromJson(dynamic json) {
final map = <String, AssetCountByTimeBucketResponseDto>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) {
final value = AssetCountByTimeBucketResponseDto.fromJson(entry.value);
if (value != null) {
map[entry.key] = value;
}
}
}
return map;
}
// maps a json object with a list of AssetCountByTimeBucketResponseDto-objects as value to a dart map
static Map<String, List<AssetCountByTimeBucketResponseDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<AssetCountByTimeBucketResponseDto>>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) {
final value = AssetCountByTimeBucketResponseDto.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>{
'totalCount',
'buckets',
};
}

View File

@@ -0,0 +1,119 @@
//
// 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 AssetCountByTimeGroupDto {
/// Returns a new [AssetCountByTimeGroupDto] instance.
AssetCountByTimeGroupDto({
required this.timeGroup,
required this.count,
});
String timeGroup;
int count;
@override
bool operator ==(Object other) => identical(this, other) || other is AssetCountByTimeGroupDto &&
other.timeGroup == timeGroup &&
other.count == count;
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(timeGroup.hashCode) +
(count.hashCode);
@override
String toString() => 'AssetCountByTimeGroupDto[timeGroup=$timeGroup, count=$count]';
Map<String, dynamic> toJson() {
final _json = <String, dynamic>{};
_json[r'timeGroup'] = timeGroup;
_json[r'count'] = count;
return _json;
}
/// Returns a new [AssetCountByTimeGroupDto] instance and imports its values from
/// [value] if it's a [Map], null otherwise.
// ignore: prefer_constructors_over_static_methods
static AssetCountByTimeGroupDto? 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 "AssetCountByTimeGroupDto[$key]" is missing from JSON.');
assert(json[key] != null, 'Required key "AssetCountByTimeGroupDto[$key]" has a null value in JSON.');
});
return true;
}());
return AssetCountByTimeGroupDto(
timeGroup: mapValueOfType<String>(json, r'timeGroup')!,
count: mapValueOfType<int>(json, r'count')!,
);
}
return null;
}
static List<AssetCountByTimeGroupDto>? listFromJson(dynamic json, {bool growable = false,}) {
final result = <AssetCountByTimeGroupDto>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = AssetCountByTimeGroupDto.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
static Map<String, AssetCountByTimeGroupDto> mapFromJson(dynamic json) {
final map = <String, AssetCountByTimeGroupDto>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) {
final value = AssetCountByTimeGroupDto.fromJson(entry.value);
if (value != null) {
map[entry.key] = value;
}
}
}
return map;
}
// maps a json object with a list of AssetCountByTimeGroupDto-objects as value to a dart map
static Map<String, List<AssetCountByTimeGroupDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<AssetCountByTimeGroupDto>>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) {
final value = AssetCountByTimeGroupDto.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>{
'timeGroup',
'count',
};
}

View File

@@ -0,0 +1,119 @@
//
// 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 AssetCountByTimeGroupResponseDto {
/// Returns a new [AssetCountByTimeGroupResponseDto] instance.
AssetCountByTimeGroupResponseDto({
required this.count,
this.buckets = const [],
});
int count;
List<AssetCountByTimeBucketResponseDto> buckets;
@override
bool operator ==(Object other) => identical(this, other) || other is AssetCountByTimeGroupResponseDto &&
other.count == count &&
other.buckets == buckets;
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(count.hashCode) +
(buckets.hashCode);
@override
String toString() => 'AssetCountByTimeGroupResponseDto[count=$count, buckets=$buckets]';
Map<String, dynamic> toJson() {
final _json = <String, dynamic>{};
_json[r'count'] = count;
_json[r'buckets'] = buckets;
return _json;
}
/// Returns a new [AssetCountByTimeGroupResponseDto] instance and imports its values from
/// [value] if it's a [Map], null otherwise.
// ignore: prefer_constructors_over_static_methods
static AssetCountByTimeGroupResponseDto? 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 "AssetCountByTimeGroupResponseDto[$key]" is missing from JSON.');
assert(json[key] != null, 'Required key "AssetCountByTimeGroupResponseDto[$key]" has a null value in JSON.');
});
return true;
}());
return AssetCountByTimeGroupResponseDto(
count: mapValueOfType<int>(json, r'count')!,
buckets: AssetCountByTimeBucketResponseDto.listFromJson(json[r'buckets'])!,
);
}
return null;
}
static List<AssetCountByTimeGroupResponseDto>? listFromJson(dynamic json, {bool growable = false,}) {
final result = <AssetCountByTimeGroupResponseDto>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = AssetCountByTimeGroupResponseDto.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
static Map<String, AssetCountByTimeGroupResponseDto> mapFromJson(dynamic json) {
final map = <String, AssetCountByTimeGroupResponseDto>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) {
final value = AssetCountByTimeGroupResponseDto.fromJson(entry.value);
if (value != null) {
map[entry.key] = value;
}
}
}
return map;
}
// maps a json object with a list of AssetCountByTimeGroupResponseDto-objects as value to a dart map
static Map<String, List<AssetCountByTimeGroupResponseDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<AssetCountByTimeGroupResponseDto>>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) {
final value = AssetCountByTimeGroupResponseDto.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>{
'count',
'buckets',
};
}

View File

@@ -0,0 +1,119 @@
//
// 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 AssetCountByUserIdResponseDto {
/// Returns a new [AssetCountByUserIdResponseDto] instance.
AssetCountByUserIdResponseDto({
required this.photos,
required this.videos,
});
int photos;
int videos;
@override
bool operator ==(Object other) => identical(this, other) || other is AssetCountByUserIdResponseDto &&
other.photos == photos &&
other.videos == videos;
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(photos.hashCode) +
(videos.hashCode);
@override
String toString() => 'AssetCountByUserIdResponseDto[photos=$photos, videos=$videos]';
Map<String, dynamic> toJson() {
final _json = <String, dynamic>{};
_json[r'photos'] = photos;
_json[r'videos'] = videos;
return _json;
}
/// Returns a new [AssetCountByUserIdResponseDto] instance and imports its values from
/// [value] if it's a [Map], null otherwise.
// ignore: prefer_constructors_over_static_methods
static AssetCountByUserIdResponseDto? 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 "AssetCountByUserIdResponseDto[$key]" is missing from JSON.');
assert(json[key] != null, 'Required key "AssetCountByUserIdResponseDto[$key]" has a null value in JSON.');
});
return true;
}());
return AssetCountByUserIdResponseDto(
photos: mapValueOfType<int>(json, r'photos')!,
videos: mapValueOfType<int>(json, r'videos')!,
);
}
return null;
}
static List<AssetCountByUserIdResponseDto>? listFromJson(dynamic json, {bool growable = false,}) {
final result = <AssetCountByUserIdResponseDto>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = AssetCountByUserIdResponseDto.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
static Map<String, AssetCountByUserIdResponseDto> mapFromJson(dynamic json) {
final map = <String, AssetCountByUserIdResponseDto>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) {
final value = AssetCountByUserIdResponseDto.fromJson(entry.value);
if (value != null) {
map[entry.key] = value;
}
}
}
return map;
}
// maps a json object with a list of AssetCountByUserIdResponseDto-objects as value to a dart map
static Map<String, List<AssetCountByUserIdResponseDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<AssetCountByUserIdResponseDto>>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) {
final value = AssetCountByUserIdResponseDto.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>{
'photos',
'videos',
};
}

View File

@@ -0,0 +1,113 @@
//
// 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 GetAssetByTimeBucketDto {
/// Returns a new [GetAssetByTimeBucketDto] instance.
GetAssetByTimeBucketDto({
this.timeBucket = const [],
});
List<String> timeBucket;
@override
bool operator ==(Object other) => identical(this, other) || other is GetAssetByTimeBucketDto &&
other.timeBucket == timeBucket;
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(timeBucket.hashCode);
@override
String toString() => 'GetAssetByTimeBucketDto[timeBucket=$timeBucket]';
Map<String, dynamic> toJson() {
final _json = <String, dynamic>{};
_json[r'timeBucket'] = timeBucket;
return _json;
}
/// Returns a new [GetAssetByTimeBucketDto] instance and imports its values from
/// [value] if it's a [Map], null otherwise.
// ignore: prefer_constructors_over_static_methods
static GetAssetByTimeBucketDto? 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 "GetAssetByTimeBucketDto[$key]" is missing from JSON.');
assert(json[key] != null, 'Required key "GetAssetByTimeBucketDto[$key]" has a null value in JSON.');
});
return true;
}());
return GetAssetByTimeBucketDto(
timeBucket: json[r'timeBucket'] is List
? (json[r'timeBucket'] as List).cast<String>()
: const [],
);
}
return null;
}
static List<GetAssetByTimeBucketDto>? listFromJson(dynamic json, {bool growable = false,}) {
final result = <GetAssetByTimeBucketDto>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = GetAssetByTimeBucketDto.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
static Map<String, GetAssetByTimeBucketDto> mapFromJson(dynamic json) {
final map = <String, GetAssetByTimeBucketDto>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) {
final value = GetAssetByTimeBucketDto.fromJson(entry.value);
if (value != null) {
map[entry.key] = value;
}
}
}
return map;
}
// maps a json object with a list of GetAssetByTimeBucketDto-objects as value to a dart map
static Map<String, List<GetAssetByTimeBucketDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<GetAssetByTimeBucketDto>>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) {
final value = GetAssetByTimeBucketDto.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>{
'timeBucket',
};
}

View File

@@ -0,0 +1,111 @@
//
// 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 GetAssetCountByTimeBucketDto {
/// Returns a new [GetAssetCountByTimeBucketDto] instance.
GetAssetCountByTimeBucketDto({
required this.timeGroup,
});
TimeGroupEnum timeGroup;
@override
bool operator ==(Object other) => identical(this, other) || other is GetAssetCountByTimeBucketDto &&
other.timeGroup == timeGroup;
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(timeGroup.hashCode);
@override
String toString() => 'GetAssetCountByTimeBucketDto[timeGroup=$timeGroup]';
Map<String, dynamic> toJson() {
final _json = <String, dynamic>{};
_json[r'timeGroup'] = timeGroup;
return _json;
}
/// Returns a new [GetAssetCountByTimeBucketDto] instance and imports its values from
/// [value] if it's a [Map], null otherwise.
// ignore: prefer_constructors_over_static_methods
static GetAssetCountByTimeBucketDto? 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 "GetAssetCountByTimeBucketDto[$key]" is missing from JSON.');
assert(json[key] != null, 'Required key "GetAssetCountByTimeBucketDto[$key]" has a null value in JSON.');
});
return true;
}());
return GetAssetCountByTimeBucketDto(
timeGroup: TimeGroupEnum.fromJson(json[r'timeGroup'])!,
);
}
return null;
}
static List<GetAssetCountByTimeBucketDto>? listFromJson(dynamic json, {bool growable = false,}) {
final result = <GetAssetCountByTimeBucketDto>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = GetAssetCountByTimeBucketDto.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
static Map<String, GetAssetCountByTimeBucketDto> mapFromJson(dynamic json) {
final map = <String, GetAssetCountByTimeBucketDto>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) {
final value = GetAssetCountByTimeBucketDto.fromJson(entry.value);
if (value != null) {
map[entry.key] = value;
}
}
}
return map;
}
// maps a json object with a list of GetAssetCountByTimeBucketDto-objects as value to a dart map
static Map<String, List<GetAssetCountByTimeBucketDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<GetAssetCountByTimeBucketDto>>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) {
final value = GetAssetCountByTimeBucketDto.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>{
'timeGroup',
};
}

View File

@@ -0,0 +1,111 @@
//
// 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 GetAssetCountByTimeGroupDto {
/// Returns a new [GetAssetCountByTimeGroupDto] instance.
GetAssetCountByTimeGroupDto({
required this.timeGroup,
});
TimeGroupEnum timeGroup;
@override
bool operator ==(Object other) => identical(this, other) || other is GetAssetCountByTimeGroupDto &&
other.timeGroup == timeGroup;
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(timeGroup.hashCode);
@override
String toString() => 'GetAssetCountByTimeGroupDto[timeGroup=$timeGroup]';
Map<String, dynamic> toJson() {
final _json = <String, dynamic>{};
_json[r'timeGroup'] = timeGroup;
return _json;
}
/// Returns a new [GetAssetCountByTimeGroupDto] instance and imports its values from
/// [value] if it's a [Map], null otherwise.
// ignore: prefer_constructors_over_static_methods
static GetAssetCountByTimeGroupDto? 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 "GetAssetCountByTimeGroupDto[$key]" is missing from JSON.');
assert(json[key] != null, 'Required key "GetAssetCountByTimeGroupDto[$key]" has a null value in JSON.');
});
return true;
}());
return GetAssetCountByTimeGroupDto(
timeGroup: TimeGroupEnum.fromJson(json[r'timeGroup'])!,
);
}
return null;
}
static List<GetAssetCountByTimeGroupDto>? listFromJson(dynamic json, {bool growable = false,}) {
final result = <GetAssetCountByTimeGroupDto>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = GetAssetCountByTimeGroupDto.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
static Map<String, GetAssetCountByTimeGroupDto> mapFromJson(dynamic json) {
final map = <String, GetAssetCountByTimeGroupDto>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) {
final value = GetAssetCountByTimeGroupDto.fromJson(entry.value);
if (value != null) {
map[entry.key] = value;
}
}
}
return map;
}
// maps a json object with a list of GetAssetCountByTimeGroupDto-objects as value to a dart map
static Map<String, List<GetAssetCountByTimeGroupDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<GetAssetCountByTimeGroupDto>>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) {
final value = GetAssetCountByTimeGroupDto.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>{
'timeGroup',
};
}

View File

@@ -0,0 +1,85 @@
//
// 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 TimeBucketEnum {
/// Instantiate a new enum with the provided [value].
const TimeBucketEnum._(this.value);
/// The underlying value of this enum member.
final String value;
@override
String toString() => value;
String toJson() => value;
static const day = TimeBucketEnum._(r'day');
static const month = TimeBucketEnum._(r'month');
/// List of all possible values in this [enum][TimeBucketEnum].
static const values = <TimeBucketEnum>[
day,
month,
];
static TimeBucketEnum? fromJson(dynamic value) => TimeBucketEnumTypeTransformer().decode(value);
static List<TimeBucketEnum>? listFromJson(dynamic json, {bool growable = false,}) {
final result = <TimeBucketEnum>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = TimeBucketEnum.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
}
/// Transformation class that can [encode] an instance of [TimeBucketEnum] to String,
/// and [decode] dynamic data back to [TimeBucketEnum].
class TimeBucketEnumTypeTransformer {
factory TimeBucketEnumTypeTransformer() => _instance ??= const TimeBucketEnumTypeTransformer._();
const TimeBucketEnumTypeTransformer._();
String encode(TimeBucketEnum data) => data.value;
/// Decodes a [dynamic value][data] to a TimeBucketEnum.
///
/// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully,
/// then null is returned. However, if [allowNull] is false and the [dynamic value][data]
/// cannot be decoded successfully, then an [UnimplementedError] is thrown.
///
/// The [allowNull] is very handy when an API changes and a new enum value is added or removed,
/// and users are still using an old app with the old code.
TimeBucketEnum? decode(dynamic data, {bool allowNull = true}) {
if (data != null) {
switch (data.toString()) {
case r'day': return TimeBucketEnum.day;
case r'month': return TimeBucketEnum.month;
default:
if (!allowNull) {
throw ArgumentError('Unknown enum value to decode: $data');
}
}
}
return null;
}
/// Singleton [TimeBucketEnumTypeTransformer] instance.
static TimeBucketEnumTypeTransformer? _instance;
}

View File

@@ -0,0 +1,85 @@
//
// 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 TimeGroupEnum {
/// Instantiate a new enum with the provided [value].
const TimeGroupEnum._(this.value);
/// The underlying value of this enum member.
final String value;
@override
String toString() => value;
String toJson() => value;
static const day = TimeGroupEnum._(r'day');
static const month = TimeGroupEnum._(r'month');
/// List of all possible values in this [enum][TimeGroupEnum].
static const values = <TimeGroupEnum>[
day,
month,
];
static TimeGroupEnum? fromJson(dynamic value) => TimeGroupEnumTypeTransformer().decode(value);
static List<TimeGroupEnum>? listFromJson(dynamic json, {bool growable = false,}) {
final result = <TimeGroupEnum>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = TimeGroupEnum.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
}
/// Transformation class that can [encode] an instance of [TimeGroupEnum] to String,
/// and [decode] dynamic data back to [TimeGroupEnum].
class TimeGroupEnumTypeTransformer {
factory TimeGroupEnumTypeTransformer() => _instance ??= const TimeGroupEnumTypeTransformer._();
const TimeGroupEnumTypeTransformer._();
String encode(TimeGroupEnum data) => data.value;
/// Decodes a [dynamic value][data] to a TimeGroupEnum.
///
/// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully,
/// then null is returned. However, if [allowNull] is false and the [dynamic value][data]
/// cannot be decoded successfully, then an [UnimplementedError] is thrown.
///
/// The [allowNull] is very handy when an API changes and a new enum value is added or removed,
/// and users are still using an old app with the old code.
TimeGroupEnum? decode(dynamic data, {bool allowNull = true}) {
if (data != null) {
switch (data.toString()) {
case r'day': return TimeGroupEnum.day;
case r'month': return TimeGroupEnum.month;
default:
if (!allowNull) {
throw ArgumentError('Unknown enum value to decode: $data');
}
}
}
return null;
}
/// Singleton [TimeGroupEnumTypeTransformer] instance.
static TimeGroupEnumTypeTransformer? _instance;
}

View File

@@ -0,0 +1,37 @@
//
// 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 AlbumCountResponseDto
void main() {
// final instance = AlbumCountResponseDto();
group('test AlbumCountResponseDto', () {
// int owned
test('to test the property `owned`', () async {
// TODO
});
// int shared
test('to test the property `shared`', () async {
// TODO
});
// int sharing
test('to test the property `sharing`', () async {
// TODO
});
});
}

View File

@@ -0,0 +1,32 @@
//
// 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 AssetCountByTimeBucketResponseDto
void main() {
// final instance = AssetCountByTimeBucketResponseDto();
group('test AssetCountByTimeBucketResponseDto', () {
// String timeGroup
test('to test the property `timeGroup`', () async {
// TODO
});
// int count
test('to test the property `count`', () async {
// TODO
});
});
}

View File

@@ -0,0 +1,32 @@
//
// 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 AssetCountByTimeBucket
void main() {
// final instance = AssetCountByTimeBucket();
group('test AssetCountByTimeBucket', () {
// String timeBucket
test('to test the property `timeBucket`', () async {
// TODO
});
// int count
test('to test the property `count`', () async {
// TODO
});
});
}

View File

@@ -0,0 +1,32 @@
//
// 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 AssetCountByTimeGroupDto
void main() {
// final instance = AssetCountByTimeGroupDto();
group('test AssetCountByTimeGroupDto', () {
// String timeGroup
test('to test the property `timeGroup`', () async {
// TODO
});
// int count
test('to test the property `count`', () async {
// TODO
});
});
}

View File

@@ -0,0 +1,32 @@
//
// 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 AssetCountByTimeGroupResponseDto
void main() {
// final instance = AssetCountByTimeGroupResponseDto();
group('test AssetCountByTimeGroupResponseDto', () {
// int totalAssets
test('to test the property `totalAssets`', () async {
// TODO
});
// List<AssetCountByTimeGroupDto> groups (default value: const [])
test('to test the property `groups`', () async {
// TODO
});
});
}

View File

@@ -0,0 +1,32 @@
//
// 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 AssetCountByUserIdResponseDto
void main() {
// final instance = AssetCountByUserIdResponseDto();
group('test AssetCountByUserIdResponseDto', () {
// int photos
test('to test the property `photos`', () async {
// TODO
});
// int videos
test('to test the property `videos`', () async {
// TODO
});
});
}

View File

@@ -0,0 +1,27 @@
//
// 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 GetAssetByTimeBucketDto
void main() {
// final instance = GetAssetByTimeBucketDto();
group('test GetAssetByTimeBucketDto', () {
// List<String> timeBucket (default value: const [])
test('to test the property `timeBucket`', () async {
// TODO
});
});
}

View File

@@ -0,0 +1,27 @@
//
// 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 GetAssetCountByTimeBucketDto
void main() {
// final instance = GetAssetCountByTimeBucketDto();
group('test GetAssetCountByTimeBucketDto', () {
// TimeGroupEnum timeGroup
test('to test the property `timeGroup`', () async {
// TODO
});
});
}

View File

@@ -0,0 +1,27 @@
//
// 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 GetAssetCountByTimeGroupDto
void main() {
// final instance = GetAssetCountByTimeGroupDto();
group('test GetAssetCountByTimeGroupDto', () {
// String timeGroup (default value: 'month')
test('to test the property `timeGroup`', () async {
// TODO
});
});
}

View File

@@ -0,0 +1,21 @@
//
// 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 TimeBucketEnum
void main() {
group('test TimeBucketEnum', () {
});
}

View File

@@ -0,0 +1,21 @@
//
// 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 TimeGroupEnum
void main() {
group('test TimeGroupEnum', () {
});
}

View File

@@ -35,7 +35,7 @@ packages:
name: async
url: "https://pub.dartlang.org"
source: hosted
version: "2.8.2"
version: "2.9.0"
auto_route:
dependency: "direct main"
description:
@@ -161,7 +161,7 @@ packages:
name: characters
url: "https://pub.dartlang.org"
source: hosted
version: "1.2.0"
version: "1.2.1"
charcode:
dependency: transitive
description:
@@ -189,7 +189,7 @@ packages:
name: clock
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.0"
version: "1.1.1"
code_builder:
dependency: transitive
description:
@@ -287,7 +287,7 @@ packages:
name: fake_async
url: "https://pub.dartlang.org"
source: hosted
version: "1.3.0"
version: "1.3.1"
ffi:
dependency: transitive
description:
@@ -601,21 +601,21 @@ packages:
name: matcher
url: "https://pub.dartlang.org"
source: hosted
version: "0.12.11"
version: "0.12.12"
material_color_utilities:
dependency: transitive
description:
name: material_color_utilities
url: "https://pub.dartlang.org"
source: hosted
version: "0.1.4"
version: "0.1.5"
meta:
dependency: transitive
description:
name: meta
url: "https://pub.dartlang.org"
source: hosted
version: "1.7.0"
version: "1.8.0"
mgrs_dart:
dependency: transitive
description:
@@ -706,7 +706,7 @@ packages:
name: path
url: "https://pub.dartlang.org"
source: hosted
version: "1.8.1"
version: "1.8.2"
path_provider:
dependency: "direct main"
description:
@@ -1026,7 +1026,7 @@ packages:
name: source_span
url: "https://pub.dartlang.org"
source: hosted
version: "1.8.2"
version: "1.9.0"
sprintf:
dependency: transitive
description:
@@ -1082,7 +1082,7 @@ packages:
name: string_scanner
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.0"
version: "1.1.1"
synchronized:
dependency: transitive
description:
@@ -1096,14 +1096,14 @@ packages:
name: term_glyph
url: "https://pub.dartlang.org"
source: hosted
version: "1.2.0"
version: "1.2.1"
test_api:
dependency: transitive
description:
name: test_api
url: "https://pub.dartlang.org"
source: hosted
version: "0.4.9"
version: "0.4.12"
timing:
dependency: transitive
description:

View File

@@ -2,7 +2,7 @@ name: immich_mobile
description: Immich - selfhosted backup media file on mobile phone
publish_to: "none"
version: 1.26.0+35
version: 1.28.0+38
environment:
sdk: ">=2.17.0 <3.0.0"

View File

@@ -10,6 +10,7 @@ import { CreateAlbumDto } from './dto/create-album.dto';
import { GetAlbumsDto } from './dto/get-albums.dto';
import { RemoveAssetsDto } from './dto/remove-assets.dto';
import { UpdateAlbumDto } from './dto/update-album.dto';
import { AlbumCountResponseDto } from './response-dto/album-count-response.dto';
import { AlbumResponseDto } from './response-dto/album-response.dto';
export interface IAlbumRepository {
@@ -22,6 +23,8 @@ export interface IAlbumRepository {
removeAssets(album: AlbumEntity, removeAssets: RemoveAssetsDto): Promise<AlbumEntity>;
addAssets(album: AlbumEntity, addAssetsDto: AddAssetsDto): Promise<AlbumEntity>;
updateAlbum(album: AlbumEntity, updateAlbumDto: UpdateAlbumDto): Promise<AlbumEntity>;
getListByAssetId(userId: string, assetId: string): Promise<AlbumEntity[]>;
getCountByUserId(userId: string): Promise<AlbumCountResponseDto>;
}
export const ALBUM_REPOSITORY = 'ALBUM_REPOSITORY';
@@ -41,6 +44,18 @@ export class AlbumRepository implements IAlbumRepository {
private dataSource: DataSource,
) {}
async getCountByUserId(userId: string): Promise<AlbumCountResponseDto> {
const ownedAlbums = await this.albumRepository.find({ where: { ownerId: userId }, relations: ['sharedUsers'] });
const sharedAlbums = await this.userAlbumRepository.count({
where: { sharedUserId: userId },
});
const sharingAlbums = ownedAlbums.map((album) => album.sharedUsers?.length || 0).reduce((a, b) => a + b, 0);
return new AlbumCountResponseDto(ownedAlbums.length, sharedAlbums, sharingAlbums);
}
async create(ownerId: string, createAlbumDto: CreateAlbumDto): Promise<AlbumEntity> {
return this.dataSource.transaction(async (transactionalEntityManager) => {
// Create album entity
@@ -149,6 +164,31 @@ export class AlbumRepository implements IAlbumRepository {
return albums;
}
async getListByAssetId(userId: string, assetId: string): Promise<AlbumEntity[]> {
let query = this.albumRepository.createQueryBuilder('album');
const albums = await query
.where('album.ownerId = :ownerId', { ownerId: userId })
.andWhere((qb) => {
// shared with userId
const subQuery = qb
.subQuery()
.select('assetAlbum.albumId')
.from(AssetAlbumEntity, 'assetAlbum')
.where('assetAlbum.assetId = :assetId', { assetId: assetId })
.getQuery();
return `album.id IN ${subQuery}`;
})
.leftJoinAndSelect('album.assets', 'assets')
.leftJoinAndSelect('assets.assetInfo', 'assetInfo')
.leftJoinAndSelect('album.sharedUsers', 'sharedUser')
.leftJoinAndSelect('sharedUser.userInfo', 'userInfo')
.orderBy('"assetInfo"."createdAt"::timestamptz', 'ASC')
.getMany();
return albums;
}
async get(albumId: string): Promise<AlbumEntity | undefined> {
let query = this.albumRepository.createQueryBuilder('album');
@@ -202,9 +242,10 @@ export class AlbumRepository implements IAlbumRepository {
// TODO: No need to return boolean if using a singe delete query
if (deleteAssetCount == removeAssetsDto.assetIds.length) {
const retAlbum = await this.get(album.id) as AlbumEntity;
const retAlbum = (await this.get(album.id)) as AlbumEntity;
if (retAlbum?.assets?.length === 0) { // is empty album
if (retAlbum?.assets?.length === 0) {
// is empty album
await this.albumRepository.update(album.id, { albumThumbnailAssetId: null });
retAlbum.albumThumbnailAssetId = null;
}

View File

@@ -11,6 +11,7 @@ import {
ParseUUIDPipe,
Put,
Query,
Header,
} from '@nestjs/common';
import { ParseMeUUIDPipe } from '../validation/parse-me-uuid-pipe';
import { AlbumService } from './album.service';
@@ -24,6 +25,7 @@ import { UpdateAlbumDto } from './dto/update-album.dto';
import { GetAlbumsDto } from './dto/get-albums.dto';
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
import { AlbumResponseDto } from './response-dto/album-response.dto';
import { AlbumCountResponseDto } from './response-dto/album-count-response.dto';
// TODO might be worth creating a AlbumParamsDto that validates `albumId` instead of using the pipe.
@UseGuards(JwtAuthGuard)
@@ -33,6 +35,11 @@ import { AlbumResponseDto } from './response-dto/album-response.dto';
export class AlbumController {
constructor(private readonly albumService: AlbumService) {}
@Get('count-by-user-id')
async getAlbumCountByUserId(@GetAuthUser() authUser: AuthUserDto): Promise<AlbumCountResponseDto> {
return this.albumService.getAlbumCountByUserId(authUser);
}
@Post()
async createAlbum(@GetAuthUser() authUser: AuthUserDto, @Body(ValidationPipe) createAlbumDto: CreateAlbumDto) {
return this.albumService.create(authUser, createAlbumDto);

View File

@@ -116,6 +116,8 @@ describe('Album service', () => {
removeAssets: jest.fn(),
removeUser: jest.fn(),
updateAlbum: jest.fn(),
getListByAssetId: jest.fn(),
getCountByUserId: jest.fn(),
};
sut = new AlbumService(albumRepositoryMock);
});

View File

@@ -9,6 +9,7 @@ import { UpdateAlbumDto } from './dto/update-album.dto';
import { GetAlbumsDto } from './dto/get-albums.dto';
import { AlbumResponseDto, mapAlbum, mapAlbumExcludeAssetInfo } from './response-dto/album-response.dto';
import { ALBUM_REPOSITORY, IAlbumRepository } from './album-repository';
import { AlbumCountResponseDto } from './response-dto/album-count-response.dto';
@Injectable()
export class AlbumService {
@@ -48,8 +49,11 @@ export class AlbumService {
* @returns All Shared Album And Its Members
*/
async getAllAlbums(authUser: AuthUserDto, getAlbumsDto: GetAlbumsDto): Promise<AlbumResponseDto[]> {
if (typeof getAlbumsDto.assetId === 'string') {
const albums = await this._albumRepository.getListByAssetId(authUser.id, getAlbumsDto.assetId);
return albums.map(mapAlbumExcludeAssetInfo);
}
const albums = await this._albumRepository.getList(authUser.id, getAlbumsDto);
return albums.map((album) => mapAlbumExcludeAssetInfo(album));
}
@@ -115,4 +119,8 @@ export class AlbumService {
const updatedAlbum = await this._albumRepository.updateAlbum(album, updateAlbumDto);
return mapAlbum(updatedAlbum);
}
async getAlbumCountByUserId(authUser: AuthUserDto): Promise<AlbumCountResponseDto> {
return this._albumRepository.getCountByUserId(authUser.id);
}
}

View File

@@ -18,4 +18,11 @@ export class GetAlbumsDto {
* undefined: shared and owned albums
*/
shared?: boolean;
/**
* Only returns albums that contain the asset
* Ignores the shared parameter
* undefined: get all albums
*/
assetId?: string;
}

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