Compare commits
9 Commits
v1.27.0_37
...
v1.28.0_38
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
853a65aef1 | ||
|
|
566039b93f | ||
|
|
18a7ff8726 | ||
|
|
6ffdf167fe | ||
|
|
92c4f0598b | ||
|
|
a337402124 | ||
|
|
209e6332b3 | ||
|
|
645bd8a109 | ||
|
|
9a471d80f7 |
4
.github/workflows/test.yml
vendored
4
.github/workflows/test.yml
vendored
@@ -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
|
||||
|
||||
@@ -97,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.*
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -30,8 +30,8 @@ platform :android do
|
||||
task: 'bundle',
|
||||
build_type: 'Release',
|
||||
properties: {
|
||||
"android.injected.version.code" => 37,
|
||||
"android.injected.version.name" => "1.27.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')
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
* Fixed remove empty translations
|
||||
* Fixed search page crashes the app on some Android models
|
||||
@@ -12,8 +12,6 @@
|
||||
"album_viewer_appbar_share_leave": "Album verlassen",
|
||||
"album_viewer_appbar_share_remove": "Entferne vom Album",
|
||||
"album_viewer_page_share_add_users": "Nutzer hinzufügen",
|
||||
"asset_list_settings_subtitle": "",
|
||||
"asset_list_settings_title": "",
|
||||
"backup_album_selection_page_albums_device": "Alben auf dem Gerät ({})",
|
||||
"backup_album_selection_page_albums_tap": "Tippen um einzuschließen, doppelt tippen um zu entfernen",
|
||||
"backup_album_selection_page_assets_scatter": "Elemente können sich über mehrere Alben verteilen. Daher können diese vor der Sicherung eingeschlossen oder ausgeschlossen werden",
|
||||
@@ -21,26 +19,7 @@
|
||||
"backup_album_selection_page_selection_info": "Auswahl",
|
||||
"backup_album_selection_page_total_assets": "Elemente",
|
||||
"backup_all": "Alle",
|
||||
"backup_background_service_backup_failed_message": "",
|
||||
"backup_background_service_connection_failed_message": "",
|
||||
"backup_background_service_current_upload_notification": "",
|
||||
"backup_background_service_default_notification": "",
|
||||
"backup_background_service_error_title": "",
|
||||
"backup_background_service_in_progress_notification": "",
|
||||
"backup_background_service_upload_failure_notification": "",
|
||||
"backup_controller_page_albums": "Gesicherte Alben",
|
||||
"backup_controller_page_background_battery_info_link": "",
|
||||
"backup_controller_page_background_battery_info_message": "",
|
||||
"backup_controller_page_background_battery_info_ok": "",
|
||||
"backup_controller_page_background_battery_info_title": "",
|
||||
"backup_controller_page_background_charging": "",
|
||||
"backup_controller_page_background_configure_error": "",
|
||||
"backup_controller_page_background_description": "",
|
||||
"backup_controller_page_background_is_off": "",
|
||||
"backup_controller_page_background_is_on": "",
|
||||
"backup_controller_page_background_turn_off": "",
|
||||
"backup_controller_page_background_turn_on": "",
|
||||
"backup_controller_page_background_wifi": "",
|
||||
"backup_controller_page_backup": "Sicherung",
|
||||
"backup_controller_page_backup_selected": "Ausgewählt: ",
|
||||
"backup_controller_page_backup_sub": "Gesicherte Fotos und Videos",
|
||||
@@ -69,19 +48,6 @@
|
||||
"backup_controller_page_uploading_file_info": "Informationen",
|
||||
"backup_err_only_album": "Das einzige Album kann nicht entfernt werden",
|
||||
"backup_info_card_assets": "Elemente",
|
||||
"cache_settings_album_thumbnails": "",
|
||||
"cache_settings_clear_cache_button": "",
|
||||
"cache_settings_clear_cache_button_title": "",
|
||||
"cache_settings_image_cache_size": "",
|
||||
"cache_settings_statistics_album": "",
|
||||
"cache_settings_statistics_assets": "",
|
||||
"cache_settings_statistics_full": "",
|
||||
"cache_settings_statistics_shared": "",
|
||||
"cache_settings_statistics_thumbnail": "",
|
||||
"cache_settings_statistics_title": "",
|
||||
"cache_settings_subtitle": "",
|
||||
"cache_settings_thumbnail_size": "",
|
||||
"cache_settings_title": "",
|
||||
"control_bottom_app_bar_delete": "Löschen",
|
||||
"control_bottom_app_bar_share": "Teilen",
|
||||
"create_album_page_untitled": "Unbenannt",
|
||||
@@ -127,13 +93,6 @@
|
||||
"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": "Suggestions",
|
||||
"setting_notifications_notify_failures_grace_period": "",
|
||||
"setting_notifications_notify_hours": "",
|
||||
"setting_notifications_notify_immediately": "",
|
||||
"setting_notifications_notify_minutes": "",
|
||||
"setting_notifications_notify_never": "",
|
||||
"setting_notifications_subtitle": "",
|
||||
"setting_notifications_title": "",
|
||||
"setting_pages_app_bar_settings": "Einstellungen",
|
||||
"share_add": "Hinzufügen",
|
||||
"share_add_photos": "Fotos hinzufügen",
|
||||
@@ -150,8 +109,6 @@
|
||||
"tab_controller_nav_photos": "Fotos",
|
||||
"tab_controller_nav_search": "Suche",
|
||||
"tab_controller_nav_sharing": "Teilen",
|
||||
"theme_setting_asset_list_storage_indicator_title": "",
|
||||
"theme_setting_asset_list_tiles_per_row_title": "",
|
||||
"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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -12,8 +12,6 @@
|
||||
"album_viewer_appbar_share_leave": "Quitter l'album",
|
||||
"album_viewer_appbar_share_remove": "Retirer de l'album",
|
||||
"album_viewer_page_share_add_users": "Ajouter des utilisateurs",
|
||||
"asset_list_settings_subtitle": "",
|
||||
"asset_list_settings_title": "",
|
||||
"backup_album_selection_page_albums_device": "Albums sur l'appareil ({})",
|
||||
"backup_album_selection_page_albums_tap": "Tapez pour inclure, tapez deux fois pour exclure",
|
||||
"backup_album_selection_page_assets_scatter": "Les éléments peuvent être répartis sur plusieurs albums. De ce fait, les albums peuvent être inclus ou exclus pendant le processus de sauvegarde.",
|
||||
@@ -21,26 +19,7 @@
|
||||
"backup_album_selection_page_selection_info": "Informations sur la sélection",
|
||||
"backup_album_selection_page_total_assets": "Total des éléments uniques",
|
||||
"backup_all": "Tout",
|
||||
"backup_background_service_backup_failed_message": "",
|
||||
"backup_background_service_connection_failed_message": "",
|
||||
"backup_background_service_current_upload_notification": "",
|
||||
"backup_background_service_default_notification": "",
|
||||
"backup_background_service_error_title": "",
|
||||
"backup_background_service_in_progress_notification": "",
|
||||
"backup_background_service_upload_failure_notification": "",
|
||||
"backup_controller_page_albums": "Sauvegarder les albums",
|
||||
"backup_controller_page_background_battery_info_link": "",
|
||||
"backup_controller_page_background_battery_info_message": "",
|
||||
"backup_controller_page_background_battery_info_ok": "",
|
||||
"backup_controller_page_background_battery_info_title": "",
|
||||
"backup_controller_page_background_charging": "",
|
||||
"backup_controller_page_background_configure_error": "",
|
||||
"backup_controller_page_background_description": "",
|
||||
"backup_controller_page_background_is_off": "",
|
||||
"backup_controller_page_background_is_on": "",
|
||||
"backup_controller_page_background_turn_off": "",
|
||||
"backup_controller_page_background_turn_on": "",
|
||||
"backup_controller_page_background_wifi": "",
|
||||
"backup_controller_page_backup": "Sauvegardé",
|
||||
"backup_controller_page_backup_selected": "Sélectionné : ",
|
||||
"backup_controller_page_backup_sub": "Photos et vidéos sauvegardées",
|
||||
@@ -69,19 +48,6 @@
|
||||
"backup_controller_page_uploading_file_info": "Envoi d'informations sur le fichier",
|
||||
"backup_err_only_album": "Impossible de retirer le seul album",
|
||||
"backup_info_card_assets": "éléments",
|
||||
"cache_settings_album_thumbnails": "",
|
||||
"cache_settings_clear_cache_button": "",
|
||||
"cache_settings_clear_cache_button_title": "",
|
||||
"cache_settings_image_cache_size": "",
|
||||
"cache_settings_statistics_album": "",
|
||||
"cache_settings_statistics_assets": "",
|
||||
"cache_settings_statistics_full": "",
|
||||
"cache_settings_statistics_shared": "",
|
||||
"cache_settings_statistics_thumbnail": "",
|
||||
"cache_settings_statistics_title": "",
|
||||
"cache_settings_subtitle": "",
|
||||
"cache_settings_thumbnail_size": "",
|
||||
"cache_settings_title": "",
|
||||
"control_bottom_app_bar_delete": "Supprimer",
|
||||
"control_bottom_app_bar_share": "Partager",
|
||||
"create_album_page_untitled": "Sans titre",
|
||||
@@ -127,14 +93,6 @@
|
||||
"select_additional_user_for_sharing_page_suggestions": "Suggestions",
|
||||
"select_user_for_sharing_page_err_album": "Échec de la création de l'album",
|
||||
"select_user_for_sharing_page_share_suggestions": "Suggestions",
|
||||
"setting_notifications_notify_failures_grace_period": "",
|
||||
"setting_notifications_notify_hours": "",
|
||||
"setting_notifications_notify_immediately": "",
|
||||
"setting_notifications_notify_minutes": "",
|
||||
"setting_notifications_notify_never": "",
|
||||
"setting_notifications_subtitle": "",
|
||||
"setting_notifications_title": "",
|
||||
"setting_pages_app_bar_settings": "",
|
||||
"share_add": "Ajouter",
|
||||
"share_add_photos": "Ajouter des photos",
|
||||
"share_add_title": "Ajouter un titre",
|
||||
@@ -150,16 +108,6 @@
|
||||
"tab_controller_nav_photos": "Photos",
|
||||
"tab_controller_nav_search": "Recherche",
|
||||
"tab_controller_nav_sharing": "Partage",
|
||||
"theme_setting_asset_list_storage_indicator_title": "",
|
||||
"theme_setting_asset_list_tiles_per_row_title": "",
|
||||
"theme_setting_dark_mode_switch": "",
|
||||
"theme_setting_image_viewer_quality_subtitle": "",
|
||||
"theme_setting_image_viewer_quality_title": "",
|
||||
"theme_setting_system_theme_switch": "",
|
||||
"theme_setting_theme_subtitle": "",
|
||||
"theme_setting_theme_title": "",
|
||||
"theme_setting_three_stage_loading_subtitle": "",
|
||||
"theme_setting_three_stage_loading_title": "",
|
||||
"version_announcement_overlay_ack": "Confirmer",
|
||||
"version_announcement_overlay_release_notes": "notes de mise à jour",
|
||||
"version_announcement_overlay_text_1": "Bonjour, une nouvelle version de",
|
||||
|
||||
@@ -19,7 +19,7 @@ platform :ios do
|
||||
desc "iOS Beta"
|
||||
lane :beta do
|
||||
increment_version_number(
|
||||
version_number: "1.27.0"
|
||||
version_number: "1.28.0"
|
||||
)
|
||||
increment_build_number(
|
||||
build_number: latest_testflight_build_number + 1,
|
||||
|
||||
@@ -6,10 +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
|
||||
@@ -70,9 +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
|
||||
@@ -111,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
|
||||
|
||||
@@ -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 |
|
||||
@@ -81,6 +82,7 @@ Class | Method | HTTP request | Description
|
||||
*AssetApi* | [**getAssetById**](doc//AssetApi.md#getassetbyid) | **GET** /asset/assetById/{assetId} |
|
||||
*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/curated-locations |
|
||||
@@ -113,9 +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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
17
mobile/openapi/doc/AlbumCountResponseDto.md
Normal file
17
mobile/openapi/doc/AlbumCountResponseDto.md
Normal 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)
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ Method | HTTP request | Description
|
||||
[**getAssetById**](AssetApi.md#getassetbyid) | **GET** /asset/assetById/{assetId} |
|
||||
[**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/curated-locations |
|
||||
@@ -363,6 +364,49 @@ 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)
|
||||
|
||||
# **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()
|
||||
|
||||
|
||||
16
mobile/openapi/doc/AssetCountByUserIdResponseDto.md
Normal file
16
mobile/openapi/doc/AssetCountByUserIdResponseDto.md
Normal 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)
|
||||
|
||||
|
||||
@@ -37,9 +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';
|
||||
|
||||
@@ -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:
|
||||
///
|
||||
|
||||
@@ -395,6 +395,47 @@ class AssetApi {
|
||||
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
|
||||
|
||||
@@ -198,12 +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':
|
||||
|
||||
127
mobile/openapi/lib/model/album_count_response_dto.dart
Normal file
127
mobile/openapi/lib/model/album_count_response_dto.dart
Normal 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',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
};
|
||||
}
|
||||
|
||||
37
mobile/openapi/test/album_count_response_dto_test.dart
Normal file
37
mobile/openapi/test/album_count_response_dto_test.dart
Normal 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
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
@@ -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
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
@@ -2,7 +2,7 @@ name: immich_mobile
|
||||
description: Immich - selfhosted backup media file on mobile phone
|
||||
|
||||
publish_to: "none"
|
||||
version: 1.27.0+37
|
||||
version: 1.28.0+38
|
||||
|
||||
environment:
|
||||
sdk: ">=2.17.0 <3.0.0"
|
||||
|
||||
@@ -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 {
|
||||
@@ -23,6 +24,7 @@ export interface IAlbumRepository {
|
||||
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';
|
||||
@@ -42,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
|
||||
@@ -154,23 +168,23 @@ export class AlbumRepository implements IAlbumRepository {
|
||||
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();
|
||||
.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;
|
||||
}
|
||||
@@ -228,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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -116,7 +116,8 @@ describe('Album service', () => {
|
||||
removeAssets: jest.fn(),
|
||||
removeUser: jest.fn(),
|
||||
updateAlbum: jest.fn(),
|
||||
getListByAssetId: jest.fn()
|
||||
getListByAssetId: jest.fn(),
|
||||
getCountByUserId: jest.fn(),
|
||||
};
|
||||
sut = new AlbumService(albumRepositoryMock);
|
||||
});
|
||||
|
||||
@@ -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 {
|
||||
@@ -118,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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
export class AlbumCountResponseDto {
|
||||
@ApiProperty({ type: 'integer' })
|
||||
owned!: number;
|
||||
|
||||
@ApiProperty({ type: 'integer' })
|
||||
shared!: number;
|
||||
|
||||
@ApiProperty({ type: 'integer' })
|
||||
sharing!: number;
|
||||
|
||||
constructor(owned: number, shared: number, sharing: number) {
|
||||
this.owned = owned;
|
||||
this.shared = shared;
|
||||
this.sharing = sharing;
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import { CuratedObjectsResponseDto } from './response-dto/curated-objects-respon
|
||||
import { AssetCountByTimeBucket } from './response-dto/asset-count-by-time-group-response.dto';
|
||||
import { TimeGroupEnum } from './dto/get-asset-count-by-time-bucket.dto';
|
||||
import { GetAssetByTimeBucketDto } from './dto/get-asset-by-time-bucket.dto';
|
||||
import { AssetCountByUserIdResponseDto } from './response-dto/asset-count-by-user-id-response.dto';
|
||||
|
||||
export interface IAssetRepository {
|
||||
create(
|
||||
@@ -25,6 +26,7 @@ export interface IAssetRepository {
|
||||
getDetectedObjectsByUserId(userId: string): Promise<CuratedObjectsResponseDto[]>;
|
||||
getSearchPropertiesByUserId(userId: string): Promise<SearchPropertiesDto[]>;
|
||||
getAssetCountByTimeBucket(userId: string, timeBucket: TimeGroupEnum): Promise<AssetCountByTimeBucket[]>;
|
||||
getAssetCountByUserId(userId: string): Promise<AssetCountByUserIdResponseDto>;
|
||||
getAssetByTimeBucket(userId: string, getAssetByTimeBucketDto: GetAssetByTimeBucketDto): Promise<AssetEntity[]>;
|
||||
getAssetByChecksum(userId: string, checksum: Buffer): Promise<AssetEntity>;
|
||||
}
|
||||
@@ -38,6 +40,28 @@ export class AssetRepository implements IAssetRepository {
|
||||
private assetRepository: Repository<AssetEntity>,
|
||||
) {}
|
||||
|
||||
async getAssetCountByUserId(userId: string): Promise<AssetCountByUserIdResponseDto> {
|
||||
// Get asset count by AssetType
|
||||
const res = await this.assetRepository
|
||||
.createQueryBuilder('asset')
|
||||
.select(`COUNT(asset.id)`, 'count')
|
||||
.addSelect(`asset.type`, 'type')
|
||||
.where('"userId" = :userId', { userId: userId })
|
||||
.groupBy('asset.type')
|
||||
.getRawMany();
|
||||
|
||||
const assetCountByUserId = new AssetCountByUserIdResponseDto(0, 0);
|
||||
res.map((item) => {
|
||||
if (item.type === 'IMAGE') {
|
||||
assetCountByUserId.photos = item.count;
|
||||
} else if (item.type === 'VIDEO') {
|
||||
assetCountByUserId.videos = item.count;
|
||||
}
|
||||
});
|
||||
|
||||
return assetCountByUserId;
|
||||
}
|
||||
|
||||
async getAssetByTimeBucket(userId: string, getAssetByTimeBucketDto: GetAssetByTimeBucketDto): Promise<AssetEntity[]> {
|
||||
// Get asset entity from a list of time buckets
|
||||
return await this.assetRepository
|
||||
|
||||
@@ -48,6 +48,7 @@ import { AssetCountByTimeBucketResponseDto } from './response-dto/asset-count-by
|
||||
import { GetAssetCountByTimeBucketDto } from './dto/get-asset-count-by-time-bucket.dto';
|
||||
import { GetAssetByTimeBucketDto } from './dto/get-asset-by-time-bucket.dto';
|
||||
import { QueryFailedError } from 'typeorm';
|
||||
import { AssetCountByUserIdResponseDto } from './response-dto/asset-count-by-user-id-response.dto';
|
||||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@ApiBearerAuth()
|
||||
@@ -78,7 +79,13 @@ export class AssetController {
|
||||
const checksum = await this.assetService.calculateChecksum(file.path);
|
||||
|
||||
try {
|
||||
const savedAsset = await this.assetService.createUserAsset(authUser, assetInfo, file.path, file.mimetype, checksum);
|
||||
const savedAsset = await this.assetService.createUserAsset(
|
||||
authUser,
|
||||
assetInfo,
|
||||
file.path,
|
||||
file.mimetype,
|
||||
checksum,
|
||||
);
|
||||
|
||||
if (!savedAsset) {
|
||||
await this.backgroundTaskService.deleteFileOnDisk([
|
||||
@@ -104,7 +111,7 @@ export class AssetController {
|
||||
]); // simulate asset to make use of delete queue (or use fs.unlink instead)
|
||||
|
||||
if (err instanceof QueryFailedError && (err as any).constraint === 'UQ_userid_checksum') {
|
||||
const existedAsset = await this.assetService.getAssetByChecksum(authUser.id, checksum)
|
||||
const existedAsset = await this.assetService.getAssetByChecksum(authUser.id, checksum);
|
||||
return new AssetFileUploadResponseDto(existedAsset.id);
|
||||
}
|
||||
|
||||
@@ -172,6 +179,10 @@ export class AssetController {
|
||||
return this.assetService.getAssetCountByTimeBucket(authUser, getAssetCountByTimeGroupDto);
|
||||
}
|
||||
|
||||
@Get('/count-by-user-id')
|
||||
async getAssetCountByUserId(@GetAuthUser() authUser: AuthUserDto): Promise<AssetCountByUserIdResponseDto> {
|
||||
return this.assetService.getAssetCountByUserId(authUser);
|
||||
}
|
||||
/**
|
||||
* Get all AssetEntity belong to the user
|
||||
*/
|
||||
|
||||
@@ -61,6 +61,7 @@ describe('AssetService', () => {
|
||||
getSearchPropertiesByUserId: jest.fn(),
|
||||
getAssetByTimeBucket: jest.fn(),
|
||||
getAssetByChecksum: jest.fn(),
|
||||
getAssetCountByUserId: jest.fn(),
|
||||
};
|
||||
|
||||
sui = new AssetService(assetRepositoryMock, a);
|
||||
|
||||
@@ -35,6 +35,7 @@ import {
|
||||
} from './response-dto/asset-count-by-time-group-response.dto';
|
||||
import { GetAssetCountByTimeBucketDto } from './dto/get-asset-count-by-time-bucket.dto';
|
||||
import { GetAssetByTimeBucketDto } from './dto/get-asset-by-time-bucket.dto';
|
||||
import { AssetCountByUserIdResponseDto } from './response-dto/asset-count-by-user-id-response.dto';
|
||||
|
||||
const fileInfo = promisify(stat);
|
||||
|
||||
@@ -479,4 +480,8 @@ export class AssetService {
|
||||
fileReadStream.pipe(sha1Hash);
|
||||
return deferred;
|
||||
}
|
||||
|
||||
getAssetCountByUserId(authUser: AuthUserDto): Promise<AssetCountByUserIdResponseDto> {
|
||||
return this._assetRepository.getAssetCountByUserId(authUser.id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
export class AssetCountByUserIdResponseDto {
|
||||
@ApiProperty({ type: 'integer' })
|
||||
photos!: number;
|
||||
|
||||
@ApiProperty({ type: 'integer' })
|
||||
videos!: number;
|
||||
|
||||
constructor(photos: number, videos: number) {
|
||||
this.photos = photos;
|
||||
this.videos = videos;
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,7 @@ export interface IServerVersion {
|
||||
|
||||
export const serverVersion: IServerVersion = {
|
||||
major: 1,
|
||||
minor: 27,
|
||||
minor: 28,
|
||||
patch: 0,
|
||||
build: 0,
|
||||
};
|
||||
|
||||
File diff suppressed because one or more lines are too long
3
web/babel.config.cjs
Normal file
3
web/babel.config.cjs
Normal file
@@ -0,0 +1,3 @@
|
||||
module.exports = {
|
||||
presets: [['@babel/preset-env', { targets: { node: 'current' } }], '@babel/preset-typescript']
|
||||
};
|
||||
201
web/jest.config.mjs
Normal file
201
web/jest.config.mjs
Normal file
@@ -0,0 +1,201 @@
|
||||
/*
|
||||
* For a detailed explanation regarding each configuration property, visit:
|
||||
* https://jestjs.io/docs/configuration
|
||||
*/
|
||||
|
||||
export default {
|
||||
// All imported modules in your tests should be mocked automatically
|
||||
// automock: false,
|
||||
|
||||
// Stop running tests after `n` failures
|
||||
// bail: 0,
|
||||
|
||||
// The directory where Jest should store its cached dependency information
|
||||
// cacheDirectory: "/private/var/folders/6n/31wm28711gzbt3gzsxhzxx500000gn/T/jest_dx",
|
||||
|
||||
// Automatically clear mock calls, instances, contexts and results before every test
|
||||
clearMocks: true,
|
||||
|
||||
// Indicates whether the coverage information should be collected while executing the test
|
||||
// collectCoverage: false,
|
||||
|
||||
// An array of glob patterns indicating a set of files for which coverage information should be collected
|
||||
// collectCoverageFrom: undefined,
|
||||
|
||||
// The directory where Jest should output its coverage files
|
||||
// coverageDirectory: undefined,
|
||||
|
||||
// An array of regexp pattern strings used to skip coverage collection
|
||||
// coveragePathIgnorePatterns: [
|
||||
// "/node_modules/"
|
||||
// ],
|
||||
|
||||
// Indicates which provider should be used to instrument code for coverage
|
||||
coverageProvider: 'v8',
|
||||
|
||||
// A list of reporter names that Jest uses when writing coverage reports
|
||||
// coverageReporters: [
|
||||
// "json",
|
||||
// "text",
|
||||
// "lcov",
|
||||
// "clover"
|
||||
// ],
|
||||
|
||||
// An object that configures minimum threshold enforcement for coverage results
|
||||
// coverageThreshold: undefined,
|
||||
|
||||
// A path to a custom dependency extractor
|
||||
// dependencyExtractor: undefined,
|
||||
|
||||
// Make calling deprecated APIs throw helpful error messages
|
||||
// errorOnDeprecated: false,
|
||||
|
||||
// The default configuration for fake timers
|
||||
// fakeTimers: {
|
||||
// "enableGlobally": false
|
||||
// },
|
||||
|
||||
// Force coverage collection from ignored files using an array of glob patterns
|
||||
// forceCoverageMatch: [],
|
||||
|
||||
// A path to a module which exports an async function that is triggered once before all test suites
|
||||
// globalSetup: undefined,
|
||||
|
||||
// A path to a module which exports an async function that is triggered once after all test suites
|
||||
// globalTeardown: undefined,
|
||||
|
||||
// A set of global variables that need to be available in all test environments
|
||||
// globals: {},
|
||||
|
||||
// The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers.
|
||||
// maxWorkers: "50%",
|
||||
|
||||
// An array of directory names to be searched recursively up from the requiring module's location
|
||||
// moduleDirectories: [
|
||||
// "node_modules"
|
||||
// ],
|
||||
|
||||
// An array of file extensions your modules use
|
||||
// moduleFileExtensions: [
|
||||
// "js",
|
||||
// "mjs",
|
||||
// "cjs",
|
||||
// "jsx",
|
||||
// "ts",
|
||||
// "tsx",
|
||||
// "json",
|
||||
// "node"
|
||||
// ],
|
||||
|
||||
// A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module
|
||||
moduleNameMapper: {
|
||||
'^\\$lib(.*)$': '<rootDir>/src/lib$1',
|
||||
'^\\@api(.*)$': '<rootDir>/src/api$1',
|
||||
'^\\@test-data(.*)$': '<rootDir>/src/test-data$1'
|
||||
},
|
||||
|
||||
// An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader
|
||||
// modulePathIgnorePatterns: [],
|
||||
|
||||
// Activates notifications for test results
|
||||
// notify: false,
|
||||
|
||||
// An enum that specifies notification mode. Requires { notify: true }
|
||||
// notifyMode: "failure-change",
|
||||
|
||||
// A preset that is used as a base for Jest's configuration
|
||||
// preset: undefined,
|
||||
|
||||
// Run tests from one or more projects
|
||||
// projects: undefined,
|
||||
|
||||
// Use this configuration option to add custom reporters to Jest
|
||||
// reporters: undefined,
|
||||
|
||||
// Automatically reset mock state before every test
|
||||
// resetMocks: false,
|
||||
|
||||
// Reset the module registry before running each individual test
|
||||
// resetModules: false,
|
||||
|
||||
// A path to a custom resolver
|
||||
// resolver: undefined,
|
||||
|
||||
// Automatically restore mock state and implementation before every test
|
||||
// restoreMocks: false,
|
||||
|
||||
// The root directory that Jest should scan for tests and modules within
|
||||
// rootDir: undefined,
|
||||
|
||||
// A list of paths to directories that Jest should use to search for files in
|
||||
// roots: [
|
||||
// "<rootDir>"
|
||||
// ],
|
||||
|
||||
// Allows you to use a custom runner instead of Jest's default test runner
|
||||
// runner: "jest-runner",
|
||||
|
||||
// The paths to modules that run some code to configure or set up the testing environment before each test
|
||||
// setupFiles: [],
|
||||
|
||||
// A list of paths to modules that run some code to configure or set up the testing framework before each test
|
||||
// setupFilesAfterEnv: [],
|
||||
|
||||
// The number of seconds after which a test is considered as slow and reported as such in the results.
|
||||
// slowTestThreshold: 5,
|
||||
|
||||
// A list of paths to snapshot serializer modules Jest should use for snapshot testing
|
||||
// snapshotSerializers: [],
|
||||
|
||||
// The test environment that will be used for testing
|
||||
testEnvironment: 'jsdom',
|
||||
|
||||
// Options that will be passed to the testEnvironment
|
||||
// testEnvironmentOptions: {},
|
||||
|
||||
// Adds a location field to test results
|
||||
// testLocationInResults: false,
|
||||
|
||||
// The glob patterns Jest uses to detect test files
|
||||
// testMatch: [
|
||||
// "**/__tests__/**/*.[jt]s?(x)",
|
||||
// "**/?(*.)+(spec|test).[tj]s?(x)"
|
||||
// ],
|
||||
|
||||
// An array of regexp pattern strings that are matched against all test paths, matched tests are skipped
|
||||
// testPathIgnorePatterns: [
|
||||
// "/node_modules/"
|
||||
// ],
|
||||
|
||||
// The regexp pattern or array of patterns that Jest uses to detect test files
|
||||
// testRegex: [],
|
||||
|
||||
// This option allows the use of a custom results processor
|
||||
// testResultsProcessor: undefined,
|
||||
|
||||
// This option allows use of a custom test runner
|
||||
// testRunner: "jest-circus/runner",
|
||||
|
||||
// A map from regular expressions to paths to transformers
|
||||
transform: {
|
||||
'\\.[jt]sx?$': 'babel-jest'
|
||||
}
|
||||
|
||||
// An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation
|
||||
// transformIgnorePatterns: [
|
||||
// "/node_modules/",
|
||||
// "\\.pnp\\.[^\\/]+$"
|
||||
// ],
|
||||
|
||||
// An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them
|
||||
// unmockedModulePathPatterns: undefined,
|
||||
|
||||
// Indicates whether each individual test should be reported during the run
|
||||
// verbose: undefined,
|
||||
|
||||
// An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode
|
||||
// watchPathIgnorePatterns: [],
|
||||
|
||||
// Whether to use watchman for file crawling
|
||||
// watchman: true,
|
||||
};
|
||||
8577
web/package-lock.json
generated
8577
web/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -9,25 +9,17 @@
|
||||
"check": "svelte-check --tsconfig ./tsconfig.json",
|
||||
"check:watch": "svelte-check --tsconfig ./tsconfig.json --watch",
|
||||
"lint": "prettier --check --plugin-search-dir=. . && eslint .",
|
||||
"format": "prettier --write --plugin-search-dir=. ."
|
||||
"format": "prettier --write --plugin-search-dir=. .",
|
||||
"test": "jest",
|
||||
"test:watch": "npm test -- --watch"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/preset-env": "^7.19.0",
|
||||
"@babel/preset-typescript": "^7.18.6",
|
||||
"@faker-js/faker": "^7.5.0",
|
||||
"@sveltejs/adapter-auto": "next",
|
||||
"@sveltejs/kit": "next",
|
||||
"@typescript-eslint/eslint-plugin": "^5.27.0",
|
||||
"@typescript-eslint/parser": "^5.27.0",
|
||||
"eslint": "^8.16.0",
|
||||
"eslint-config-prettier": "^8.3.0",
|
||||
"eslint-plugin-svelte3": "^4.0.0",
|
||||
"prettier": "^2.6.2",
|
||||
"prettier-plugin-svelte": "^2.7.0",
|
||||
"svelte": "^3.44.0",
|
||||
"svelte-check": "^2.7.1",
|
||||
"svelte-preprocess": "^4.10.6",
|
||||
"tslib": "^2.3.1",
|
||||
"typescript": "^4.7.4",
|
||||
"vite": "^3.0.0",
|
||||
"@sveltejs/adapter-node": "next",
|
||||
"@sveltejs/kit": "next",
|
||||
"@types/bcrypt": "^5.0.0",
|
||||
"@types/cookie": "^0.4.1",
|
||||
"@types/fluent-ffmpeg": "^2.1.20",
|
||||
@@ -35,9 +27,27 @@
|
||||
"@types/lodash": "^4.14.182",
|
||||
"@types/lodash-es": "^4.17.6",
|
||||
"@types/socket.io-client": "^3.0.0",
|
||||
"@typescript-eslint/eslint-plugin": "^5.27.0",
|
||||
"@typescript-eslint/parser": "^5.27.0",
|
||||
"autoprefixer": "^10.4.7",
|
||||
"babel-jest": "^29.0.2",
|
||||
"eslint": "^8.16.0",
|
||||
"eslint-config-prettier": "^8.3.0",
|
||||
"eslint-plugin-svelte3": "^4.0.0",
|
||||
"factory.ts": "^1.2.0",
|
||||
"jest": "^29.0.2",
|
||||
"jest-environment-jsdom": "^29.0.2",
|
||||
"postcss": "^8.4.13",
|
||||
"tailwindcss": "^3.0.24"
|
||||
"prettier": "^2.6.2",
|
||||
"prettier-plugin-svelte": "^2.7.0",
|
||||
"svelte": "^3.44.0",
|
||||
"svelte-check": "^2.7.1",
|
||||
"svelte-jester": "^2.3.2",
|
||||
"svelte-preprocess": "^4.10.6",
|
||||
"tailwindcss": "^3.0.24",
|
||||
"tslib": "^2.3.1",
|
||||
"typescript": "^4.7.4",
|
||||
"vite": "^3.0.0"
|
||||
},
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
|
||||
@@ -84,6 +84,31 @@ export interface AdminSignupResponseDto {
|
||||
*/
|
||||
'createdAt': string;
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
* @interface AlbumCountResponseDto
|
||||
*/
|
||||
export interface AlbumCountResponseDto {
|
||||
/**
|
||||
*
|
||||
* @type {number}
|
||||
* @memberof AlbumCountResponseDto
|
||||
*/
|
||||
'owned': number;
|
||||
/**
|
||||
*
|
||||
* @type {number}
|
||||
* @memberof AlbumCountResponseDto
|
||||
*/
|
||||
'shared': number;
|
||||
/**
|
||||
*
|
||||
* @type {number}
|
||||
* @memberof AlbumCountResponseDto
|
||||
*/
|
||||
'sharing': number;
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
@@ -183,6 +208,25 @@ export interface AssetCountByTimeBucketResponseDto {
|
||||
*/
|
||||
'buckets': Array<AssetCountByTimeBucket>;
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
* @interface AssetCountByUserIdResponseDto
|
||||
*/
|
||||
export interface AssetCountByUserIdResponseDto {
|
||||
/**
|
||||
*
|
||||
* @type {number}
|
||||
* @memberof AssetCountByUserIdResponseDto
|
||||
*/
|
||||
'photos': number;
|
||||
/**
|
||||
*
|
||||
* @type {number}
|
||||
* @memberof AssetCountByUserIdResponseDto
|
||||
*/
|
||||
'videos': number;
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
@@ -1408,6 +1452,39 @@ export const AlbumApiAxiosParamCreator = function (configuration?: Configuration
|
||||
|
||||
|
||||
|
||||
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
||||
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
||||
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
|
||||
|
||||
return {
|
||||
url: toPathString(localVarUrlObj),
|
||||
options: localVarRequestOptions,
|
||||
};
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
getAlbumCountByUserId: async (options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
|
||||
const localVarPath = `/album/count-by-user-id`;
|
||||
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
||||
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
|
||||
let baseOptions;
|
||||
if (configuration) {
|
||||
baseOptions = configuration.baseOptions;
|
||||
}
|
||||
|
||||
const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};
|
||||
const localVarHeaderParameter = {} as any;
|
||||
const localVarQueryParameter = {} as any;
|
||||
|
||||
// authentication bearer required
|
||||
// http bearer authentication required
|
||||
await setBearerAuthToObject(localVarHeaderParameter, configuration)
|
||||
|
||||
|
||||
|
||||
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
||||
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
||||
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
|
||||
@@ -1676,6 +1753,15 @@ export const AlbumApiFp = function(configuration?: Configuration) {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.deleteAlbum(albumId, options);
|
||||
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
async getAlbumCountByUserId(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<AlbumCountResponseDto>> {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.getAlbumCountByUserId(options);
|
||||
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {string} albumId
|
||||
@@ -1778,6 +1864,14 @@ export const AlbumApiFactory = function (configuration?: Configuration, basePath
|
||||
deleteAlbum(albumId: string, options?: any): AxiosPromise<void> {
|
||||
return localVarFp.deleteAlbum(albumId, options).then((request) => request(axios, basePath));
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
getAlbumCountByUserId(options?: any): AxiosPromise<AlbumCountResponseDto> {
|
||||
return localVarFp.getAlbumCountByUserId(options).then((request) => request(axios, basePath));
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {string} albumId
|
||||
@@ -1883,6 +1977,16 @@ export class AlbumApi extends BaseAPI {
|
||||
return AlbumApiFp(this.configuration).deleteAlbum(albumId, options).then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
* @memberof AlbumApi
|
||||
*/
|
||||
public getAlbumCountByUserId(options?: AxiosRequestConfig) {
|
||||
return AlbumApiFp(this.configuration).getAlbumCountByUserId(options).then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} albumId
|
||||
@@ -2236,6 +2340,39 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
|
||||
options: localVarRequestOptions,
|
||||
};
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
getAssetCountByUserId: async (options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
|
||||
const localVarPath = `/asset/count-by-user-id`;
|
||||
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
||||
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
|
||||
let baseOptions;
|
||||
if (configuration) {
|
||||
baseOptions = configuration.baseOptions;
|
||||
}
|
||||
|
||||
const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};
|
||||
const localVarHeaderParameter = {} as any;
|
||||
const localVarQueryParameter = {} as any;
|
||||
|
||||
// authentication bearer required
|
||||
// http bearer authentication required
|
||||
await setBearerAuthToObject(localVarHeaderParameter, configuration)
|
||||
|
||||
|
||||
|
||||
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
||||
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
||||
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
|
||||
|
||||
return {
|
||||
url: toPathString(localVarUrlObj),
|
||||
options: localVarRequestOptions,
|
||||
};
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {*} [options] Override http request option.
|
||||
@@ -2640,6 +2777,15 @@ export const AssetApiFp = function(configuration?: Configuration) {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.getAssetCountByTimeBucket(getAssetCountByTimeBucketDto, options);
|
||||
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
async getAssetCountByUserId(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<AssetCountByUserIdResponseDto>> {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.getAssetCountByUserId(options);
|
||||
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {*} [options] Override http request option.
|
||||
@@ -2800,6 +2946,14 @@ export const AssetApiFactory = function (configuration?: Configuration, basePath
|
||||
getAssetCountByTimeBucket(getAssetCountByTimeBucketDto: GetAssetCountByTimeBucketDto, options?: any): AxiosPromise<AssetCountByTimeBucketResponseDto> {
|
||||
return localVarFp.getAssetCountByTimeBucket(getAssetCountByTimeBucketDto, options).then((request) => request(axios, basePath));
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
getAssetCountByUserId(options?: any): AxiosPromise<AssetCountByUserIdResponseDto> {
|
||||
return localVarFp.getAssetCountByUserId(options).then((request) => request(axios, basePath));
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {*} [options] Override http request option.
|
||||
@@ -2966,6 +3120,16 @@ export class AssetApi extends BaseAPI {
|
||||
return AssetApiFp(this.configuration).getAssetCountByTimeBucket(getAssetCountByTimeBucketDto, options).then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
* @memberof AssetApi
|
||||
*/
|
||||
public getAssetCountByUserId(options?: AxiosRequestConfig) {
|
||||
return AssetApiFp(this.configuration).getAssetCountByUserId(options).then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {*} [options] Override http request option.
|
||||
|
||||
@@ -1,3 +1,16 @@
|
||||
<script lang="ts" context="module">
|
||||
type OnShowContextMenu = {
|
||||
showalbumcontextmenu: OnShowContextMenuDetail;
|
||||
};
|
||||
|
||||
type OnClick = {
|
||||
click: OnClickDetail;
|
||||
};
|
||||
|
||||
export type OnShowContextMenuDetail = { x: number; y: number };
|
||||
export type OnClickDetail = AlbumResponseDto;
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { AlbumResponseDto, api, ThumbnailFormat } from '@api';
|
||||
import { createEventDispatcher, onMount } from 'svelte';
|
||||
@@ -8,7 +21,8 @@
|
||||
export let album: AlbumResponseDto;
|
||||
|
||||
let imageData: string = `/api/asset/thumbnail/${album.albumThumbnailAssetId}?format=${ThumbnailFormat.Webp}`;
|
||||
const dispatch = createEventDispatcher();
|
||||
const dispatchClick = createEventDispatcher<OnClick>();
|
||||
const dispatchShowContextMenu = createEventDispatcher<OnShowContextMenu>();
|
||||
|
||||
const loadHighQualityThumbnail = async (thubmnailId: string | null) => {
|
||||
if (thubmnailId == null) {
|
||||
@@ -25,7 +39,7 @@
|
||||
};
|
||||
|
||||
const showAlbumContextMenu = (e: MouseEvent) => {
|
||||
dispatch('showalbumcontextmenu', {
|
||||
dispatchShowContextMenu('showalbumcontextmenu', {
|
||||
x: e.clientX,
|
||||
y: e.clientY
|
||||
});
|
||||
@@ -38,7 +52,7 @@
|
||||
|
||||
<div
|
||||
class="h-[339px] w-[275px] hover:cursor-pointer mt-4 relative"
|
||||
on:click={() => dispatch('click', album)}
|
||||
on:click={() => dispatchClick('click', album)}
|
||||
>
|
||||
<div
|
||||
id={`icon-${album.id}`}
|
||||
|
||||
@@ -205,7 +205,7 @@
|
||||
<div
|
||||
transition:fly={{ duration: 150 }}
|
||||
id="detail-panel"
|
||||
class="bg-immich-bg w-[360px] row-span-full transition-all "
|
||||
class="bg-immich-bg w-[360px] row-span-full transition-all overflow-y-auto"
|
||||
translate="yes"
|
||||
>
|
||||
<DetailPanel {asset} albums={appearsInAlbums} on:close={() => (isShowDetail = false)} />
|
||||
|
||||
@@ -5,11 +5,19 @@
|
||||
import ImageAlbum from 'svelte-material-icons/ImageAlbum.svelte';
|
||||
import ImageOutline from 'svelte-material-icons/ImageOutline.svelte';
|
||||
import AccountMultipleOutline from 'svelte-material-icons/AccountMultipleOutline.svelte';
|
||||
import InformationOutline from 'svelte-material-icons/InformationOutline.svelte';
|
||||
import SideBarButton from './side-bar-button.svelte';
|
||||
import StatusBox from '../status-box.svelte';
|
||||
import { AlbumCountResponseDto, api, AssetCountByUserIdResponseDto } from '@api';
|
||||
import { fade } from 'svelte/transition';
|
||||
import LoadingSpinner from '../loading-spinner.svelte';
|
||||
|
||||
let selectedAction: AppSideBarSelection;
|
||||
|
||||
let showAssetCount: boolean = false;
|
||||
let showSharingCount = false;
|
||||
let showAlbumsCount = false;
|
||||
|
||||
onMount(async () => {
|
||||
if ($page.routeId == 'albums') {
|
||||
selectedAction = AppSideBarSelection.ALBUMS;
|
||||
@@ -19,35 +27,130 @@
|
||||
selectedAction = AppSideBarSelection.SHARING;
|
||||
}
|
||||
});
|
||||
|
||||
const getAssetCount = async () => {
|
||||
const { data: assetCount } = await api.assetApi.getAssetCountByUserId();
|
||||
|
||||
return {
|
||||
videos: assetCount.videos,
|
||||
photos: assetCount.photos
|
||||
};
|
||||
};
|
||||
|
||||
const getAlbumCount = async () => {
|
||||
const { data: albumCount } = await api.albumApi.getAlbumCountByUserId();
|
||||
return {
|
||||
shared: albumCount.shared,
|
||||
sharing: albumCount.sharing,
|
||||
owned: albumCount.owned
|
||||
};
|
||||
};
|
||||
</script>
|
||||
|
||||
<section id="sidebar" class="flex flex-col gap-1 pt-8 pr-6">
|
||||
<a sveltekit:prefetch sveltekit:noscroll href={$page.routeId !== 'photos' ? `/photos` : null}>
|
||||
<a
|
||||
sveltekit:prefetch
|
||||
sveltekit:noscroll
|
||||
href={$page.routeId !== 'photos' ? `/photos` : null}
|
||||
class="relative"
|
||||
>
|
||||
<SideBarButton
|
||||
title="Photos"
|
||||
title={`Photos`}
|
||||
logo={ImageOutline}
|
||||
actionType={AppSideBarSelection.PHOTOS}
|
||||
isSelected={selectedAction === AppSideBarSelection.PHOTOS}
|
||||
/></a
|
||||
>
|
||||
<a sveltekit:prefetch href={$page.routeId !== 'sharing' ? `/sharing` : null}>
|
||||
/>
|
||||
<div
|
||||
id="asset-count-info"
|
||||
class="absolute right-4 top-[15px] z-40 text-xs hover:cursor-help"
|
||||
on:mouseenter={() => (showAssetCount = true)}
|
||||
on:mouseleave={() => (showAssetCount = false)}
|
||||
>
|
||||
<InformationOutline size={18} color="#4250af" />
|
||||
{#if showAssetCount}
|
||||
<div
|
||||
transition:fade={{ duration: 200 }}
|
||||
id="asset-count-info-detail"
|
||||
class="w-32 rounded-lg px-4 py-2 shadow-lg bg-white absolute -right-[135px] top-0 z-[9999] flex place-items-center place-content-center"
|
||||
>
|
||||
{#await getAssetCount()}
|
||||
<LoadingSpinner />
|
||||
{:then data}
|
||||
<div>
|
||||
<p>{data.videos} Videos</p>
|
||||
<p>{data.photos} Photos</p>
|
||||
</div>
|
||||
{/await}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<a sveltekit:prefetch href={$page.routeId !== 'sharing' ? `/sharing` : null} class="relative">
|
||||
<SideBarButton
|
||||
title="Sharing"
|
||||
logo={AccountMultipleOutline}
|
||||
actionType={AppSideBarSelection.SHARING}
|
||||
isSelected={selectedAction === AppSideBarSelection.SHARING}
|
||||
/></a
|
||||
>
|
||||
/>
|
||||
<div
|
||||
id="sharing-count-info"
|
||||
class="absolute right-4 top-[15px] z-40 text-xs hover:cursor-help"
|
||||
on:mouseenter={() => (showSharingCount = true)}
|
||||
on:mouseleave={() => (showSharingCount = false)}
|
||||
>
|
||||
<InformationOutline size={18} color="#4250af" />
|
||||
{#if showSharingCount}
|
||||
<div
|
||||
transition:fade={{ duration: 200 }}
|
||||
id="asset-count-info-detail"
|
||||
class="w-32 rounded-lg px-4 py-2 shadow-lg bg-white absolute -right-[135px] top-0 z-[9999] flex place-items-center place-content-center"
|
||||
>
|
||||
{#await getAlbumCount()}
|
||||
<LoadingSpinner />
|
||||
{:then data}
|
||||
<div>
|
||||
<p>{data.shared + data.sharing} albums</p>
|
||||
</div>
|
||||
{/await}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</a>
|
||||
<div class="text-xs ml-5 my-4">
|
||||
<p>LIBRARY</p>
|
||||
</div>
|
||||
<a sveltekit:prefetch href={$page.routeId !== 'albums' ? `/albums` : null}>
|
||||
<a sveltekit:prefetch href={$page.routeId !== 'albums' ? `/albums` : null} class="relative">
|
||||
<SideBarButton
|
||||
title="Albums"
|
||||
logo={ImageAlbum}
|
||||
actionType={AppSideBarSelection.ALBUMS}
|
||||
isSelected={selectedAction === AppSideBarSelection.ALBUMS}
|
||||
/>
|
||||
|
||||
<div
|
||||
id="album-count-info"
|
||||
class="absolute right-4 top-[15px] z-40 text-xs hover:cursor-help"
|
||||
on:mouseenter={() => (showAlbumsCount = true)}
|
||||
on:mouseleave={() => (showAlbumsCount = false)}
|
||||
>
|
||||
<InformationOutline size={18} color="#4250af" />
|
||||
{#if showAlbumsCount}
|
||||
<div
|
||||
transition:fade={{ duration: 200 }}
|
||||
id="asset-count-info-detail"
|
||||
class="w-32 rounded-lg px-4 py-2 shadow-lg bg-white absolute -right-[135px] top-0 z-[9999] flex place-items-center place-content-center"
|
||||
>
|
||||
{#await getAlbumCount()}
|
||||
<LoadingSpinner />
|
||||
{:then data}
|
||||
<div>
|
||||
<p>{data.owned} albums</p>
|
||||
</div>
|
||||
{/await}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<!-- Status Box -->
|
||||
|
||||
@@ -21,7 +21,7 @@ export const checkAppVersion = async (): Promise<CheckAppVersionReponse> => {
|
||||
|
||||
if (!appVersion) {
|
||||
return {
|
||||
shouldShowAnnouncement: true,
|
||||
shouldShowAnnouncement: false,
|
||||
remoteVersion: latestRelease.tag_name,
|
||||
localVersion: 'empty'
|
||||
};
|
||||
|
||||
@@ -6,93 +6,32 @@
|
||||
import MenuOption from '$lib/components/shared-components/context-menu/menu-option.svelte';
|
||||
import DeleteOutline from 'svelte-material-icons/DeleteOutline.svelte';
|
||||
import type { PageData } from './$types';
|
||||
import { AlbumResponseDto, api } from '@api';
|
||||
import NavigationBar from '$lib/components/shared-components/navigation-bar.svelte';
|
||||
import SideBar from '$lib/components/shared-components/side-bar/side-bar.svelte';
|
||||
import PlusBoxOutline from 'svelte-material-icons/PlusBoxOutline.svelte';
|
||||
import {
|
||||
notificationController,
|
||||
NotificationType
|
||||
} from '$lib/components/shared-components/notification/notification';
|
||||
import { useAlbums } from './albums-bloc';
|
||||
|
||||
export let data: PageData;
|
||||
|
||||
let isShowContextMenu = false;
|
||||
let contextMenuPosition = { x: 0, y: 0 };
|
||||
let targetAlbum: AlbumResponseDto;
|
||||
const {
|
||||
albums,
|
||||
isShowContextMenu,
|
||||
contextMenuPosition,
|
||||
createAlbum,
|
||||
deleteSelectedContextAlbum,
|
||||
loadAlbums,
|
||||
showAlbumContextMenu,
|
||||
closeAlbumContextMenu
|
||||
} = useAlbums({ albums: data.albums });
|
||||
|
||||
onMount(async () => {
|
||||
const getAllAlbumsRes = await api.albumApi.getAllAlbums();
|
||||
data.albums = getAllAlbumsRes.data;
|
||||
|
||||
// Delete album that has no photos and is named 'Untitled'
|
||||
for (const album of data.albums) {
|
||||
if (album.albumName === 'Untitled' && album.assetCount === 0) {
|
||||
setTimeout(async () => {
|
||||
await autoDeleteAlbum(album);
|
||||
data.albums = data.albums.filter((a) => a.id !== album.id);
|
||||
}, 500);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const createAlbum = async () => {
|
||||
try {
|
||||
const { data: newAlbum } = await api.albumApi.createAlbum({
|
||||
albumName: 'Untitled'
|
||||
});
|
||||
onMount(loadAlbums);
|
||||
|
||||
const handleCreateAlbum = async () => {
|
||||
const newAlbum = await createAlbum();
|
||||
if (newAlbum) {
|
||||
goto('/albums/' + newAlbum.id);
|
||||
} catch (e) {
|
||||
console.error('Error [createAlbum] ', e);
|
||||
notificationController.show({
|
||||
message: 'Error creating album, check console for more details',
|
||||
type: NotificationType.Error
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const autoDeleteAlbum = async (album: AlbumResponseDto) => {
|
||||
try {
|
||||
await api.albumApi.deleteAlbum(album.id);
|
||||
return true;
|
||||
} catch (e) {
|
||||
console.error('Error [autoDeleteAlbum] ', e);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const userDeleteMenu = async () => {
|
||||
if (
|
||||
window.confirm(
|
||||
`Are you sure you want to delete album ${targetAlbum.albumName}? If the album is shared, other users will not be able to access it.`
|
||||
)
|
||||
) {
|
||||
try {
|
||||
await api.albumApi.deleteAlbum(targetAlbum.id);
|
||||
data.albums = data.albums.filter((a) => a.id !== targetAlbum.id);
|
||||
} catch (e) {
|
||||
console.error('Error [userDeleteMenu] ', e);
|
||||
notificationController.show({
|
||||
message: 'Error deleting user, check console for more details',
|
||||
type: NotificationType.Error
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
isShowContextMenu = false;
|
||||
};
|
||||
|
||||
const showAlbumContextMenu = (event: CustomEvent, album: AlbumResponseDto) => {
|
||||
targetAlbum = album;
|
||||
|
||||
contextMenuPosition = {
|
||||
x: event.detail.x,
|
||||
y: event.detail.y
|
||||
};
|
||||
|
||||
isShowContextMenu = !isShowContextMenu;
|
||||
};
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
@@ -116,7 +55,7 @@
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button on:click={createAlbum} class="immich-text-button text-sm">
|
||||
<button on:click={handleCreateAlbum} class="immich-text-button text-sm">
|
||||
<span>
|
||||
<PlusBoxOutline size="18" />
|
||||
</span>
|
||||
@@ -131,17 +70,20 @@
|
||||
|
||||
<!-- Album Card -->
|
||||
<div class="flex flex-wrap gap-8">
|
||||
{#each data.albums as album}
|
||||
{#each $albums as album}
|
||||
{#key album.id}
|
||||
<a sveltekit:prefetch href={`albums/${album.id}`}>
|
||||
<AlbumCard {album} on:showalbumcontextmenu={(e) => showAlbumContextMenu(e, album)} />
|
||||
<AlbumCard
|
||||
{album}
|
||||
on:showalbumcontextmenu={(e) => showAlbumContextMenu(e.detail, album)}
|
||||
/>
|
||||
</a>
|
||||
{/key}
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<!-- Empty Message -->
|
||||
{#if data.albums.length === 0}
|
||||
{#if $albums.length === 0}
|
||||
<div
|
||||
class="border p-5 w-[50%] m-auto mt-10 bg-gray-50 rounded-3xl flex flex-col place-content-center place-items-center"
|
||||
>
|
||||
@@ -156,9 +98,9 @@
|
||||
</section>
|
||||
|
||||
<!-- Context Menu -->
|
||||
{#if isShowContextMenu}
|
||||
<ContextMenu {...contextMenuPosition} on:clickoutside={() => (isShowContextMenu = false)}>
|
||||
<MenuOption on:click={userDeleteMenu}>
|
||||
{#if $isShowContextMenu}
|
||||
<ContextMenu {...$contextMenuPosition} on:clickoutside={closeAlbumContextMenu}>
|
||||
<MenuOption on:click={deleteSelectedContextAlbum}>
|
||||
<span class="flex place-items-center place-content-center gap-2">
|
||||
<DeleteOutline size="18" />
|
||||
<p>Delete album</p>
|
||||
|
||||
185
web/src/routes/albums/__tests__/albums-bloc.spec.ts
Normal file
185
web/src/routes/albums/__tests__/albums-bloc.spec.ts
Normal file
@@ -0,0 +1,185 @@
|
||||
import { jest, describe, it, expect, beforeEach, afterEach } from '@jest/globals';
|
||||
import { useAlbums } from '../albums-bloc';
|
||||
import { api, CreateAlbumDto } from '@api';
|
||||
import {
|
||||
notificationController,
|
||||
NotificationType
|
||||
} from '$lib/components/shared-components/notification/notification';
|
||||
import { albumFactory } from '@test-data';
|
||||
import { get } from 'svelte/store';
|
||||
|
||||
jest.mock('@api');
|
||||
|
||||
const apiMock: jest.MockedObject<typeof api> = api as jest.MockedObject<typeof api>;
|
||||
|
||||
function mockWindowConfirm(result: boolean) {
|
||||
jest.spyOn(global, 'confirm').mockReturnValueOnce(result);
|
||||
}
|
||||
|
||||
describe('Albums BLoC', () => {
|
||||
let sut: ReturnType<typeof useAlbums>;
|
||||
const _albums = albumFactory.buildList(5);
|
||||
|
||||
beforeEach(() => {
|
||||
sut = useAlbums({ albums: [..._albums] });
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
const notifications = get(notificationController.notificationList);
|
||||
|
||||
notifications.forEach((notification) =>
|
||||
notificationController.removeNotificationById(notification.id)
|
||||
);
|
||||
});
|
||||
|
||||
it('inits with provided albums', () => {
|
||||
const albums = get(sut.albums);
|
||||
expect(albums.length).toEqual(5);
|
||||
expect(albums).toEqual(_albums);
|
||||
});
|
||||
|
||||
it('loads albums from the server', async () => {
|
||||
// TODO: this method currently deletes albums with no assets and albumName === 'Untitled' which might not be the best approach
|
||||
const loadedAlbums = [..._albums, albumFactory.build({ id: 'new_loaded_uuid' })];
|
||||
|
||||
apiMock.albumApi.getAllAlbums.mockResolvedValueOnce({
|
||||
data: loadedAlbums,
|
||||
config: {},
|
||||
headers: {},
|
||||
status: 200,
|
||||
statusText: ''
|
||||
});
|
||||
|
||||
await sut.loadAlbums();
|
||||
const albums = get(sut.albums);
|
||||
|
||||
expect(apiMock.albumApi.getAllAlbums).toHaveBeenCalledTimes(1);
|
||||
expect(albums).toEqual(loadedAlbums);
|
||||
});
|
||||
|
||||
it('shows error message when it fails loading albums', async () => {
|
||||
apiMock.albumApi.getAllAlbums.mockRejectedValueOnce({}); // TODO: implement APIProblem interface in the server
|
||||
|
||||
expect(get(notificationController.notificationList)).toHaveLength(0);
|
||||
await sut.loadAlbums();
|
||||
const albums = get(sut.albums);
|
||||
const notifications = get(notificationController.notificationList);
|
||||
|
||||
expect(apiMock.albumApi.getAllAlbums).toHaveBeenCalledTimes(1);
|
||||
expect(albums).toEqual(_albums);
|
||||
expect(notifications).toHaveLength(1);
|
||||
expect(notifications[0].type).toEqual(NotificationType.Error);
|
||||
});
|
||||
|
||||
it('creates a new album', async () => {
|
||||
// TODO: we probably shouldn't hardcode the album name "untitled" here and let the user input the album name before creating it
|
||||
const payload: CreateAlbumDto = {
|
||||
albumName: 'Untitled'
|
||||
};
|
||||
|
||||
const returnedAlbum = albumFactory.build();
|
||||
|
||||
apiMock.albumApi.createAlbum.mockResolvedValueOnce({
|
||||
data: returnedAlbum,
|
||||
config: {},
|
||||
headers: {},
|
||||
status: 200,
|
||||
statusText: ''
|
||||
});
|
||||
|
||||
const newAlbum = await sut.createAlbum();
|
||||
|
||||
expect(apiMock.albumApi.createAlbum).toHaveBeenCalledTimes(1);
|
||||
expect(apiMock.albumApi.createAlbum).toHaveBeenCalledWith(payload);
|
||||
expect(newAlbum).toEqual(returnedAlbum);
|
||||
});
|
||||
|
||||
it('shows error message when it fails creating an album', async () => {
|
||||
apiMock.albumApi.createAlbum.mockRejectedValueOnce({});
|
||||
|
||||
const newAlbum = await sut.createAlbum();
|
||||
const notifications = get(notificationController.notificationList);
|
||||
|
||||
expect(apiMock.albumApi.createAlbum).toHaveBeenCalledTimes(1);
|
||||
expect(newAlbum).not.toBeDefined();
|
||||
expect(notifications).toHaveLength(1);
|
||||
expect(notifications[0].type).toEqual(NotificationType.Error);
|
||||
});
|
||||
|
||||
it('selects an album and deletes it', async () => {
|
||||
apiMock.albumApi.deleteAlbum.mockResolvedValueOnce({
|
||||
data: undefined,
|
||||
config: {},
|
||||
headers: {},
|
||||
status: 200,
|
||||
statusText: ''
|
||||
});
|
||||
|
||||
mockWindowConfirm(true);
|
||||
|
||||
const albumToDelete = get(sut.albums)[2]; // delete third album
|
||||
const albumToDeleteId = albumToDelete.id;
|
||||
const contextMenuCoords = { x: 100, y: 150 };
|
||||
|
||||
expect(get(sut.isShowContextMenu)).toBe(false);
|
||||
sut.showAlbumContextMenu(contextMenuCoords, albumToDelete);
|
||||
expect(get(sut.contextMenuPosition)).toEqual(contextMenuCoords);
|
||||
expect(get(sut.isShowContextMenu)).toBe(true);
|
||||
|
||||
await sut.deleteSelectedContextAlbum();
|
||||
const updatedAlbums = get(sut.albums);
|
||||
|
||||
expect(apiMock.albumApi.deleteAlbum).toHaveBeenCalledTimes(1);
|
||||
expect(apiMock.albumApi.deleteAlbum).toHaveBeenCalledWith(albumToDeleteId);
|
||||
expect(updatedAlbums).toHaveLength(4);
|
||||
expect(updatedAlbums).not.toContain(albumToDelete);
|
||||
expect(get(sut.isShowContextMenu)).toBe(false);
|
||||
});
|
||||
|
||||
it('shows error message when it fails deleting an album', async () => {
|
||||
mockWindowConfirm(true);
|
||||
|
||||
const albumToDelete = get(sut.albums)[2]; // delete third album
|
||||
const contextMenuCoords = { x: 100, y: 150 };
|
||||
|
||||
apiMock.albumApi.deleteAlbum.mockRejectedValueOnce({});
|
||||
|
||||
sut.showAlbumContextMenu(contextMenuCoords, albumToDelete);
|
||||
const newAlbum = await sut.deleteSelectedContextAlbum();
|
||||
const notifications = get(notificationController.notificationList);
|
||||
|
||||
expect(apiMock.albumApi.deleteAlbum).toHaveBeenCalledTimes(1);
|
||||
expect(newAlbum).not.toBeDefined();
|
||||
expect(notifications).toHaveLength(1);
|
||||
expect(notifications[0].type).toEqual(NotificationType.Error);
|
||||
});
|
||||
|
||||
it('prevents deleting an album when rejecting confirm dialog', async () => {
|
||||
const albumToDelete = get(sut.albums)[2]; // delete third album
|
||||
|
||||
mockWindowConfirm(false);
|
||||
|
||||
sut.showAlbumContextMenu({ x: 100, y: 150 }, albumToDelete);
|
||||
await sut.deleteSelectedContextAlbum();
|
||||
|
||||
expect(apiMock.albumApi.deleteAlbum).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('prevents deleting an album when not previously selected', async () => {
|
||||
mockWindowConfirm(true);
|
||||
|
||||
await sut.deleteSelectedContextAlbum();
|
||||
|
||||
expect(apiMock.albumApi.deleteAlbum).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('closes album context menu, deselecting album', () => {
|
||||
const albumToDelete = get(sut.albums)[2]; // delete third album
|
||||
sut.showAlbumContextMenu({ x: 100, y: 150 }, albumToDelete);
|
||||
|
||||
expect(get(sut.isShowContextMenu)).toBe(true);
|
||||
|
||||
sut.closeAlbumContextMenu();
|
||||
expect(get(sut.isShowContextMenu)).toBe(false);
|
||||
});
|
||||
});
|
||||
114
web/src/routes/albums/albums-bloc.ts
Normal file
114
web/src/routes/albums/albums-bloc.ts
Normal file
@@ -0,0 +1,114 @@
|
||||
import {
|
||||
notificationController,
|
||||
NotificationType
|
||||
} from '$lib/components/shared-components/notification/notification';
|
||||
import { AlbumResponseDto, api } from '@api';
|
||||
import { OnShowContextMenuDetail } from '$lib/components/album-page/album-card.svelte';
|
||||
import { writable, derived, get } from 'svelte/store';
|
||||
|
||||
type AlbumsProps = { albums: AlbumResponseDto[] };
|
||||
|
||||
export const useAlbums = (props: AlbumsProps) => {
|
||||
const albums = writable([...props.albums]);
|
||||
const contextMenuPosition = writable<OnShowContextMenuDetail>({ x: 0, y: 0 });
|
||||
const contextMenuTargetAlbum = writable<AlbumResponseDto | undefined>();
|
||||
const isShowContextMenu = derived(contextMenuTargetAlbum, ($selectedAlbum) => !!$selectedAlbum);
|
||||
|
||||
async function loadAlbums(): Promise<void> {
|
||||
try {
|
||||
const { data } = await api.albumApi.getAllAlbums();
|
||||
albums.set(data);
|
||||
|
||||
// Delete album that has no photos and is named 'Untitled'
|
||||
for (const album of data) {
|
||||
if (album.albumName === 'Untitled' && album.assetCount === 0) {
|
||||
setTimeout(async () => {
|
||||
await deleteAlbum(album);
|
||||
const _albums = get(albums);
|
||||
albums.set(_albums.filter((a) => a.id !== album.id));
|
||||
}, 500);
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
notificationController.show({
|
||||
message: 'Error loading albums',
|
||||
type: NotificationType.Error
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function createAlbum(): Promise<AlbumResponseDto | undefined> {
|
||||
try {
|
||||
const { data: newAlbum } = await api.albumApi.createAlbum({
|
||||
albumName: 'Untitled'
|
||||
});
|
||||
|
||||
return newAlbum;
|
||||
} catch {
|
||||
notificationController.show({
|
||||
message: 'Error creating album',
|
||||
type: NotificationType.Error
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteAlbum(album: AlbumResponseDto): Promise<void> {
|
||||
try {
|
||||
await api.albumApi.deleteAlbum(album.id);
|
||||
} catch {
|
||||
// Do nothing?
|
||||
}
|
||||
}
|
||||
|
||||
async function showAlbumContextMenu(
|
||||
contextMenuDetail: OnShowContextMenuDetail,
|
||||
album: AlbumResponseDto
|
||||
): Promise<void> {
|
||||
contextMenuTargetAlbum.set(album);
|
||||
|
||||
contextMenuPosition.set({
|
||||
x: contextMenuDetail.x,
|
||||
y: contextMenuDetail.y
|
||||
});
|
||||
}
|
||||
|
||||
function closeAlbumContextMenu() {
|
||||
contextMenuTargetAlbum.set(undefined);
|
||||
}
|
||||
|
||||
async function deleteSelectedContextAlbum(): Promise<void> {
|
||||
const albumToDelete = get(contextMenuTargetAlbum);
|
||||
if (!albumToDelete) {
|
||||
return;
|
||||
}
|
||||
if (
|
||||
window.confirm(
|
||||
`Are you sure you want to delete album ${albumToDelete.albumName}? If the album is shared, other users will not be able to access it.`
|
||||
)
|
||||
) {
|
||||
try {
|
||||
await api.albumApi.deleteAlbum(albumToDelete.id);
|
||||
const _albums = get(albums);
|
||||
albums.set(_albums.filter((a) => a.id !== albumToDelete.id));
|
||||
} catch {
|
||||
notificationController.show({
|
||||
message: 'Error deleting album',
|
||||
type: NotificationType.Error
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
closeAlbumContextMenu();
|
||||
}
|
||||
|
||||
return {
|
||||
albums,
|
||||
isShowContextMenu,
|
||||
contextMenuPosition,
|
||||
loadAlbums,
|
||||
createAlbum,
|
||||
showAlbumContextMenu,
|
||||
closeAlbumContextMenu,
|
||||
deleteSelectedContextAlbum
|
||||
};
|
||||
};
|
||||
@@ -26,14 +26,6 @@
|
||||
|
||||
export let data: PageData;
|
||||
|
||||
onMount(async () => {
|
||||
openWebsocketConnection();
|
||||
|
||||
return () => {
|
||||
closeWebsocketConnection();
|
||||
};
|
||||
});
|
||||
|
||||
const deleteSelectedAssetHandler = async () => {
|
||||
try {
|
||||
if (
|
||||
|
||||
15
web/src/test-data/factories/album-factory.ts
Normal file
15
web/src/test-data/factories/album-factory.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { AlbumResponseDto } from '@api';
|
||||
import { Sync } from 'factory.ts';
|
||||
import { faker } from '@faker-js/faker';
|
||||
|
||||
export const albumFactory = Sync.makeFactory<AlbumResponseDto>({
|
||||
albumName: Sync.each(() => faker.commerce.product()),
|
||||
albumThumbnailAssetId: null,
|
||||
assetCount: Sync.each((i) => i % 5),
|
||||
assets: [],
|
||||
createdAt: Sync.each(() => faker.date.past().toISOString()),
|
||||
id: Sync.each(() => faker.datatype.uuid()),
|
||||
ownerId: Sync.each(() => faker.datatype.uuid()),
|
||||
shared: false,
|
||||
sharedUsers: []
|
||||
});
|
||||
1
web/src/test-data/index.ts
Normal file
1
web/src/test-data/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './factories/album-factory';
|
||||
@@ -27,6 +27,9 @@
|
||||
],
|
||||
"@api": [
|
||||
"./src/api"
|
||||
],
|
||||
"@test-data": [
|
||||
"./src/test-data"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user