mirror of
https://github.com/immich-app/immich.git
synced 2025-07-09 03:04:16 -04:00
Merge branch 'main' of https://github.com/immich-app/immich into feat/xxhash
This commit is contained in:
commit
c4b7073240
@ -22,6 +22,7 @@ services:
|
|||||||
- IMMICH_METRICS=true
|
- IMMICH_METRICS=true
|
||||||
- IMMICH_ENV=testing
|
- IMMICH_ENV=testing
|
||||||
- IMMICH_PORT=2285
|
- IMMICH_PORT=2285
|
||||||
|
- IMMICH_IGNORE_MOUNT_CHECK_ERRORS=true
|
||||||
volumes:
|
volumes:
|
||||||
- ./test-assets:/test-assets
|
- ./test-assets:/test-assets
|
||||||
extra_hosts:
|
extra_hosts:
|
||||||
|
@ -1,4 +1,23 @@
|
|||||||
{
|
{
|
||||||
|
"all": "All",
|
||||||
|
"shared_with_me": "Shared with me",
|
||||||
|
"my_albums": "My albums",
|
||||||
|
"create_new": "CREATE NEW",
|
||||||
|
"create_album": "Create album",
|
||||||
|
"videos": "Videos",
|
||||||
|
"recently_added": "Recently added",
|
||||||
|
"partners": "Partners",
|
||||||
|
"library": "Library",
|
||||||
|
"on_this_device": "On this device",
|
||||||
|
"add_a_name": "Add a name",
|
||||||
|
"places": "Places",
|
||||||
|
"albums": "Albums",
|
||||||
|
"people": "People",
|
||||||
|
"shared_links": "Shared links",
|
||||||
|
"trash": "Trash",
|
||||||
|
"archived": "Archived",
|
||||||
|
"favorites": "Favorites",
|
||||||
|
"search_albums": "Search albums",
|
||||||
"action_common_back": "Back",
|
"action_common_back": "Back",
|
||||||
"action_common_cancel": "Cancel",
|
"action_common_cancel": "Cancel",
|
||||||
"action_common_clear": "Clear",
|
"action_common_clear": "Clear",
|
||||||
@ -11,11 +30,8 @@
|
|||||||
"advanced_settings_log_level_title": "Log level: {}",
|
"advanced_settings_log_level_title": "Log level: {}",
|
||||||
"advanced_settings_prefer_remote_subtitle": "Some devices are painfully slow to load thumbnails from assets on the device. Activate this setting to load remote images instead.",
|
"advanced_settings_prefer_remote_subtitle": "Some devices are painfully slow to load thumbnails from assets on the device. Activate this setting to load remote images instead.",
|
||||||
"advanced_settings_prefer_remote_title": "Prefer remote images",
|
"advanced_settings_prefer_remote_title": "Prefer remote images",
|
||||||
"advanced_settings_proxy_headers_subtitle": "Define proxy headers Immich should send with each network request",
|
|
||||||
"advanced_settings_proxy_headers_title": "Proxy Headers",
|
|
||||||
"advanced_settings_self_signed_ssl_subtitle": "Skips SSL certificate verification for the server endpoint. Required for self-signed certificates.",
|
"advanced_settings_self_signed_ssl_subtitle": "Skips SSL certificate verification for the server endpoint. Required for self-signed certificates.",
|
||||||
"advanced_settings_self_signed_ssl_title": "Allow self-signed SSL certificates",
|
"advanced_settings_self_signed_ssl_title": "Allow self-signed SSL certificates",
|
||||||
"advanced_settings_tile_subtitle": "Advanced user's settings",
|
|
||||||
"advanced_settings_tile_title": "Advanced",
|
"advanced_settings_tile_title": "Advanced",
|
||||||
"advanced_settings_troubleshooting_subtitle": "Enable additional features for troubleshooting",
|
"advanced_settings_troubleshooting_subtitle": "Enable additional features for troubleshooting",
|
||||||
"advanced_settings_troubleshooting_title": "Troubleshooting",
|
"advanced_settings_troubleshooting_title": "Troubleshooting",
|
||||||
@ -23,7 +39,7 @@
|
|||||||
"album_info_card_backup_album_included": "INCLUDED",
|
"album_info_card_backup_album_included": "INCLUDED",
|
||||||
"album_thumbnail_card_item": "1 item",
|
"album_thumbnail_card_item": "1 item",
|
||||||
"album_thumbnail_card_items": "{} items",
|
"album_thumbnail_card_items": "{} items",
|
||||||
"album_thumbnail_card_shared": " · Shared",
|
"album_thumbnail_card_shared": " \u00b7 Shared",
|
||||||
"album_thumbnail_owned": "Owned",
|
"album_thumbnail_owned": "Owned",
|
||||||
"album_thumbnail_shared_by": "Shared by {}",
|
"album_thumbnail_shared_by": "Shared by {}",
|
||||||
"album_viewer_appbar_delete_confirm": "Are you sure you want to delete this album from your account?",
|
"album_viewer_appbar_delete_confirm": "Are you sure you want to delete this album from your account?",
|
||||||
@ -34,16 +50,13 @@
|
|||||||
"album_viewer_appbar_share_err_title": "Failed to change album title",
|
"album_viewer_appbar_share_err_title": "Failed to change album title",
|
||||||
"album_viewer_appbar_share_leave": "Leave album",
|
"album_viewer_appbar_share_leave": "Leave album",
|
||||||
"album_viewer_appbar_share_remove": "Remove from album",
|
"album_viewer_appbar_share_remove": "Remove from album",
|
||||||
"album_viewer_appbar_share_to": "Share To",
|
|
||||||
"album_viewer_page_share_add_users": "Add users",
|
"album_viewer_page_share_add_users": "Add users",
|
||||||
"all_people_page_title": "People",
|
"all_people_page_title": "People",
|
||||||
"all_videos_page_title": "Videos",
|
"all_videos_page_title": "Videos",
|
||||||
"app_bar_signout_dialog_content": "Are you sure you want to sign out?",
|
"app_bar_signout_dialog_content": "Are you sure you want to sign out?",
|
||||||
"app_bar_signout_dialog_ok": "Yes",
|
"app_bar_signout_dialog_ok": "Yes",
|
||||||
"app_bar_signout_dialog_title": "Sign out",
|
"app_bar_signout_dialog_title": "Sign out",
|
||||||
"archive_page_no_archived_assets": "No archived assets found",
|
|
||||||
"archive_page_title": "Archive ({})",
|
"archive_page_title": "Archive ({})",
|
||||||
"asset_action_delete_err_read_only": "Cannot delete read only asset(s), skipping",
|
|
||||||
"asset_action_share_err_offline": "Cannot fetch offline asset(s), skipping",
|
"asset_action_share_err_offline": "Cannot fetch offline asset(s), skipping",
|
||||||
"asset_list_group_by_sub_title": "Group by",
|
"asset_list_group_by_sub_title": "Group by",
|
||||||
"asset_list_layout_settings_dynamic_layout_title": "Dynamic layout",
|
"asset_list_layout_settings_dynamic_layout_title": "Dynamic layout",
|
||||||
@ -52,7 +65,6 @@
|
|||||||
"asset_list_layout_settings_group_by_month": "Month",
|
"asset_list_layout_settings_group_by_month": "Month",
|
||||||
"asset_list_layout_settings_group_by_month_day": "Month + day",
|
"asset_list_layout_settings_group_by_month_day": "Month + day",
|
||||||
"asset_list_layout_sub_title": "Layout",
|
"asset_list_layout_sub_title": "Layout",
|
||||||
"asset_list_settings_subtitle": "Photo grid layout settings",
|
|
||||||
"asset_list_settings_title": "Photo Grid",
|
"asset_list_settings_title": "Photo Grid",
|
||||||
"asset_restored_successfully": "Asset restored successfully",
|
"asset_restored_successfully": "Asset restored successfully",
|
||||||
"assets_deleted_permanently": "{} asset(s) deleted permanently",
|
"assets_deleted_permanently": "{} asset(s) deleted permanently",
|
||||||
@ -67,14 +79,13 @@
|
|||||||
"backup_album_selection_page_assets_scatter": "Assets can scatter across multiple albums. Thus, albums can be included or excluded during the backup process.",
|
"backup_album_selection_page_assets_scatter": "Assets can scatter across multiple albums. Thus, albums can be included or excluded during the backup process.",
|
||||||
"backup_album_selection_page_select_albums": "Select albums",
|
"backup_album_selection_page_select_albums": "Select albums",
|
||||||
"backup_album_selection_page_selection_info": "Selection Info",
|
"backup_album_selection_page_selection_info": "Selection Info",
|
||||||
"backup_album_selection_page_total_assets": "Total unique assets",
|
|
||||||
"backup_all": "All",
|
"backup_all": "All",
|
||||||
"backup_background_service_backup_failed_message": "Failed to backup assets. Retrying…",
|
"backup_background_service_backup_failed_message": "Failed to backup assets. Retrying\u2026",
|
||||||
"backup_background_service_connection_failed_message": "Failed to connect to the server. Retrying…",
|
"backup_background_service_connection_failed_message": "Failed to connect to the server. Retrying\u2026",
|
||||||
"backup_background_service_current_upload_notification": "Uploading {}",
|
"backup_background_service_current_upload_notification": "Uploading {}",
|
||||||
"backup_background_service_default_notification": "Checking for new assets…",
|
"backup_background_service_default_notification": "Checking for new assets\u2026",
|
||||||
"backup_background_service_error_title": "Backup error",
|
"backup_background_service_error_title": "Backup error",
|
||||||
"backup_background_service_in_progress_notification": "Backing up your assets…",
|
"backup_background_service_in_progress_notification": "Backing up your assets\u2026",
|
||||||
"backup_background_service_upload_failure_notification": "Failed to upload {}",
|
"backup_background_service_upload_failure_notification": "Failed to upload {}",
|
||||||
"backup_controller_page_albums": "Backup Albums",
|
"backup_controller_page_albums": "Backup Albums",
|
||||||
"backup_controller_page_background_app_refresh_disabled_content": "Enable background app refresh in Settings > General > Background App Refresh in order to use background backup.",
|
"backup_controller_page_background_app_refresh_disabled_content": "Enable background app refresh in Settings > General > Background App Refresh in order to use background backup.",
|
||||||
@ -105,7 +116,6 @@
|
|||||||
"backup_controller_page_failed": "Failed ({})",
|
"backup_controller_page_failed": "Failed ({})",
|
||||||
"backup_controller_page_filename": "File name: {} [{}]",
|
"backup_controller_page_filename": "File name: {} [{}]",
|
||||||
"backup_controller_page_id": "ID: {}",
|
"backup_controller_page_id": "ID: {}",
|
||||||
"backup_controller_page_info": "Backup Information",
|
|
||||||
"backup_controller_page_none_selected": "None selected",
|
"backup_controller_page_none_selected": "None selected",
|
||||||
"backup_controller_page_remainder": "Remainder",
|
"backup_controller_page_remainder": "Remainder",
|
||||||
"backup_controller_page_remainder_sub": "Remaining photos and videos to back up from selection",
|
"backup_controller_page_remainder_sub": "Remaining photos and videos to back up from selection",
|
||||||
@ -121,7 +131,6 @@
|
|||||||
"backup_controller_page_turn_off": "Turn off foreground backup",
|
"backup_controller_page_turn_off": "Turn off foreground backup",
|
||||||
"backup_controller_page_turn_on": "Turn on foreground backup",
|
"backup_controller_page_turn_on": "Turn on foreground backup",
|
||||||
"backup_controller_page_uploading_file_info": "Uploading file info",
|
"backup_controller_page_uploading_file_info": "Uploading file info",
|
||||||
"backup_err_only_album": "Cannot remove the only album",
|
|
||||||
"backup_info_card_assets": "assets",
|
"backup_info_card_assets": "assets",
|
||||||
"backup_manual_cancelled": "Cancelled",
|
"backup_manual_cancelled": "Cancelled",
|
||||||
"backup_manual_failed": "Failed",
|
"backup_manual_failed": "Failed",
|
||||||
@ -129,24 +138,9 @@
|
|||||||
"backup_manual_success": "Success",
|
"backup_manual_success": "Success",
|
||||||
"backup_manual_title": "Upload status",
|
"backup_manual_title": "Upload status",
|
||||||
"backup_options_page_title": "Backup options",
|
"backup_options_page_title": "Backup options",
|
||||||
"cache_settings_album_thumbnails": "Library page thumbnails ({} assets)",
|
|
||||||
"cache_settings_clear_cache_button": "Clear cache",
|
|
||||||
"cache_settings_clear_cache_button_title": "Clears the app's cache. This will significantly impact the app's performance until the cache has rebuilt.",
|
|
||||||
"cache_settings_duplicated_assets_clear_button": "CLEAR",
|
"cache_settings_duplicated_assets_clear_button": "CLEAR",
|
||||||
"cache_settings_duplicated_assets_subtitle": "Photos and videos that are black listed by the app",
|
"cache_settings_duplicated_assets_subtitle": "Photos and videos that are black listed by the app",
|
||||||
"cache_settings_duplicated_assets_title": "Duplicated Assets ({})",
|
"cache_settings_duplicated_assets_title": "Duplicated Assets ({})",
|
||||||
"cache_settings_image_cache_size": "Image cache size ({} assets)",
|
|
||||||
"cache_settings_statistics_album": "Library thumbnails",
|
|
||||||
"cache_settings_statistics_assets": "{} assets ({})",
|
|
||||||
"cache_settings_statistics_full": "Full images",
|
|
||||||
"cache_settings_statistics_shared": "Shared album thumbnails",
|
|
||||||
"cache_settings_statistics_thumbnail": "Thumbnails",
|
|
||||||
"cache_settings_statistics_title": "Cache usage",
|
|
||||||
"cache_settings_subtitle": "Control the caching behaviour of the Immich mobile application",
|
|
||||||
"cache_settings_thumbnail_size": "Thumbnail cache size ({} assets)",
|
|
||||||
"cache_settings_tile_subtitle": "Control the local storage behaviour",
|
|
||||||
"cache_settings_tile_title": "Local Storage",
|
|
||||||
"cache_settings_title": "Caching Settings",
|
|
||||||
"change_password_form_confirm_password": "Confirm Password",
|
"change_password_form_confirm_password": "Confirm Password",
|
||||||
"change_password_form_description": "Hi {name},\n\nThis is either the first time you are signing into the system or a request has been made to change your password. Please enter the new password below.",
|
"change_password_form_description": "Hi {name},\n\nThis is either the first time you are signing into the system or a request has been made to change your password. Please enter the new password below.",
|
||||||
"change_password_form_new_password": "New Password",
|
"change_password_form_new_password": "New Password",
|
||||||
@ -167,11 +161,7 @@
|
|||||||
"common_server_error": "Please check your network connection, make sure the server is reachable and app/server versions are compatible.",
|
"common_server_error": "Please check your network connection, make sure the server is reachable and app/server versions are compatible.",
|
||||||
"common_shared": "Shared",
|
"common_shared": "Shared",
|
||||||
"contextual_search": "Sunrise on the beach",
|
"contextual_search": "Sunrise on the beach",
|
||||||
"control_bottom_app_bar_add_to_album": "Add to album",
|
|
||||||
"control_bottom_app_bar_album_info": "{} items",
|
|
||||||
"control_bottom_app_bar_album_info_shared": "{} items · Shared",
|
|
||||||
"control_bottom_app_bar_archive": "Archive",
|
"control_bottom_app_bar_archive": "Archive",
|
||||||
"control_bottom_app_bar_create_new_album": "Create new album",
|
|
||||||
"control_bottom_app_bar_delete": "Delete",
|
"control_bottom_app_bar_delete": "Delete",
|
||||||
"control_bottom_app_bar_delete_from_immich": "Delete from Immich",
|
"control_bottom_app_bar_delete_from_immich": "Delete from Immich",
|
||||||
"control_bottom_app_bar_delete_from_local": "Delete from device",
|
"control_bottom_app_bar_delete_from_local": "Delete from device",
|
||||||
@ -194,17 +184,13 @@
|
|||||||
"create_shared_album_page_share_select_photos": "Select Photos",
|
"create_shared_album_page_share_select_photos": "Select Photos",
|
||||||
"crop": "Crop",
|
"crop": "Crop",
|
||||||
"curated_location_page_title": "Places",
|
"curated_location_page_title": "Places",
|
||||||
"curated_object_page_title": "Things",
|
"date_format": "E, LLL d, y \u2022 h:mm a",
|
||||||
"daily_title_text_date": "E, MMM dd",
|
|
||||||
"daily_title_text_date_year": "E, MMM dd, yyyy",
|
|
||||||
"date_format": "E, LLL d, y • h:mm a",
|
|
||||||
"delete_dialog_alert": "These items will be permanently deleted from Immich and from your device",
|
"delete_dialog_alert": "These items will be permanently deleted from Immich and from your device",
|
||||||
"delete_dialog_alert_local": "These items will be permanently removed from your device but still be available on the Immich server",
|
"delete_dialog_alert_local": "These items will be permanently removed from your device but still be available on the Immich server",
|
||||||
"delete_dialog_alert_local_non_backed_up": "Some of the items aren't backed up to Immich and will be permanently removed from your device",
|
"delete_dialog_alert_local_non_backed_up": "Some of the items aren't backed up to Immich and will be permanently removed from your device",
|
||||||
"delete_dialog_alert_remote": "These items will be permanently deleted from the Immich server",
|
"delete_dialog_alert_remote": "These items will be permanently deleted from the Immich server",
|
||||||
"delete_dialog_cancel": "Cancel",
|
"delete_dialog_cancel": "Cancel",
|
||||||
"delete_dialog_ok": "Delete",
|
"delete_dialog_ok": "Delete",
|
||||||
"delete_dialog_ok_force": "Delete Anyway",
|
|
||||||
"delete_dialog_title": "Delete Permanently",
|
"delete_dialog_title": "Delete Permanently",
|
||||||
"delete_local_dialog_ok_backed_up_only": "Delete Backed Up Only",
|
"delete_local_dialog_ok_backed_up_only": "Delete Backed Up Only",
|
||||||
"delete_local_dialog_ok_force": "Delete Anyway",
|
"delete_local_dialog_ok_force": "Delete Anyway",
|
||||||
@ -212,26 +198,16 @@
|
|||||||
"delete_shared_link_dialog_title": "Delete Shared Link",
|
"delete_shared_link_dialog_title": "Delete Shared Link",
|
||||||
"description_input_hint_text": "Add description...",
|
"description_input_hint_text": "Add description...",
|
||||||
"description_input_submit_error": "Error updating description, check the log for more details",
|
"description_input_submit_error": "Error updating description, check the log for more details",
|
||||||
"download_error": "Download Error",
|
|
||||||
"download_started": "Download started",
|
|
||||||
"download_sucess": "Download success",
|
|
||||||
"download_sucess_android": "The media has been downloaded to DCIM/Immich",
|
|
||||||
"edit_date_time_dialog_date_time": "Date and Time",
|
"edit_date_time_dialog_date_time": "Date and Time",
|
||||||
"edit_date_time_dialog_timezone": "Timezone",
|
"edit_date_time_dialog_timezone": "Timezone",
|
||||||
"edit_image_title": "Edit",
|
"edit_image_title": "Edit",
|
||||||
"edit_location_dialog_title": "Location",
|
"edit_location_dialog_title": "Location",
|
||||||
"error_saving_image": "Error: {}",
|
"error_saving_image": "Error: {}",
|
||||||
"exif_bottom_sheet_description": "Add Description...",
|
|
||||||
"exif_bottom_sheet_details": "DETAILS",
|
"exif_bottom_sheet_details": "DETAILS",
|
||||||
"exif_bottom_sheet_location": "LOCATION",
|
"exif_bottom_sheet_location": "LOCATION",
|
||||||
"exif_bottom_sheet_location_add": "Add a location",
|
"exif_bottom_sheet_location_add": "Add a location",
|
||||||
"exif_bottom_sheet_people": "PEOPLE",
|
"exif_bottom_sheet_people": "PEOPLE",
|
||||||
"exif_bottom_sheet_person_add_person": "Add name",
|
"exif_bottom_sheet_person_add_person": "Add name",
|
||||||
"experimental_settings_new_asset_list_subtitle": "Work in progress",
|
|
||||||
"experimental_settings_new_asset_list_title": "Enable experimental photo grid",
|
|
||||||
"experimental_settings_subtitle": "Use at your own risk!",
|
|
||||||
"experimental_settings_title": "Experimental",
|
|
||||||
"favorites_page_no_favorites": "No favorite assets found",
|
|
||||||
"favorites_page_title": "Favorites",
|
"favorites_page_title": "Favorites",
|
||||||
"filename_search": "File name or extension",
|
"filename_search": "File name or extension",
|
||||||
"haptic_feedback_switch": "Enable haptic feedback",
|
"haptic_feedback_switch": "Enable haptic feedback",
|
||||||
@ -246,7 +222,6 @@
|
|||||||
"home_page_add_to_album_conflicts": "Added {added} assets to album {album}. {failed} assets are already in the album.",
|
"home_page_add_to_album_conflicts": "Added {added} assets to album {album}. {failed} assets are already in the album.",
|
||||||
"home_page_add_to_album_err_local": "Can not add local assets to albums yet, skipping",
|
"home_page_add_to_album_err_local": "Can not add local assets to albums yet, skipping",
|
||||||
"home_page_add_to_album_success": "Added {added} assets to album {album}.",
|
"home_page_add_to_album_success": "Added {added} assets to album {album}.",
|
||||||
"home_page_album_err_partner": "Can not add partner assets to an album yet, skipping",
|
|
||||||
"home_page_archive_err_local": "Can not archive local assets yet, skipping",
|
"home_page_archive_err_local": "Can not archive local assets yet, skipping",
|
||||||
"home_page_archive_err_partner": "Can not archive partner assets, skipping",
|
"home_page_archive_err_partner": "Can not archive partner assets, skipping",
|
||||||
"home_page_building_timeline": "Building the timeline",
|
"home_page_building_timeline": "Building the timeline",
|
||||||
@ -256,20 +231,9 @@
|
|||||||
"home_page_favorite_err_partner": "Can not favorite partner assets yet, skipping",
|
"home_page_favorite_err_partner": "Can not favorite partner assets yet, skipping",
|
||||||
"home_page_first_time_notice": "If this is your first time using the app, please make sure to choose a backup album(s) so that the timeline can populate photos and videos in the album(s).",
|
"home_page_first_time_notice": "If this is your first time using the app, please make sure to choose a backup album(s) so that the timeline can populate photos and videos in the album(s).",
|
||||||
"home_page_share_err_local": "Can not share local assets via link, skipping",
|
"home_page_share_err_local": "Can not share local assets via link, skipping",
|
||||||
"home_page_upload_err_limit": "Can only upload a maximum of 30 assets at a time, skipping",
|
|
||||||
"image_saved_successfully": "Image saved",
|
|
||||||
"image_viewer_page_state_provider_download_error": "Download Error",
|
|
||||||
"image_viewer_page_state_provider_download_started": "Download Started",
|
|
||||||
"image_viewer_page_state_provider_download_success": "Download Success",
|
|
||||||
"image_viewer_page_state_provider_share_error": "Share Error",
|
"image_viewer_page_state_provider_share_error": "Share Error",
|
||||||
"invalid_date": "Invalid date",
|
"invalid_date": "Invalid date",
|
||||||
"invalid_date_format": "Invalid date format",
|
"invalid_date_format": "Invalid date format",
|
||||||
"library_page_albums": "Albums",
|
|
||||||
"library_page_archive": "Archive",
|
|
||||||
"library_page_device_albums": "Albums on Device",
|
|
||||||
"library_page_favorites": "Favorites",
|
|
||||||
"library_page_new_album": "New album",
|
|
||||||
"library_page_sharing": "Sharing",
|
|
||||||
"library_page_sort_asset_count": "Number of assets",
|
"library_page_sort_asset_count": "Number of assets",
|
||||||
"library_page_sort_created": "Created date",
|
"library_page_sort_created": "Created date",
|
||||||
"library_page_sort_last_modified": "Last modified",
|
"library_page_sort_last_modified": "Last modified",
|
||||||
@ -290,7 +254,6 @@
|
|||||||
"login_form_email_hint": "youremail@email.com",
|
"login_form_email_hint": "youremail@email.com",
|
||||||
"login_form_endpoint_hint": "http://your-server-ip:port/api",
|
"login_form_endpoint_hint": "http://your-server-ip:port/api",
|
||||||
"login_form_endpoint_url": "Server Endpoint URL",
|
"login_form_endpoint_url": "Server Endpoint URL",
|
||||||
"login_form_err_http": "Please specify http:// or https://",
|
|
||||||
"login_form_err_invalid_email": "Invalid Email",
|
"login_form_err_invalid_email": "Invalid Email",
|
||||||
"login_form_err_invalid_url": "Invalid URL",
|
"login_form_err_invalid_url": "Invalid URL",
|
||||||
"login_form_err_leading_whitespace": "Leading whitespace",
|
"login_form_err_leading_whitespace": "Leading whitespace",
|
||||||
@ -303,7 +266,6 @@
|
|||||||
"login_form_label_password": "Password",
|
"login_form_label_password": "Password",
|
||||||
"login_form_next_button": "Next",
|
"login_form_next_button": "Next",
|
||||||
"login_form_password_hint": "password",
|
"login_form_password_hint": "password",
|
||||||
"login_form_save_login": "Stay logged in",
|
|
||||||
"login_form_server_empty": "Enter a server URL.",
|
"login_form_server_empty": "Enter a server URL.",
|
||||||
"login_form_server_error": "Could not connect to server.",
|
"login_form_server_error": "Could not connect to server.",
|
||||||
"login_password_changed_error": "There was an error updating your password",
|
"login_password_changed_error": "There was an error updating your password",
|
||||||
@ -319,15 +281,11 @@
|
|||||||
"map_no_assets_in_bounds": "No photos in this area",
|
"map_no_assets_in_bounds": "No photos in this area",
|
||||||
"map_no_location_permission_content": "Location permission is needed to display assets from your current location. Do you want to allow it now?",
|
"map_no_location_permission_content": "Location permission is needed to display assets from your current location. Do you want to allow it now?",
|
||||||
"map_no_location_permission_title": "Location Permission denied",
|
"map_no_location_permission_title": "Location Permission denied",
|
||||||
"map_settings_dark_mode": "Dark mode",
|
|
||||||
"map_settings_date_range_option_all": "All",
|
"map_settings_date_range_option_all": "All",
|
||||||
"map_settings_date_range_option_day": "Past 24 hours",
|
"map_settings_date_range_option_day": "Past 24 hours",
|
||||||
"map_settings_date_range_option_days": "Past {} days",
|
"map_settings_date_range_option_days": "Past {} days",
|
||||||
"map_settings_date_range_option_year": "Past year",
|
"map_settings_date_range_option_year": "Past year",
|
||||||
"map_settings_date_range_option_years": "Past {} years",
|
"map_settings_date_range_option_years": "Past {} years",
|
||||||
"map_settings_dialog_cancel": "Cancel",
|
|
||||||
"map_settings_dialog_save": "Save",
|
|
||||||
"map_settings_dialog_title": "Map Settings",
|
|
||||||
"map_settings_include_show_archived": "Include Archived",
|
"map_settings_include_show_archived": "Include Archived",
|
||||||
"map_settings_include_show_partners": "Include Partners",
|
"map_settings_include_show_partners": "Include Partners",
|
||||||
"map_settings_only_relative_range": "Date range",
|
"map_settings_only_relative_range": "Date range",
|
||||||
@ -340,10 +298,7 @@
|
|||||||
"memories_swipe_to_close": "Swipe up to close",
|
"memories_swipe_to_close": "Swipe up to close",
|
||||||
"memories_year_ago": "A year ago",
|
"memories_year_ago": "A year ago",
|
||||||
"memories_years_ago": "{} years ago",
|
"memories_years_ago": "{} years ago",
|
||||||
"monthly_title_text_date_format": "MMMM y",
|
|
||||||
"motion_photos_page_title": "Motion Photos",
|
"motion_photos_page_title": "Motion Photos",
|
||||||
"multiselect_grid_edit_date_time_err_read_only": "Cannot edit date of read only asset(s), skipping",
|
|
||||||
"multiselect_grid_edit_gps_err_read_only": "Cannot edit location of read only asset(s), skipping",
|
|
||||||
"no_assets_to_show": "No assets to show",
|
"no_assets_to_show": "No assets to show",
|
||||||
"no_name": "No name",
|
"no_name": "No name",
|
||||||
"notification_permission_dialog_cancel": "Cancel",
|
"notification_permission_dialog_cancel": "Cancel",
|
||||||
@ -353,7 +308,6 @@
|
|||||||
"notification_permission_list_tile_enable_button": "Enable Notifications",
|
"notification_permission_list_tile_enable_button": "Enable Notifications",
|
||||||
"notification_permission_list_tile_title": "Notification Permission",
|
"notification_permission_list_tile_title": "Notification Permission",
|
||||||
"partner_list_user_photos": "{user}'s photos",
|
"partner_list_user_photos": "{user}'s photos",
|
||||||
"partner_list_view_all": "View all",
|
|
||||||
"partner_page_add_partner": "Add partner",
|
"partner_page_add_partner": "Add partner",
|
||||||
"partner_page_empty_message": "Your photos are not yet shared with any partner.",
|
"partner_page_empty_message": "Your photos are not yet shared with any partner.",
|
||||||
"partner_page_no_more_users": "No more users to add",
|
"partner_page_no_more_users": "No more users to add",
|
||||||
@ -362,13 +316,11 @@
|
|||||||
"partner_page_shared_to_title": "Shared to",
|
"partner_page_shared_to_title": "Shared to",
|
||||||
"partner_page_stop_sharing_content": "{} will no longer be able to access your photos.",
|
"partner_page_stop_sharing_content": "{} will no longer be able to access your photos.",
|
||||||
"partner_page_stop_sharing_title": "Stop sharing your photos?",
|
"partner_page_stop_sharing_title": "Stop sharing your photos?",
|
||||||
"partner_page_title": "Partner",
|
|
||||||
"permission_onboarding_back": "Back",
|
"permission_onboarding_back": "Back",
|
||||||
"permission_onboarding_continue_anyway": "Continue anyway",
|
"permission_onboarding_continue_anyway": "Continue anyway",
|
||||||
"permission_onboarding_get_started": "Get started",
|
"permission_onboarding_get_started": "Get started",
|
||||||
"permission_onboarding_go_to_settings": "Go to settings",
|
"permission_onboarding_go_to_settings": "Go to settings",
|
||||||
"permission_onboarding_grant_permission": "Grant permission",
|
"permission_onboarding_grant_permission": "Grant permission",
|
||||||
"permission_onboarding_log_out": "Log out",
|
|
||||||
"permission_onboarding_permission_denied": "Permission denied. To use Immich, grant photo and video permissions in Settings.",
|
"permission_onboarding_permission_denied": "Permission denied. To use Immich, grant photo and video permissions in Settings.",
|
||||||
"permission_onboarding_permission_granted": "Permission granted! You are all set.",
|
"permission_onboarding_permission_granted": "Permission granted! You are all set.",
|
||||||
"permission_onboarding_permission_limited": "Permission limited. To let Immich backup and manage your entire gallery collection, grant photo and video permissions in Settings.",
|
"permission_onboarding_permission_limited": "Permission limited. To let Immich backup and manage your entire gallery collection, grant photo and video permissions in Settings.",
|
||||||
@ -384,7 +336,6 @@
|
|||||||
"profile_drawer_server_out_of_date_minor": "Server is out of date. Please update to the latest minor version.",
|
"profile_drawer_server_out_of_date_minor": "Server is out of date. Please update to the latest minor version.",
|
||||||
"profile_drawer_settings": "Settings",
|
"profile_drawer_settings": "Settings",
|
||||||
"profile_drawer_sign_out": "Sign Out",
|
"profile_drawer_sign_out": "Sign Out",
|
||||||
"profile_drawer_trash": "Trash",
|
|
||||||
"recently_added_page_title": "Recently Added",
|
"recently_added_page_title": "Recently Added",
|
||||||
"save_to_gallery": "Save to gallery",
|
"save_to_gallery": "Save to gallery",
|
||||||
"scaffold_body_error_occurred": "Error occurred",
|
"scaffold_body_error_occurred": "Error occurred",
|
||||||
@ -417,9 +368,6 @@
|
|||||||
"search_page_categories": "Categories",
|
"search_page_categories": "Categories",
|
||||||
"search_page_favorites": "Favorites",
|
"search_page_favorites": "Favorites",
|
||||||
"search_page_motion_photos": "Motion Photos",
|
"search_page_motion_photos": "Motion Photos",
|
||||||
"search_page_no_objects": "No Objects Info Available",
|
|
||||||
"search_page_no_places": "No Places Info Available",
|
|
||||||
"search_page_people": "People",
|
|
||||||
"search_page_person_add_name_dialog_cancel": "Cancel",
|
"search_page_person_add_name_dialog_cancel": "Cancel",
|
||||||
"search_page_person_add_name_dialog_hint": "Name",
|
"search_page_person_add_name_dialog_hint": "Name",
|
||||||
"search_page_person_add_name_dialog_save": "Save",
|
"search_page_person_add_name_dialog_save": "Save",
|
||||||
@ -427,18 +375,10 @@
|
|||||||
"search_page_person_add_name_subtitle": "Find them fast by name with search",
|
"search_page_person_add_name_subtitle": "Find them fast by name with search",
|
||||||
"search_page_person_add_name_title": "Add a name",
|
"search_page_person_add_name_title": "Add a name",
|
||||||
"search_page_person_edit_name": "Edit name",
|
"search_page_person_edit_name": "Edit name",
|
||||||
"search_page_places": "Places",
|
|
||||||
"search_page_recently_added": "Recently added",
|
"search_page_recently_added": "Recently added",
|
||||||
"search_page_screenshots": "Screenshots",
|
|
||||||
"search_page_selfies": "Selfies",
|
|
||||||
"search_page_things": "Things",
|
|
||||||
"search_page_videos": "Videos",
|
"search_page_videos": "Videos",
|
||||||
"search_page_view_all_button": "View all",
|
"search_page_view_all_button": "View all",
|
||||||
"search_page_your_activity": "Your activity",
|
|
||||||
"search_page_your_map": "Your Map",
|
"search_page_your_map": "Your Map",
|
||||||
"search_result_page_new_search_hint": "New Search",
|
|
||||||
"search_suggestion_list_smart_search_hint_1": "Smart search is enabled by default, to search for metadata use the syntax ",
|
|
||||||
"search_suggestion_list_smart_search_hint_2": "m:your-search-term",
|
|
||||||
"select_additional_user_for_sharing_page_suggestions": "Suggestions",
|
"select_additional_user_for_sharing_page_suggestions": "Suggestions",
|
||||||
"select_user_for_sharing_page_err_album": "Failed to create album",
|
"select_user_for_sharing_page_err_album": "Failed to create album",
|
||||||
"select_user_for_sharing_page_share_suggestions": "Suggestions",
|
"select_user_for_sharing_page_share_suggestions": "Suggestions",
|
||||||
@ -462,12 +402,10 @@
|
|||||||
"setting_notifications_notify_seconds": "{} seconds",
|
"setting_notifications_notify_seconds": "{} seconds",
|
||||||
"setting_notifications_single_progress_subtitle": "Detailed upload progress information per asset",
|
"setting_notifications_single_progress_subtitle": "Detailed upload progress information per asset",
|
||||||
"setting_notifications_single_progress_title": "Show background backup detail progress",
|
"setting_notifications_single_progress_title": "Show background backup detail progress",
|
||||||
"setting_notifications_subtitle": "Adjust your notification preferences",
|
|
||||||
"setting_notifications_title": "Notifications",
|
"setting_notifications_title": "Notifications",
|
||||||
"setting_notifications_total_progress_subtitle": "Overall upload progress (done/total assets)",
|
"setting_notifications_total_progress_subtitle": "Overall upload progress (done/total assets)",
|
||||||
"setting_notifications_total_progress_title": "Show background backup total progress",
|
"setting_notifications_total_progress_title": "Show background backup total progress",
|
||||||
"setting_pages_app_bar_settings": "Settings",
|
"setting_pages_app_bar_settings": "Settings",
|
||||||
"settings_require_restart": "Please restart Immich to apply this setting",
|
|
||||||
"setting_video_viewer_looping_subtitle": "Enable to automatically loop a video in the detail viewer.",
|
"setting_video_viewer_looping_subtitle": "Enable to automatically loop a video in the detail viewer.",
|
||||||
"setting_video_viewer_looping_title": "Looping",
|
"setting_video_viewer_looping_title": "Looping",
|
||||||
"setting_video_viewer_title": "Videos",
|
"setting_video_viewer_title": "Videos",
|
||||||
@ -490,7 +428,6 @@
|
|||||||
"share_dialog_preparing": "Preparing...",
|
"share_dialog_preparing": "Preparing...",
|
||||||
"shared_link_app_bar_title": "Shared Links",
|
"shared_link_app_bar_title": "Shared Links",
|
||||||
"shared_link_clipboard_copied_massage": "Copied to clipboard",
|
"shared_link_clipboard_copied_massage": "Copied to clipboard",
|
||||||
"shared_link_clipboard_text": "Link: {}\nPassword: {}",
|
|
||||||
"shared_link_create_app_bar_title": "Create link to share",
|
"shared_link_create_app_bar_title": "Create link to share",
|
||||||
"shared_link_create_error": "Error while creating shared link",
|
"shared_link_create_error": "Error while creating shared link",
|
||||||
"shared_link_create_info": "Let anyone with the link see the selected photo(s)",
|
"shared_link_create_info": "Let anyone with the link see the selected photo(s)",
|
||||||
@ -524,7 +461,7 @@
|
|||||||
"shared_link_expires_hours": "Expires in {} hours",
|
"shared_link_expires_hours": "Expires in {} hours",
|
||||||
"shared_link_expires_minute": "Expires in {} minute",
|
"shared_link_expires_minute": "Expires in {} minute",
|
||||||
"shared_link_expires_minutes": "Expires in {} minutes",
|
"shared_link_expires_minutes": "Expires in {} minutes",
|
||||||
"shared_link_expires_never": "Expires ∞",
|
"shared_link_expires_never": "Expires \u221e",
|
||||||
"shared_link_expires_second": "Expires in {} second",
|
"shared_link_expires_second": "Expires in {} second",
|
||||||
"shared_link_expires_seconds": "Expires in {} seconds",
|
"shared_link_expires_seconds": "Expires in {} seconds",
|
||||||
"shared_link_individual_shared": "Individual shared",
|
"shared_link_individual_shared": "Individual shared",
|
||||||
@ -535,35 +472,22 @@
|
|||||||
"shared_link_public_album": "Public album",
|
"shared_link_public_album": "Public album",
|
||||||
"share_done": "Done",
|
"share_done": "Done",
|
||||||
"share_invite": "Invite to album",
|
"share_invite": "Invite to album",
|
||||||
"sharing_page_album": "Shared albums",
|
|
||||||
"sharing_page_description": "Create shared albums to share photos and videos with people in your network.",
|
|
||||||
"sharing_page_empty_list": "EMPTY LIST",
|
|
||||||
"sharing_silver_appbar_create_shared_album": "New shared album",
|
|
||||||
"sharing_silver_appbar_shared_links": "Shared links",
|
|
||||||
"sharing_silver_appbar_share_partner": "Share with partner",
|
|
||||||
"sync": "Sync",
|
"sync": "Sync",
|
||||||
"sync_albums": "Sync albums",
|
"sync_albums": "Sync albums",
|
||||||
"sync_albums_manual_subtitle": "Sync all uploaded videos and photos to the selected backup albums",
|
"sync_albums_manual_subtitle": "Sync all uploaded videos and photos to the selected backup albums",
|
||||||
"sync_upload_album_setting_subtitle": "Create and upload your photos and videos to the selected albums on Immich",
|
"sync_upload_album_setting_subtitle": "Create and upload your photos and videos to the selected albums on Immich",
|
||||||
"tab_controller_nav_library": "Library",
|
|
||||||
"tab_controller_nav_photos": "Photos",
|
"tab_controller_nav_photos": "Photos",
|
||||||
"tab_controller_nav_search": "Search",
|
"tab_controller_nav_search": "Search",
|
||||||
"tab_controller_nav_sharing": "Sharing",
|
|
||||||
"theme_setting_asset_list_storage_indicator_title": "Show storage indicator on asset tiles",
|
"theme_setting_asset_list_storage_indicator_title": "Show storage indicator on asset tiles",
|
||||||
"theme_setting_asset_list_tiles_per_row_title": "Number of assets per row ({})",
|
"theme_setting_asset_list_tiles_per_row_title": "Number of assets per row ({})",
|
||||||
"theme_setting_colorful_interface_subtitle": "Apply primary color to background surfaces.",
|
"theme_setting_colorful_interface_subtitle": "Apply primary color to background surfaces.",
|
||||||
"theme_setting_colorful_interface_title": "Colorful interface",
|
"theme_setting_colorful_interface_title": "Colorful interface",
|
||||||
"theme_setting_dark_mode_switch": "Dark mode",
|
"theme_setting_dark_mode_switch": "Dark mode",
|
||||||
"theme_setting_image_viewer_quality_subtitle": "Adjust the quality of the detail image viewer",
|
|
||||||
"theme_setting_image_viewer_quality_title": "Image viewer quality",
|
|
||||||
"theme_setting_primary_color_subtitle": "Pick a color for primary actions and accents.",
|
"theme_setting_primary_color_subtitle": "Pick a color for primary actions and accents.",
|
||||||
"theme_setting_primary_color_title": "Primary color",
|
"theme_setting_primary_color_title": "Primary color",
|
||||||
"theme_setting_system_primary_color_title": "Use system color",
|
"theme_setting_system_primary_color_title": "Use system color",
|
||||||
"theme_setting_system_theme_switch": "Automatic (Follow system setting)",
|
"theme_setting_system_theme_switch": "Automatic (Follow system setting)",
|
||||||
"theme_setting_theme_subtitle": "Choose the app's theme setting",
|
|
||||||
"theme_setting_theme_title": "Theme",
|
"theme_setting_theme_title": "Theme",
|
||||||
"theme_setting_three_stage_loading_subtitle": "Three-stage loading might increase the loading performance but causes significantly higher network load",
|
|
||||||
"theme_setting_three_stage_loading_title": "Enable three-stage loading",
|
|
||||||
"translated_text_options": "Options",
|
"translated_text_options": "Options",
|
||||||
"trash_emptied": "Emptied trash",
|
"trash_emptied": "Emptied trash",
|
||||||
"trash_page_delete": "Delete",
|
"trash_page_delete": "Delete",
|
||||||
@ -582,19 +506,10 @@
|
|||||||
"upload_dialog_info": "Do you want to backup the selected Asset(s) to the server?",
|
"upload_dialog_info": "Do you want to backup the selected Asset(s) to the server?",
|
||||||
"upload_dialog_ok": "Upload",
|
"upload_dialog_ok": "Upload",
|
||||||
"upload_dialog_title": "Upload Asset",
|
"upload_dialog_title": "Upload Asset",
|
||||||
"version_announcement_overlay_ack": "Acknowledge",
|
|
||||||
"version_announcement_overlay_release_notes": "release notes",
|
|
||||||
"version_announcement_overlay_text_1": "Hi friend, there is a new release of",
|
|
||||||
"version_announcement_overlay_text_2": "please take your time to visit the ",
|
|
||||||
"version_announcement_overlay_text_3": " and ensure your docker-compose and .env setup is up-to-date to prevent any misconfigurations, especially if you use WatchTower or any mechanism that handles updating your server application automatically.",
|
|
||||||
"version_announcement_overlay_title": "New Server Version Available \uD83C\uDF89",
|
|
||||||
"viewer_remove_from_stack": "Remove from Stack",
|
|
||||||
"viewer_stack_use_as_main_asset": "Use as Main Asset",
|
|
||||||
"viewer_unstack": "Un-Stack",
|
"viewer_unstack": "Un-Stack",
|
||||||
"filter": "Filter",
|
"filter": "Filter",
|
||||||
"downloading_media": "Downloading media",
|
"downloading_media": "Downloading media",
|
||||||
"download_finished": "Download finished",
|
"download_finished": "Download finished",
|
||||||
"download_filename": "file: {}",
|
|
||||||
"downloading": "Downloading...",
|
"downloading": "Downloading...",
|
||||||
"download_complete": "Download complete",
|
"download_complete": "Download complete",
|
||||||
"download_failed": "Download failed",
|
"download_failed": "Download failed",
|
||||||
|
@ -3,7 +3,7 @@ PODS:
|
|||||||
- Flutter
|
- Flutter
|
||||||
- connectivity_plus (0.0.1):
|
- connectivity_plus (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- ReachabilitySwift
|
- FlutterMacOS
|
||||||
- device_info_plus (0.0.1):
|
- device_info_plus (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- DKImagePickerController/Core (4.3.9):
|
- DKImagePickerController/Core (4.3.9):
|
||||||
@ -77,7 +77,6 @@ PODS:
|
|||||||
- photo_manager (2.0.0):
|
- photo_manager (2.0.0):
|
||||||
- Flutter
|
- Flutter
|
||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
- ReachabilitySwift (5.0.0)
|
|
||||||
- SAMKeychain (1.5.3)
|
- SAMKeychain (1.5.3)
|
||||||
- SDWebImage (5.19.4):
|
- SDWebImage (5.19.4):
|
||||||
- SDWebImage/Core (= 5.19.4)
|
- SDWebImage/Core (= 5.19.4)
|
||||||
@ -102,7 +101,7 @@ PODS:
|
|||||||
|
|
||||||
DEPENDENCIES:
|
DEPENDENCIES:
|
||||||
- background_downloader (from `.symlinks/plugins/background_downloader/ios`)
|
- background_downloader (from `.symlinks/plugins/background_downloader/ios`)
|
||||||
- connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`)
|
- connectivity_plus (from `.symlinks/plugins/connectivity_plus/darwin`)
|
||||||
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
|
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
|
||||||
- file_picker (from `.symlinks/plugins/file_picker/ios`)
|
- file_picker (from `.symlinks/plugins/file_picker/ios`)
|
||||||
- Flutter (from `Flutter`)
|
- Flutter (from `Flutter`)
|
||||||
@ -133,7 +132,6 @@ SPEC REPOS:
|
|||||||
- DKImagePickerController
|
- DKImagePickerController
|
||||||
- DKPhotoGallery
|
- DKPhotoGallery
|
||||||
- MapLibre
|
- MapLibre
|
||||||
- ReachabilitySwift
|
|
||||||
- SAMKeychain
|
- SAMKeychain
|
||||||
- SDWebImage
|
- SDWebImage
|
||||||
- SwiftyGif
|
- SwiftyGif
|
||||||
@ -143,7 +141,7 @@ EXTERNAL SOURCES:
|
|||||||
background_downloader:
|
background_downloader:
|
||||||
:path: ".symlinks/plugins/background_downloader/ios"
|
:path: ".symlinks/plugins/background_downloader/ios"
|
||||||
connectivity_plus:
|
connectivity_plus:
|
||||||
:path: ".symlinks/plugins/connectivity_plus/ios"
|
:path: ".symlinks/plugins/connectivity_plus/darwin"
|
||||||
device_info_plus:
|
device_info_plus:
|
||||||
:path: ".symlinks/plugins/device_info_plus/ios"
|
:path: ".symlinks/plugins/device_info_plus/ios"
|
||||||
file_picker:
|
file_picker:
|
||||||
@ -195,8 +193,8 @@ EXTERNAL SOURCES:
|
|||||||
|
|
||||||
SPEC CHECKSUMS:
|
SPEC CHECKSUMS:
|
||||||
background_downloader: 9f788ffc5de45acf87d6380e91ca0841066c18cf
|
background_downloader: 9f788ffc5de45acf87d6380e91ca0841066c18cf
|
||||||
connectivity_plus: bf0076dd84a130856aa636df1c71ccaff908fa1d
|
connectivity_plus: ddd7f30999e1faaef5967c23d5b6d503d10434db
|
||||||
device_info_plus: c6fb39579d0f423935b0c9ce7ee2f44b71b9fce6
|
device_info_plus: 97af1d7e84681a90d0693e63169a5d50e0839a0d
|
||||||
DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c
|
DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c
|
||||||
DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60
|
DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60
|
||||||
file_picker: 09aa5ec1ab24135ccd7a1621c46c84134bfd6655
|
file_picker: 09aa5ec1ab24135ccd7a1621c46c84134bfd6655
|
||||||
@ -217,7 +215,6 @@ SPEC CHECKSUMS:
|
|||||||
path_provider_ios: 14f3d2fd28c4fdb42f44e0f751d12861c43cee02
|
path_provider_ios: 14f3d2fd28c4fdb42f44e0f751d12861c43cee02
|
||||||
permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2
|
permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2
|
||||||
photo_manager: ff695c7a1dd5bc379974953a2b5c0a293f7c4c8a
|
photo_manager: ff695c7a1dd5bc379974953a2b5c0a293f7c4c8a
|
||||||
ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825
|
|
||||||
SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c
|
SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c
|
||||||
SDWebImage: 066c47b573f408f18caa467d71deace7c0f8280d
|
SDWebImage: 066c47b573f408f18caa467d71deace7c0f8280d
|
||||||
share_plus: 8875f4f2500512ea181eef553c3e27dba5135aad
|
share_plus: 8875f4f2500512ea181eef553c3e27dba5135aad
|
||||||
|
@ -24,7 +24,10 @@ final Map<ImmichColorPreset, ImmichTheme> _themePresetsMap = {
|
|||||||
ImmichColorPreset.indigo: ImmichTheme(
|
ImmichColorPreset.indigo: ImmichTheme(
|
||||||
light: ColorScheme.fromSeed(
|
light: ColorScheme.fromSeed(
|
||||||
seedColor: immichBrandColorLight,
|
seedColor: immichBrandColorLight,
|
||||||
).copyWith(primary: immichBrandColorLight),
|
).copyWith(
|
||||||
|
primary: immichBrandColorLight,
|
||||||
|
onSurface: const Color.fromARGB(255, 34, 31, 32),
|
||||||
|
),
|
||||||
dark: ColorScheme.fromSeed(
|
dark: ColorScheme.fromSeed(
|
||||||
seedColor: immichBrandColorDark,
|
seedColor: immichBrandColorDark,
|
||||||
brightness: Brightness.dark,
|
brightness: Brightness.dark,
|
||||||
|
141
mobile/lib/entities/asset.entity.g.dart
generated
141
mobile/lib/entities/asset.entity.g.dart
generated
@ -57,64 +57,69 @@ const AssetSchema = CollectionSchema(
|
|||||||
name: r'isFavorite',
|
name: r'isFavorite',
|
||||||
type: IsarType.bool,
|
type: IsarType.bool,
|
||||||
),
|
),
|
||||||
r'isTrashed': PropertySchema(
|
r'isOffline': PropertySchema(
|
||||||
id: 8,
|
id: 8,
|
||||||
|
name: r'isOffline',
|
||||||
|
type: IsarType.bool,
|
||||||
|
),
|
||||||
|
r'isTrashed': PropertySchema(
|
||||||
|
id: 9,
|
||||||
name: r'isTrashed',
|
name: r'isTrashed',
|
||||||
type: IsarType.bool,
|
type: IsarType.bool,
|
||||||
),
|
),
|
||||||
r'livePhotoVideoId': PropertySchema(
|
r'livePhotoVideoId': PropertySchema(
|
||||||
id: 9,
|
id: 10,
|
||||||
name: r'livePhotoVideoId',
|
name: r'livePhotoVideoId',
|
||||||
type: IsarType.string,
|
type: IsarType.string,
|
||||||
),
|
),
|
||||||
r'localId': PropertySchema(
|
r'localId': PropertySchema(
|
||||||
id: 10,
|
id: 11,
|
||||||
name: r'localId',
|
name: r'localId',
|
||||||
type: IsarType.string,
|
type: IsarType.string,
|
||||||
),
|
),
|
||||||
r'ownerId': PropertySchema(
|
r'ownerId': PropertySchema(
|
||||||
id: 11,
|
id: 12,
|
||||||
name: r'ownerId',
|
name: r'ownerId',
|
||||||
type: IsarType.long,
|
type: IsarType.long,
|
||||||
),
|
),
|
||||||
r'remoteId': PropertySchema(
|
r'remoteId': PropertySchema(
|
||||||
id: 12,
|
id: 13,
|
||||||
name: r'remoteId',
|
name: r'remoteId',
|
||||||
type: IsarType.string,
|
type: IsarType.string,
|
||||||
),
|
),
|
||||||
r'stackCount': PropertySchema(
|
r'stackCount': PropertySchema(
|
||||||
id: 13,
|
id: 14,
|
||||||
name: r'stackCount',
|
name: r'stackCount',
|
||||||
type: IsarType.long,
|
type: IsarType.long,
|
||||||
),
|
),
|
||||||
r'stackId': PropertySchema(
|
r'stackId': PropertySchema(
|
||||||
id: 14,
|
id: 15,
|
||||||
name: r'stackId',
|
name: r'stackId',
|
||||||
type: IsarType.string,
|
type: IsarType.string,
|
||||||
),
|
),
|
||||||
r'stackPrimaryAssetId': PropertySchema(
|
r'stackPrimaryAssetId': PropertySchema(
|
||||||
id: 15,
|
id: 16,
|
||||||
name: r'stackPrimaryAssetId',
|
name: r'stackPrimaryAssetId',
|
||||||
type: IsarType.string,
|
type: IsarType.string,
|
||||||
),
|
),
|
||||||
r'thumbhash': PropertySchema(
|
r'thumbhash': PropertySchema(
|
||||||
id: 16,
|
id: 17,
|
||||||
name: r'thumbhash',
|
name: r'thumbhash',
|
||||||
type: IsarType.string,
|
type: IsarType.string,
|
||||||
),
|
),
|
||||||
r'type': PropertySchema(
|
r'type': PropertySchema(
|
||||||
id: 17,
|
id: 18,
|
||||||
name: r'type',
|
name: r'type',
|
||||||
type: IsarType.byte,
|
type: IsarType.byte,
|
||||||
enumMap: _AssettypeEnumValueMap,
|
enumMap: _AssettypeEnumValueMap,
|
||||||
),
|
),
|
||||||
r'updatedAt': PropertySchema(
|
r'updatedAt': PropertySchema(
|
||||||
id: 18,
|
id: 19,
|
||||||
name: r'updatedAt',
|
name: r'updatedAt',
|
||||||
type: IsarType.dateTime,
|
type: IsarType.dateTime,
|
||||||
),
|
),
|
||||||
r'width': PropertySchema(
|
r'width': PropertySchema(
|
||||||
id: 19,
|
id: 20,
|
||||||
name: r'width',
|
name: r'width',
|
||||||
type: IsarType.int,
|
type: IsarType.int,
|
||||||
)
|
)
|
||||||
@ -239,18 +244,19 @@ void _assetSerialize(
|
|||||||
writer.writeInt(offsets[5], object.height);
|
writer.writeInt(offsets[5], object.height);
|
||||||
writer.writeBool(offsets[6], object.isArchived);
|
writer.writeBool(offsets[6], object.isArchived);
|
||||||
writer.writeBool(offsets[7], object.isFavorite);
|
writer.writeBool(offsets[7], object.isFavorite);
|
||||||
writer.writeBool(offsets[8], object.isTrashed);
|
writer.writeBool(offsets[8], object.isOffline);
|
||||||
writer.writeString(offsets[9], object.livePhotoVideoId);
|
writer.writeBool(offsets[9], object.isTrashed);
|
||||||
writer.writeString(offsets[10], object.localId);
|
writer.writeString(offsets[10], object.livePhotoVideoId);
|
||||||
writer.writeLong(offsets[11], object.ownerId);
|
writer.writeString(offsets[11], object.localId);
|
||||||
writer.writeString(offsets[12], object.remoteId);
|
writer.writeLong(offsets[12], object.ownerId);
|
||||||
writer.writeLong(offsets[13], object.stackCount);
|
writer.writeString(offsets[13], object.remoteId);
|
||||||
writer.writeString(offsets[14], object.stackId);
|
writer.writeLong(offsets[14], object.stackCount);
|
||||||
writer.writeString(offsets[15], object.stackPrimaryAssetId);
|
writer.writeString(offsets[15], object.stackId);
|
||||||
writer.writeString(offsets[16], object.thumbhash);
|
writer.writeString(offsets[16], object.stackPrimaryAssetId);
|
||||||
writer.writeByte(offsets[17], object.type.index);
|
writer.writeString(offsets[17], object.thumbhash);
|
||||||
writer.writeDateTime(offsets[18], object.updatedAt);
|
writer.writeByte(offsets[18], object.type.index);
|
||||||
writer.writeInt(offsets[19], object.width);
|
writer.writeDateTime(offsets[19], object.updatedAt);
|
||||||
|
writer.writeInt(offsets[20], object.width);
|
||||||
}
|
}
|
||||||
|
|
||||||
Asset _assetDeserialize(
|
Asset _assetDeserialize(
|
||||||
@ -269,19 +275,20 @@ Asset _assetDeserialize(
|
|||||||
id: id,
|
id: id,
|
||||||
isArchived: reader.readBoolOrNull(offsets[6]) ?? false,
|
isArchived: reader.readBoolOrNull(offsets[6]) ?? false,
|
||||||
isFavorite: reader.readBoolOrNull(offsets[7]) ?? false,
|
isFavorite: reader.readBoolOrNull(offsets[7]) ?? false,
|
||||||
isTrashed: reader.readBoolOrNull(offsets[8]) ?? false,
|
isOffline: reader.readBoolOrNull(offsets[8]) ?? false,
|
||||||
livePhotoVideoId: reader.readStringOrNull(offsets[9]),
|
isTrashed: reader.readBoolOrNull(offsets[9]) ?? false,
|
||||||
localId: reader.readStringOrNull(offsets[10]),
|
livePhotoVideoId: reader.readStringOrNull(offsets[10]),
|
||||||
ownerId: reader.readLong(offsets[11]),
|
localId: reader.readStringOrNull(offsets[11]),
|
||||||
remoteId: reader.readStringOrNull(offsets[12]),
|
ownerId: reader.readLong(offsets[12]),
|
||||||
stackCount: reader.readLongOrNull(offsets[13]) ?? 0,
|
remoteId: reader.readStringOrNull(offsets[13]),
|
||||||
stackId: reader.readStringOrNull(offsets[14]),
|
stackCount: reader.readLongOrNull(offsets[14]) ?? 0,
|
||||||
stackPrimaryAssetId: reader.readStringOrNull(offsets[15]),
|
stackId: reader.readStringOrNull(offsets[15]),
|
||||||
thumbhash: reader.readStringOrNull(offsets[16]),
|
stackPrimaryAssetId: reader.readStringOrNull(offsets[16]),
|
||||||
type: _AssettypeValueEnumMap[reader.readByteOrNull(offsets[17])] ??
|
thumbhash: reader.readStringOrNull(offsets[17]),
|
||||||
|
type: _AssettypeValueEnumMap[reader.readByteOrNull(offsets[18])] ??
|
||||||
AssetType.other,
|
AssetType.other,
|
||||||
updatedAt: reader.readDateTime(offsets[18]),
|
updatedAt: reader.readDateTime(offsets[19]),
|
||||||
width: reader.readIntOrNull(offsets[19]),
|
width: reader.readIntOrNull(offsets[20]),
|
||||||
);
|
);
|
||||||
return object;
|
return object;
|
||||||
}
|
}
|
||||||
@ -312,27 +319,29 @@ P _assetDeserializeProp<P>(
|
|||||||
case 8:
|
case 8:
|
||||||
return (reader.readBoolOrNull(offset) ?? false) as P;
|
return (reader.readBoolOrNull(offset) ?? false) as P;
|
||||||
case 9:
|
case 9:
|
||||||
return (reader.readStringOrNull(offset)) as P;
|
return (reader.readBoolOrNull(offset) ?? false) as P;
|
||||||
case 10:
|
case 10:
|
||||||
return (reader.readStringOrNull(offset)) as P;
|
return (reader.readStringOrNull(offset)) as P;
|
||||||
case 11:
|
case 11:
|
||||||
return (reader.readLong(offset)) as P;
|
return (reader.readStringOrNull(offset)) as P;
|
||||||
case 12:
|
case 12:
|
||||||
return (reader.readStringOrNull(offset)) as P;
|
return (reader.readLong(offset)) as P;
|
||||||
case 13:
|
case 13:
|
||||||
return (reader.readLongOrNull(offset) ?? 0) as P;
|
|
||||||
case 14:
|
|
||||||
return (reader.readStringOrNull(offset)) as P;
|
return (reader.readStringOrNull(offset)) as P;
|
||||||
|
case 14:
|
||||||
|
return (reader.readLongOrNull(offset) ?? 0) as P;
|
||||||
case 15:
|
case 15:
|
||||||
return (reader.readStringOrNull(offset)) as P;
|
return (reader.readStringOrNull(offset)) as P;
|
||||||
case 16:
|
case 16:
|
||||||
return (reader.readStringOrNull(offset)) as P;
|
return (reader.readStringOrNull(offset)) as P;
|
||||||
case 17:
|
case 17:
|
||||||
|
return (reader.readStringOrNull(offset)) as P;
|
||||||
|
case 18:
|
||||||
return (_AssettypeValueEnumMap[reader.readByteOrNull(offset)] ??
|
return (_AssettypeValueEnumMap[reader.readByteOrNull(offset)] ??
|
||||||
AssetType.other) as P;
|
AssetType.other) as P;
|
||||||
case 18:
|
|
||||||
return (reader.readDateTime(offset)) as P;
|
|
||||||
case 19:
|
case 19:
|
||||||
|
return (reader.readDateTime(offset)) as P;
|
||||||
|
case 20:
|
||||||
return (reader.readIntOrNull(offset)) as P;
|
return (reader.readIntOrNull(offset)) as P;
|
||||||
default:
|
default:
|
||||||
throw IsarError('Unknown property with id $propertyId');
|
throw IsarError('Unknown property with id $propertyId');
|
||||||
@ -1353,6 +1362,16 @@ extension AssetQueryFilter on QueryBuilder<Asset, Asset, QFilterCondition> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QueryBuilder<Asset, Asset, QAfterFilterCondition> isOfflineEqualTo(
|
||||||
|
bool value) {
|
||||||
|
return QueryBuilder.apply(this, (query) {
|
||||||
|
return query.addFilterCondition(FilterCondition.equalTo(
|
||||||
|
property: r'isOffline',
|
||||||
|
value: value,
|
||||||
|
));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
QueryBuilder<Asset, Asset, QAfterFilterCondition> isTrashedEqualTo(
|
QueryBuilder<Asset, Asset, QAfterFilterCondition> isTrashedEqualTo(
|
||||||
bool value) {
|
bool value) {
|
||||||
return QueryBuilder.apply(this, (query) {
|
return QueryBuilder.apply(this, (query) {
|
||||||
@ -2628,6 +2647,18 @@ extension AssetQuerySortBy on QueryBuilder<Asset, Asset, QSortBy> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QueryBuilder<Asset, Asset, QAfterSortBy> sortByIsOffline() {
|
||||||
|
return QueryBuilder.apply(this, (query) {
|
||||||
|
return query.addSortBy(r'isOffline', Sort.asc);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
QueryBuilder<Asset, Asset, QAfterSortBy> sortByIsOfflineDesc() {
|
||||||
|
return QueryBuilder.apply(this, (query) {
|
||||||
|
return query.addSortBy(r'isOffline', Sort.desc);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
QueryBuilder<Asset, Asset, QAfterSortBy> sortByIsTrashed() {
|
QueryBuilder<Asset, Asset, QAfterSortBy> sortByIsTrashed() {
|
||||||
return QueryBuilder.apply(this, (query) {
|
return QueryBuilder.apply(this, (query) {
|
||||||
return query.addSortBy(r'isTrashed', Sort.asc);
|
return query.addSortBy(r'isTrashed', Sort.asc);
|
||||||
@ -2882,6 +2913,18 @@ extension AssetQuerySortThenBy on QueryBuilder<Asset, Asset, QSortThenBy> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QueryBuilder<Asset, Asset, QAfterSortBy> thenByIsOffline() {
|
||||||
|
return QueryBuilder.apply(this, (query) {
|
||||||
|
return query.addSortBy(r'isOffline', Sort.asc);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
QueryBuilder<Asset, Asset, QAfterSortBy> thenByIsOfflineDesc() {
|
||||||
|
return QueryBuilder.apply(this, (query) {
|
||||||
|
return query.addSortBy(r'isOffline', Sort.desc);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
QueryBuilder<Asset, Asset, QAfterSortBy> thenByIsTrashed() {
|
QueryBuilder<Asset, Asset, QAfterSortBy> thenByIsTrashed() {
|
||||||
return QueryBuilder.apply(this, (query) {
|
return QueryBuilder.apply(this, (query) {
|
||||||
return query.addSortBy(r'isTrashed', Sort.asc);
|
return query.addSortBy(r'isTrashed', Sort.asc);
|
||||||
@ -3078,6 +3121,12 @@ extension AssetQueryWhereDistinct on QueryBuilder<Asset, Asset, QDistinct> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QueryBuilder<Asset, Asset, QDistinct> distinctByIsOffline() {
|
||||||
|
return QueryBuilder.apply(this, (query) {
|
||||||
|
return query.addDistinctBy(r'isOffline');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
QueryBuilder<Asset, Asset, QDistinct> distinctByIsTrashed() {
|
QueryBuilder<Asset, Asset, QDistinct> distinctByIsTrashed() {
|
||||||
return QueryBuilder.apply(this, (query) {
|
return QueryBuilder.apply(this, (query) {
|
||||||
return query.addDistinctBy(r'isTrashed');
|
return query.addDistinctBy(r'isTrashed');
|
||||||
@ -3214,6 +3263,12 @@ extension AssetQueryProperty on QueryBuilder<Asset, Asset, QQueryProperty> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QueryBuilder<Asset, bool, QQueryOperations> isOfflineProperty() {
|
||||||
|
return QueryBuilder.apply(this, (query) {
|
||||||
|
return query.addPropertyName(r'isOffline');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
QueryBuilder<Asset, bool, QQueryOperations> isTrashedProperty() {
|
QueryBuilder<Asset, bool, QQueryOperations> isTrashedProperty() {
|
||||||
return QueryBuilder.apply(this, (query) {
|
return QueryBuilder.apply(this, (query) {
|
||||||
return query.addPropertyName(r'isTrashed');
|
return query.addPropertyName(r'isTrashed');
|
||||||
|
@ -2,6 +2,7 @@ import 'package:immich_mobile/entities/album.entity.dart';
|
|||||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||||
import 'package:immich_mobile/entities/user.entity.dart';
|
import 'package:immich_mobile/entities/user.entity.dart';
|
||||||
import 'package:immich_mobile/interfaces/database.interface.dart';
|
import 'package:immich_mobile/interfaces/database.interface.dart';
|
||||||
|
import 'package:immich_mobile/models/albums/album_search.model.dart';
|
||||||
|
|
||||||
abstract interface class IAlbumRepository implements IDatabaseRepository {
|
abstract interface class IAlbumRepository implements IDatabaseRepository {
|
||||||
Future<Album> create(Album album);
|
Future<Album> create(Album album);
|
||||||
@ -38,6 +39,8 @@ abstract interface class IAlbumRepository implements IDatabaseRepository {
|
|||||||
Future<void> removeAssets(Album album, List<Asset> assets);
|
Future<void> removeAssets(Album album, List<Asset> assets);
|
||||||
|
|
||||||
Future<Album> recalculateMetadata(Album album);
|
Future<Album> recalculateMetadata(Album album);
|
||||||
|
|
||||||
|
Future<List<Album>> search(String searchTerm, QuickFilterMode filterMode);
|
||||||
}
|
}
|
||||||
|
|
||||||
enum AlbumSort { remoteId, localId }
|
enum AlbumSort { remoteId, localId }
|
||||||
|
5
mobile/lib/models/albums/album_search.model.dart
Normal file
5
mobile/lib/models/albums/album_search.model.dart
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
enum QuickFilterMode {
|
||||||
|
all,
|
||||||
|
sharedWithMe,
|
||||||
|
myAlbums,
|
||||||
|
}
|
469
mobile/lib/pages/albums/albums.page.dart
Normal file
469
mobile/lib/pages/albums/albums.page.dart
Normal file
@ -0,0 +1,469 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'dart:math';
|
||||||
|
|
||||||
|
import 'package:auto_route/auto_route.dart';
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
|
import 'package:immich_mobile/extensions/theme_extensions.dart';
|
||||||
|
import 'package:immich_mobile/models/albums/album_search.model.dart';
|
||||||
|
import 'package:immich_mobile/pages/common/large_leading_tile.dart';
|
||||||
|
import 'package:immich_mobile/providers/album/album.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/album/album_sort_by_options.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/user.provider.dart';
|
||||||
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
|
import 'package:immich_mobile/widgets/album/album_thumbnail_card.dart';
|
||||||
|
import 'package:immich_mobile/widgets/common/immich_app_bar.dart';
|
||||||
|
import 'package:immich_mobile/widgets/common/immich_thumbnail.dart';
|
||||||
|
|
||||||
|
@RoutePage()
|
||||||
|
class AlbumsPage extends HookConsumerWidget {
|
||||||
|
const AlbumsPage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final albums =
|
||||||
|
ref.watch(albumProvider).where((album) => album.isRemote).toList();
|
||||||
|
final albumSortOption = ref.watch(albumSortByOptionsProvider);
|
||||||
|
final albumSortIsReverse = ref.watch(albumSortOrderProvider);
|
||||||
|
final sorted = albumSortOption.sortFn(albums, albumSortIsReverse);
|
||||||
|
final isGrid = useState(false);
|
||||||
|
final searchController = useTextEditingController();
|
||||||
|
final debounceTimer = useRef<Timer?>(null);
|
||||||
|
final filterMode = useState(QuickFilterMode.all);
|
||||||
|
final userId = ref.watch(currentUserProvider)?.id;
|
||||||
|
final searchFocusNode = useFocusNode();
|
||||||
|
|
||||||
|
toggleViewMode() {
|
||||||
|
isGrid.value = !isGrid.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
onSearch(String searchTerm, QuickFilterMode mode) {
|
||||||
|
debounceTimer.value?.cancel();
|
||||||
|
debounceTimer.value = Timer(const Duration(milliseconds: 300), () {
|
||||||
|
ref.read(albumProvider.notifier).searchAlbums(searchTerm, mode);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
changeFilter(QuickFilterMode mode) {
|
||||||
|
filterMode.value = mode;
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(
|
||||||
|
() {
|
||||||
|
searchController.addListener(() {
|
||||||
|
onSearch(searchController.text, filterMode.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
return () {
|
||||||
|
searchController.removeListener(() {
|
||||||
|
onSearch(searchController.text, filterMode.value);
|
||||||
|
});
|
||||||
|
debounceTimer.value?.cancel();
|
||||||
|
};
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
clearSearch() {
|
||||||
|
filterMode.value = QuickFilterMode.all;
|
||||||
|
searchController.clear();
|
||||||
|
onSearch('', QuickFilterMode.all);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
appBar: ImmichAppBar(
|
||||||
|
showUploadButton: false,
|
||||||
|
actions: [
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(
|
||||||
|
Icons.add_rounded,
|
||||||
|
size: 28,
|
||||||
|
),
|
||||||
|
onPressed: () => context.pushRoute(
|
||||||
|
CreateAlbumRoute(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
body: RefreshIndicator(
|
||||||
|
displacement: 70,
|
||||||
|
onRefresh: () async {
|
||||||
|
await ref.read(albumProvider.notifier).refreshRemoteAlbums();
|
||||||
|
},
|
||||||
|
child: ListView(
|
||||||
|
shrinkWrap: true,
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12),
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
border: Border.all(
|
||||||
|
color: context.colorScheme.onSurface.withAlpha(0),
|
||||||
|
width: 0,
|
||||||
|
),
|
||||||
|
borderRadius: BorderRadius.circular(24),
|
||||||
|
gradient: LinearGradient(
|
||||||
|
colors: [
|
||||||
|
context.colorScheme.primary.withOpacity(0.075),
|
||||||
|
context.colorScheme.primary.withOpacity(0.09),
|
||||||
|
context.colorScheme.primary.withOpacity(0.075),
|
||||||
|
],
|
||||||
|
begin: Alignment.topLeft,
|
||||||
|
end: Alignment.bottomRight,
|
||||||
|
transform: GradientRotation(0.5 * pi),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: TextField(
|
||||||
|
autofocus: false,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
contentPadding: EdgeInsets.all(16),
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(25),
|
||||||
|
borderSide: BorderSide(
|
||||||
|
color: context.colorScheme.surfaceDim,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
enabledBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(25),
|
||||||
|
borderSide: BorderSide(
|
||||||
|
color: context.colorScheme.surfaceContainer,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
disabledBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(25),
|
||||||
|
borderSide: BorderSide(
|
||||||
|
color: context.colorScheme.surfaceDim,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
focusedBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(25),
|
||||||
|
borderSide: BorderSide(
|
||||||
|
color: context.colorScheme.primary.withAlpha(100),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
hintText: 'search_albums'.tr(),
|
||||||
|
hintStyle: context.textTheme.bodyLarge?.copyWith(
|
||||||
|
color: context.colorScheme.onSurfaceSecondary,
|
||||||
|
),
|
||||||
|
prefixIcon: const Icon(Icons.search_rounded),
|
||||||
|
suffixIcon: searchController.text.isNotEmpty
|
||||||
|
? IconButton(
|
||||||
|
icon: const Icon(Icons.clear_rounded),
|
||||||
|
onPressed: clearSearch,
|
||||||
|
)
|
||||||
|
: const SizedBox.shrink(),
|
||||||
|
),
|
||||||
|
controller: searchController,
|
||||||
|
onChanged: (_) =>
|
||||||
|
onSearch(searchController.text, filterMode.value),
|
||||||
|
focusNode: searchFocusNode,
|
||||||
|
onTapOutside: (_) => searchFocusNode.unfocus(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Wrap(
|
||||||
|
spacing: 4,
|
||||||
|
runSpacing: 4,
|
||||||
|
children: [
|
||||||
|
QuickFilterButton(
|
||||||
|
label: 'all'.tr(),
|
||||||
|
isSelected: filterMode.value == QuickFilterMode.all,
|
||||||
|
onTap: () {
|
||||||
|
changeFilter(QuickFilterMode.all);
|
||||||
|
onSearch(searchController.text, QuickFilterMode.all);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
QuickFilterButton(
|
||||||
|
label: 'shared_with_me'.tr(),
|
||||||
|
isSelected: filterMode.value == QuickFilterMode.sharedWithMe,
|
||||||
|
onTap: () {
|
||||||
|
changeFilter(QuickFilterMode.sharedWithMe);
|
||||||
|
onSearch(
|
||||||
|
searchController.text,
|
||||||
|
QuickFilterMode.sharedWithMe,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
QuickFilterButton(
|
||||||
|
label: 'my_albums'.tr(),
|
||||||
|
isSelected: filterMode.value == QuickFilterMode.myAlbums,
|
||||||
|
onTap: () {
|
||||||
|
changeFilter(QuickFilterMode.myAlbums);
|
||||||
|
onSearch(
|
||||||
|
searchController.text,
|
||||||
|
QuickFilterMode.myAlbums,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
const SortButton(),
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(
|
||||||
|
isGrid.value
|
||||||
|
? Icons.view_list_outlined
|
||||||
|
: Icons.grid_view_outlined,
|
||||||
|
size: 24,
|
||||||
|
),
|
||||||
|
onPressed: toggleViewMode,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 5),
|
||||||
|
AnimatedSwitcher(
|
||||||
|
duration: const Duration(milliseconds: 500),
|
||||||
|
child: isGrid.value
|
||||||
|
? GridView.builder(
|
||||||
|
shrinkWrap: true,
|
||||||
|
physics: const ClampingScrollPhysics(),
|
||||||
|
gridDelegate:
|
||||||
|
const SliverGridDelegateWithMaxCrossAxisExtent(
|
||||||
|
maxCrossAxisExtent: 250,
|
||||||
|
mainAxisSpacing: 12,
|
||||||
|
crossAxisSpacing: 12,
|
||||||
|
childAspectRatio: .7,
|
||||||
|
),
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
return AlbumThumbnailCard(
|
||||||
|
album: sorted[index],
|
||||||
|
onTap: () => context.pushRoute(
|
||||||
|
AlbumViewerRoute(albumId: sorted[index].id),
|
||||||
|
),
|
||||||
|
showOwner: true,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
itemCount: sorted.length,
|
||||||
|
)
|
||||||
|
: ListView.builder(
|
||||||
|
shrinkWrap: true,
|
||||||
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
|
itemCount: sorted.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 8.0),
|
||||||
|
child: LargeLeadingTile(
|
||||||
|
title: Text(
|
||||||
|
sorted[index].name,
|
||||||
|
maxLines: 2,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: context.textTheme.titleSmall?.copyWith(
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
subtitle: sorted[index].ownerId == userId
|
||||||
|
? Text(
|
||||||
|
'${sorted[index].assetCount} items',
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style:
|
||||||
|
context.textTheme.bodyMedium?.copyWith(
|
||||||
|
color: context
|
||||||
|
.colorScheme.onSurfaceSecondary,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: sorted[index].ownerName != null
|
||||||
|
? Text(
|
||||||
|
'${sorted[index].assetCount} items • ${'album_thumbnail_shared_by'.tr(
|
||||||
|
args: [
|
||||||
|
sorted[index].ownerName!,
|
||||||
|
],
|
||||||
|
)}',
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: context.textTheme.bodyMedium
|
||||||
|
?.copyWith(
|
||||||
|
color: context
|
||||||
|
.colorScheme.onSurfaceSecondary,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
onTap: () => context.pushRoute(
|
||||||
|
AlbumViewerRoute(albumId: sorted[index].id),
|
||||||
|
),
|
||||||
|
leadingPadding: const EdgeInsets.only(
|
||||||
|
right: 16,
|
||||||
|
),
|
||||||
|
leading: ClipRRect(
|
||||||
|
borderRadius: const BorderRadius.all(
|
||||||
|
Radius.circular(15),
|
||||||
|
),
|
||||||
|
child: ImmichThumbnail(
|
||||||
|
asset: sorted[index].thumbnail.value,
|
||||||
|
width: 80,
|
||||||
|
height: 80,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// minVerticalPadding: 1,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class QuickFilterButton extends StatelessWidget {
|
||||||
|
const QuickFilterButton({
|
||||||
|
super.key,
|
||||||
|
required this.isSelected,
|
||||||
|
required this.onTap,
|
||||||
|
required this.label,
|
||||||
|
});
|
||||||
|
|
||||||
|
final bool isSelected;
|
||||||
|
final VoidCallback onTap;
|
||||||
|
final String label;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return TextButton(
|
||||||
|
onPressed: onTap,
|
||||||
|
style: ButtonStyle(
|
||||||
|
backgroundColor: WidgetStateProperty.all(
|
||||||
|
isSelected ? context.colorScheme.primary : Colors.transparent,
|
||||||
|
),
|
||||||
|
shape: WidgetStateProperty.all(
|
||||||
|
RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
side: BorderSide(
|
||||||
|
color: context.colorScheme.onSurface.withAlpha(25),
|
||||||
|
width: 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
label,
|
||||||
|
style: TextStyle(
|
||||||
|
color: isSelected
|
||||||
|
? context.colorScheme.onPrimary
|
||||||
|
: context.colorScheme.onSurface,
|
||||||
|
fontSize: 14,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class SortButton extends ConsumerWidget {
|
||||||
|
const SortButton({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final albumSortOption = ref.watch(albumSortByOptionsProvider);
|
||||||
|
final albumSortIsReverse = ref.watch(albumSortOrderProvider);
|
||||||
|
|
||||||
|
return MenuAnchor(
|
||||||
|
style: MenuStyle(
|
||||||
|
elevation: WidgetStatePropertyAll(1),
|
||||||
|
shape: WidgetStateProperty.all(
|
||||||
|
RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(24),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
padding: WidgetStatePropertyAll(
|
||||||
|
EdgeInsets.all(4),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
consumeOutsideTap: true,
|
||||||
|
menuChildren: AlbumSortMode.values
|
||||||
|
.map(
|
||||||
|
(mode) => MenuItemButton(
|
||||||
|
leadingIcon: albumSortOption == mode
|
||||||
|
? albumSortIsReverse
|
||||||
|
? Icon(
|
||||||
|
Icons.keyboard_arrow_down,
|
||||||
|
color: albumSortOption == mode
|
||||||
|
? context.colorScheme.onPrimary
|
||||||
|
: context.colorScheme.onSurface,
|
||||||
|
)
|
||||||
|
: Icon(
|
||||||
|
Icons.keyboard_arrow_up_rounded,
|
||||||
|
color: albumSortOption == mode
|
||||||
|
? context.colorScheme.onPrimary
|
||||||
|
: context.colorScheme.onSurface,
|
||||||
|
)
|
||||||
|
: const Icon(Icons.abc, color: Colors.transparent),
|
||||||
|
onPressed: () {
|
||||||
|
final selected = albumSortOption == mode;
|
||||||
|
// Switch direction
|
||||||
|
if (selected) {
|
||||||
|
ref
|
||||||
|
.read(albumSortOrderProvider.notifier)
|
||||||
|
.changeSortDirection(!albumSortIsReverse);
|
||||||
|
} else {
|
||||||
|
ref
|
||||||
|
.read(albumSortByOptionsProvider.notifier)
|
||||||
|
.changeSortMode(mode);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
style: ButtonStyle(
|
||||||
|
padding: WidgetStateProperty.all(
|
||||||
|
const EdgeInsets.fromLTRB(16, 16, 32, 16),
|
||||||
|
),
|
||||||
|
backgroundColor: WidgetStateProperty.all(
|
||||||
|
albumSortOption == mode
|
||||||
|
? context.colorScheme.primary
|
||||||
|
: Colors.transparent,
|
||||||
|
),
|
||||||
|
shape: WidgetStateProperty.all(
|
||||||
|
RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(24),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
mode.label.tr(),
|
||||||
|
style: context.textTheme.titleSmall?.copyWith(
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: albumSortOption == mode
|
||||||
|
? context.colorScheme.onPrimary
|
||||||
|
: context.colorScheme.onSurface.withAlpha(185),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
builder: (context, controller, child) {
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: () {
|
||||||
|
if (controller.isOpen) {
|
||||||
|
controller.close();
|
||||||
|
} else {
|
||||||
|
controller.open();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(right: 5),
|
||||||
|
child: Transform.rotate(
|
||||||
|
angle: 90 * pi / 180,
|
||||||
|
child: Icon(
|
||||||
|
Icons.compare_arrows_rounded,
|
||||||
|
size: 18,
|
||||||
|
color: context.colorScheme.onSurface.withAlpha(225),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
albumSortOption.label.tr(),
|
||||||
|
style: context.textTheme.bodyLarge?.copyWith(
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
color: context.colorScheme.onSurface.withAlpha(225),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -151,7 +151,7 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
|
|||||||
|
|
||||||
handleSyncAlbumToggle(bool isEnable) async {
|
handleSyncAlbumToggle(bool isEnable) async {
|
||||||
if (isEnable) {
|
if (isEnable) {
|
||||||
await ref.read(albumProvider.notifier).getAllAlbums();
|
await ref.read(albumProvider.notifier).refreshRemoteAlbums();
|
||||||
for (final album in selectedBackupAlbums) {
|
for (final album in selectedBackupAlbums) {
|
||||||
await ref.read(albumProvider.notifier).createSyncAlbum(album.name);
|
await ref.read(albumProvider.notifier).createSyncAlbum(album.name);
|
||||||
}
|
}
|
||||||
|
@ -212,7 +212,7 @@ class BackupControllerPage extends HookConsumerWidget {
|
|||||||
.read(backupProvider.notifier)
|
.read(backupProvider.notifier)
|
||||||
.backupAlbumSelectionDone();
|
.backupAlbumSelectionDone();
|
||||||
// waited until backup albums are stored in DB
|
// waited until backup albums are stored in DB
|
||||||
ref.read(albumProvider.notifier).getDeviceAlbums();
|
ref.read(albumProvider.notifier).refreshDeviceAlbums();
|
||||||
},
|
},
|
||||||
child: const Text(
|
child: const Text(
|
||||||
"backup_controller_page_select",
|
"backup_controller_page_select",
|
||||||
|
@ -6,7 +6,7 @@ import 'package:fluttertoast/fluttertoast.dart';
|
|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
import 'package:immich_mobile/extensions/theme_extensions.dart';
|
import 'package:immich_mobile/extensions/theme_extensions.dart';
|
||||||
import 'package:immich_mobile/providers/album/shared_album.provider.dart';
|
import 'package:immich_mobile/providers/album/album.provider.dart';
|
||||||
import 'package:immich_mobile/providers/authentication.provider.dart';
|
import 'package:immich_mobile/providers/authentication.provider.dart';
|
||||||
import 'package:immich_mobile/utils/immich_loading_overlay.dart';
|
import 'package:immich_mobile/utils/immich_loading_overlay.dart';
|
||||||
import 'package:immich_mobile/routing/router.dart';
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
@ -45,11 +45,11 @@ class AlbumOptionsPage extends HookConsumerWidget {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
final isSuccess =
|
final isSuccess =
|
||||||
await ref.read(sharedAlbumProvider.notifier).leaveAlbum(album);
|
await ref.read(albumProvider.notifier).leaveAlbum(album);
|
||||||
|
|
||||||
if (isSuccess) {
|
if (isSuccess) {
|
||||||
context.navigateTo(
|
context.navigateTo(
|
||||||
const TabControllerRoute(children: [SharingRoute()]),
|
TabControllerRoute(children: [AlbumsRoute()]),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
showErrorMessage();
|
showErrorMessage();
|
||||||
@ -65,9 +65,7 @@ class AlbumOptionsPage extends HookConsumerWidget {
|
|||||||
isProcessing.value = true;
|
isProcessing.value = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await ref
|
await ref.read(albumProvider.notifier).removeUser(album, user);
|
||||||
.read(sharedAlbumProvider.notifier)
|
|
||||||
.removeUserFromAlbum(album, user);
|
|
||||||
album.sharedUsers.remove(user);
|
album.sharedUsers.remove(user);
|
||||||
sharedUsers.value = album.sharedUsers.toList();
|
sharedUsers.value = album.sharedUsers.toList();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -200,8 +198,8 @@ class AlbumOptionsPage extends HookConsumerWidget {
|
|||||||
onChanged: (bool value) async {
|
onChanged: (bool value) async {
|
||||||
activityEnabled.value = value;
|
activityEnabled.value = value;
|
||||||
if (await ref
|
if (await ref
|
||||||
.read(sharedAlbumProvider.notifier)
|
.read(albumProvider.notifier)
|
||||||
.setActivityEnabled(album, value)) {
|
.setActivitystatus(album, value)) {
|
||||||
album.activityEnabled = value;
|
album.activityEnabled = value;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -5,8 +5,8 @@ import 'package:flutter_hooks/flutter_hooks.dart';
|
|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
|
import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
|
||||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
|
import 'package:immich_mobile/providers/album/album.provider.dart';
|
||||||
import 'package:immich_mobile/providers/album/album_title.provider.dart';
|
import 'package:immich_mobile/providers/album/album_title.provider.dart';
|
||||||
import 'package:immich_mobile/providers/album/shared_album.provider.dart';
|
|
||||||
import 'package:immich_mobile/providers/album/suggested_shared_users.provider.dart';
|
import 'package:immich_mobile/providers/album/suggested_shared_users.provider.dart';
|
||||||
import 'package:immich_mobile/routing/router.dart';
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||||
@ -25,20 +25,15 @@ class AlbumSharedUserSelectionPage extends HookConsumerWidget {
|
|||||||
final suggestedShareUsers = ref.watch(otherUsersProvider);
|
final suggestedShareUsers = ref.watch(otherUsersProvider);
|
||||||
|
|
||||||
createSharedAlbum() async {
|
createSharedAlbum() async {
|
||||||
var newAlbum =
|
var newAlbum = await ref.watch(albumProvider.notifier).createAlbum(
|
||||||
await ref.watch(sharedAlbumProvider.notifier).createSharedAlbum(
|
|
||||||
ref.watch(albumTitleProvider),
|
ref.watch(albumTitleProvider),
|
||||||
assets,
|
assets,
|
||||||
sharedUsersList.value,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (newAlbum != null) {
|
if (newAlbum != null) {
|
||||||
await ref.watch(sharedAlbumProvider.notifier).getAllSharedAlbums();
|
|
||||||
// ref.watch(assetSelectionProvider.notifier).removeAll();
|
|
||||||
ref.watch(albumTitleProvider.notifier).clearAlbumTitle();
|
ref.watch(albumTitleProvider.notifier).clearAlbumTitle();
|
||||||
context.maybePop(true);
|
context.maybePop(true);
|
||||||
context
|
context.navigateTo(TabControllerRoute(children: [AlbumsRoute()]));
|
||||||
.navigateTo(const TabControllerRoute(children: [SharingRoute()]));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ScaffoldMessenger(
|
ScaffoldMessenger(
|
||||||
|
@ -11,9 +11,7 @@ import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
|||||||
import 'package:immich_mobile/models/albums/asset_selection_page_result.model.dart';
|
import 'package:immich_mobile/models/albums/asset_selection_page_result.model.dart';
|
||||||
import 'package:immich_mobile/providers/album/album.provider.dart';
|
import 'package:immich_mobile/providers/album/album.provider.dart';
|
||||||
import 'package:immich_mobile/providers/album/current_album.provider.dart';
|
import 'package:immich_mobile/providers/album/current_album.provider.dart';
|
||||||
import 'package:immich_mobile/providers/album/shared_album.provider.dart';
|
|
||||||
import 'package:immich_mobile/utils/immich_loading_overlay.dart';
|
import 'package:immich_mobile/utils/immich_loading_overlay.dart';
|
||||||
import 'package:immich_mobile/services/album.service.dart';
|
|
||||||
import 'package:immich_mobile/widgets/album/album_action_filled_button.dart';
|
import 'package:immich_mobile/widgets/album/album_action_filled_button.dart';
|
||||||
import 'package:immich_mobile/widgets/album/album_viewer_editable_title.dart';
|
import 'package:immich_mobile/widgets/album/album_viewer_editable_title.dart';
|
||||||
import 'package:immich_mobile/providers/multiselect.provider.dart';
|
import 'package:immich_mobile/providers/multiselect.provider.dart';
|
||||||
@ -50,9 +48,7 @@ class AlbumViewerPage extends HookConsumerWidget {
|
|||||||
Future<bool> onRemoveFromAlbumPressed(Iterable<Asset> assets) async {
|
Future<bool> onRemoveFromAlbumPressed(Iterable<Asset> assets) async {
|
||||||
final a = album.valueOrNull;
|
final a = album.valueOrNull;
|
||||||
final bool isSuccess = a != null &&
|
final bool isSuccess = a != null &&
|
||||||
await ref
|
await ref.read(albumProvider.notifier).removeAsset(a, assets);
|
||||||
.read(sharedAlbumProvider.notifier)
|
|
||||||
.removeAssetFromAlbum(a, assets);
|
|
||||||
|
|
||||||
if (!isSuccess) {
|
if (!isSuccess) {
|
||||||
ImmichToast.show(
|
ImmichToast.show(
|
||||||
@ -81,9 +77,9 @@ class AlbumViewerPage extends HookConsumerWidget {
|
|||||||
// Check if there is new assets add
|
// Check if there is new assets add
|
||||||
isProcessing.value = true;
|
isProcessing.value = true;
|
||||||
|
|
||||||
await ref.watch(albumServiceProvider).addAdditionalAssetToAlbum(
|
await ref.watch(albumProvider.notifier).addAssets(
|
||||||
returnPayload.selectedAssets,
|
|
||||||
albumInfo,
|
albumInfo,
|
||||||
|
returnPayload.selectedAssets,
|
||||||
);
|
);
|
||||||
|
|
||||||
isProcessing.value = false;
|
isProcessing.value = false;
|
||||||
@ -98,9 +94,7 @@ class AlbumViewerPage extends HookConsumerWidget {
|
|||||||
if (sharedUserIds != null) {
|
if (sharedUserIds != null) {
|
||||||
isProcessing.value = true;
|
isProcessing.value = true;
|
||||||
|
|
||||||
await ref
|
await ref.watch(albumProvider.notifier).addUsers(album, sharedUserIds);
|
||||||
.watch(albumServiceProvider)
|
|
||||||
.addAdditionalUserToAlbum(sharedUserIds, album);
|
|
||||||
|
|
||||||
isProcessing.value = false;
|
isProcessing.value = false;
|
||||||
}
|
}
|
||||||
@ -184,7 +178,8 @@ class AlbumViewerPage extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget buildSharedUserIconsRow(Album album) {
|
Widget buildSharedUserIconsRow(Album album) {
|
||||||
return GestureDetector(
|
return album.sharedUsers.isNotEmpty
|
||||||
|
? GestureDetector(
|
||||||
onTap: () => context.pushRoute(AlbumOptionsRoute(album: album)),
|
onTap: () => context.pushRoute(AlbumOptionsRoute(album: album)),
|
||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
height: 50,
|
height: 50,
|
||||||
@ -204,7 +199,8 @@ class AlbumViewerPage extends HookConsumerWidget {
|
|||||||
itemCount: album.sharedUsers.length,
|
itemCount: album.sharedUsers.length,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
)
|
||||||
|
: const SizedBox.shrink();
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget buildHeader(Album album) {
|
Widget buildHeader(Album album) {
|
||||||
@ -214,7 +210,7 @@ class AlbumViewerPage extends HookConsumerWidget {
|
|||||||
children: [
|
children: [
|
||||||
buildTitle(album),
|
buildTitle(album),
|
||||||
if (album.assets.isNotEmpty == true) buildAlbumDateRange(album),
|
if (album.assets.isNotEmpty == true) buildAlbumDateRange(album),
|
||||||
if (album.shared) buildSharedUserIconsRow(album),
|
buildSharedUserIconsRow(album),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -231,17 +227,17 @@ class AlbumViewerPage extends HookConsumerWidget {
|
|||||||
body: Stack(
|
body: Stack(
|
||||||
children: [
|
children: [
|
||||||
album.widgetWhen(
|
album.widgetWhen(
|
||||||
onData: (data) => MultiselectGrid(
|
onData: (albumInfo) => MultiselectGrid(
|
||||||
renderListProvider: albumRenderlistProvider(albumId),
|
renderListProvider: albumRenderlistProvider(albumId),
|
||||||
topWidget: Column(
|
topWidget: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
buildHeader(data),
|
buildHeader(albumInfo),
|
||||||
if (data.isRemote) buildControlButton(data),
|
if (albumInfo.isRemote) buildControlButton(albumInfo),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
onRemoveFromAlbum: onRemoveFromAlbumPressed,
|
onRemoveFromAlbum: onRemoveFromAlbumPressed,
|
||||||
editEnabled: data.ownerId == userId,
|
editEnabled: albumInfo.ownerId == userId,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
AnimatedPositioned(
|
AnimatedPositioned(
|
||||||
|
@ -17,13 +17,11 @@ import 'package:immich_mobile/widgets/album/shared_album_thumbnail_image.dart';
|
|||||||
@RoutePage()
|
@RoutePage()
|
||||||
// ignore: must_be_immutable
|
// ignore: must_be_immutable
|
||||||
class CreateAlbumPage extends HookConsumerWidget {
|
class CreateAlbumPage extends HookConsumerWidget {
|
||||||
final bool isSharedAlbum;
|
final List<Asset>? assets;
|
||||||
final List<Asset>? initialAssets;
|
|
||||||
|
|
||||||
const CreateAlbumPage({
|
const CreateAlbumPage({
|
||||||
super.key,
|
super.key,
|
||||||
required this.isSharedAlbum,
|
this.assets,
|
||||||
this.initialAssets,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -34,18 +32,9 @@ class CreateAlbumPage extends HookConsumerWidget {
|
|||||||
final isAlbumTitleTextFieldFocus = useState(false);
|
final isAlbumTitleTextFieldFocus = useState(false);
|
||||||
final isAlbumTitleEmpty = useState(true);
|
final isAlbumTitleEmpty = useState(true);
|
||||||
final selectedAssets = useState<Set<Asset>>(
|
final selectedAssets = useState<Set<Asset>>(
|
||||||
initialAssets != null ? Set.from(initialAssets!) : const {},
|
assets != null ? Set.from(assets!) : const {},
|
||||||
);
|
);
|
||||||
|
|
||||||
showSelectUserPage() async {
|
|
||||||
final bool? ok = await context.pushRoute<bool?>(
|
|
||||||
AlbumSharedUserSelectionRoute(assets: selectedAssets.value),
|
|
||||||
);
|
|
||||||
if (ok == true) {
|
|
||||||
selectedAssets.value = {};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void onBackgroundTapped() {
|
void onBackgroundTapped() {
|
||||||
albumTitleTextFieldFocusNode.unfocus();
|
albumTitleTextFieldFocusNode.unfocus();
|
||||||
isAlbumTitleTextFieldFocus.value = false;
|
isAlbumTitleTextFieldFocus.value = false;
|
||||||
@ -199,7 +188,7 @@ class CreateAlbumPage extends HookConsumerWidget {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (newAlbum != null) {
|
if (newAlbum != null) {
|
||||||
ref.watch(albumProvider.notifier).getAllAlbums();
|
ref.watch(albumProvider.notifier).refreshRemoteAlbums();
|
||||||
selectedAssets.value = {};
|
selectedAssets.value = {};
|
||||||
ref.watch(albumTitleProvider.notifier).clearAlbumTitle();
|
ref.watch(albumTitleProvider.notifier).clearAlbumTitle();
|
||||||
|
|
||||||
@ -223,22 +212,6 @@ class CreateAlbumPage extends HookConsumerWidget {
|
|||||||
'share_create_album',
|
'share_create_album',
|
||||||
).tr(),
|
).tr(),
|
||||||
actions: [
|
actions: [
|
||||||
if (isSharedAlbum)
|
|
||||||
TextButton(
|
|
||||||
onPressed: albumTitleController.text.isNotEmpty
|
|
||||||
? showSelectUserPage
|
|
||||||
: null,
|
|
||||||
child: Text(
|
|
||||||
'create_shared_album_page_share'.tr(),
|
|
||||||
style: TextStyle(
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
color: albumTitleController.text.isEmpty
|
|
||||||
? context.themeData.disabledColor
|
|
||||||
: context.primaryColor,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (!isSharedAlbum)
|
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: albumTitleController.text.isNotEmpty
|
onPressed: albumTitleController.text.isNotEmpty
|
||||||
? createNonSharedAlbum
|
? createNonSharedAlbum
|
||||||
|
50
mobile/lib/pages/common/large_leading_tile.dart
Normal file
50
mobile/lib/pages/common/large_leading_tile.dart
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class LargeLeadingTile extends StatelessWidget {
|
||||||
|
const LargeLeadingTile({
|
||||||
|
super.key,
|
||||||
|
required this.leading,
|
||||||
|
required this.onTap,
|
||||||
|
required this.title,
|
||||||
|
this.subtitle,
|
||||||
|
this.leadingPadding = const EdgeInsets.symmetric(
|
||||||
|
vertical: 8,
|
||||||
|
horizontal: 16.0,
|
||||||
|
),
|
||||||
|
this.borderRadius = 20.0,
|
||||||
|
});
|
||||||
|
|
||||||
|
final Widget leading;
|
||||||
|
final VoidCallback onTap;
|
||||||
|
final Widget title;
|
||||||
|
final Widget? subtitle;
|
||||||
|
final EdgeInsetsGeometry leadingPadding;
|
||||||
|
final double borderRadius;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return InkWell(
|
||||||
|
borderRadius: BorderRadius.circular(borderRadius),
|
||||||
|
onTap: onTap,
|
||||||
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: leadingPadding,
|
||||||
|
child: leading,
|
||||||
|
),
|
||||||
|
Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
SizedBox(
|
||||||
|
width: MediaQuery.of(context).size.width * 0.6,
|
||||||
|
child: title,
|
||||||
|
),
|
||||||
|
subtitle ?? const SizedBox.shrink(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -3,6 +3,7 @@ import 'package:easy_localization/easy_localization.dart';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
|
import 'package:immich_mobile/providers/album/album.provider.dart';
|
||||||
import 'package:immich_mobile/providers/asset_viewer/scroll_notifier.provider.dart';
|
import 'package:immich_mobile/providers/asset_viewer/scroll_notifier.provider.dart';
|
||||||
import 'package:immich_mobile/providers/multiselect.provider.dart';
|
import 'package:immich_mobile/providers/multiselect.provider.dart';
|
||||||
import 'package:immich_mobile/routing/router.dart';
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
@ -16,10 +17,11 @@ class TabControllerPage extends HookConsumerWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final refreshing = ref.watch(assetProvider);
|
final isRefreshingAssets = ref.watch(assetProvider);
|
||||||
|
final isRefreshingRemoteAlbums = ref.watch(isRefreshingRemoteAlbumProvider);
|
||||||
|
|
||||||
Widget buildIcon(Widget icon) {
|
Widget buildIcon({required Widget icon, required bool isProcessing}) {
|
||||||
if (!refreshing) return icon;
|
if (!isProcessing) return icon;
|
||||||
return Stack(
|
return Stack(
|
||||||
alignment: Alignment.center,
|
alignment: Alignment.center,
|
||||||
clipBehavior: Clip.none,
|
clipBehavior: Clip.none,
|
||||||
@ -84,15 +86,15 @@ class TabControllerPage extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
NavigationRailDestination(
|
NavigationRailDestination(
|
||||||
padding: const EdgeInsets.all(4),
|
padding: const EdgeInsets.all(4),
|
||||||
icon: const Icon(Icons.share_rounded),
|
icon: const Icon(Icons.photo_album_outlined),
|
||||||
selectedIcon: const Icon(Icons.share),
|
selectedIcon: const Icon(Icons.photo_album),
|
||||||
label: const Text('tab_controller_nav_sharing').tr(),
|
label: const Text('albums').tr(),
|
||||||
),
|
),
|
||||||
NavigationRailDestination(
|
NavigationRailDestination(
|
||||||
padding: const EdgeInsets.all(4),
|
padding: const EdgeInsets.all(4),
|
||||||
icon: const Icon(Icons.photo_album_outlined),
|
icon: const Icon(Icons.space_dashboard_outlined),
|
||||||
selectedIcon: const Icon(Icons.photo_album),
|
selectedIcon: const Icon(Icons.space_dashboard_rounded),
|
||||||
label: const Text('tab_controller_nav_library').tr(),
|
label: const Text('library').tr(),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
@ -118,7 +120,8 @@ class TabControllerPage extends HookConsumerWidget {
|
|||||||
Icons.photo_library_outlined,
|
Icons.photo_library_outlined,
|
||||||
),
|
),
|
||||||
selectedIcon: buildIcon(
|
selectedIcon: buildIcon(
|
||||||
Icon(
|
isProcessing: isRefreshingAssets,
|
||||||
|
icon: Icon(
|
||||||
Icons.photo_library,
|
Icons.photo_library,
|
||||||
color: context.primaryColor,
|
color: context.primaryColor,
|
||||||
),
|
),
|
||||||
@ -135,38 +138,42 @@ class TabControllerPage extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
NavigationDestination(
|
NavigationDestination(
|
||||||
label: 'tab_controller_nav_sharing'.tr(),
|
label: 'albums'.tr(),
|
||||||
icon: const Icon(
|
|
||||||
Icons.group_outlined,
|
|
||||||
),
|
|
||||||
selectedIcon: Icon(
|
|
||||||
Icons.group,
|
|
||||||
color: context.primaryColor,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
NavigationDestination(
|
|
||||||
label: 'tab_controller_nav_library'.tr(),
|
|
||||||
icon: const Icon(
|
icon: const Icon(
|
||||||
Icons.photo_album_outlined,
|
Icons.photo_album_outlined,
|
||||||
),
|
),
|
||||||
selectedIcon: buildIcon(
|
selectedIcon: buildIcon(
|
||||||
Icon(
|
isProcessing: isRefreshingRemoteAlbums,
|
||||||
|
icon: Icon(
|
||||||
Icons.photo_album_rounded,
|
Icons.photo_album_rounded,
|
||||||
color: context.primaryColor,
|
color: context.primaryColor,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
NavigationDestination(
|
||||||
|
label: 'library'.tr(),
|
||||||
|
icon: const Icon(
|
||||||
|
Icons.space_dashboard_outlined,
|
||||||
|
),
|
||||||
|
selectedIcon: buildIcon(
|
||||||
|
isProcessing: isRefreshingAssets,
|
||||||
|
icon: Icon(
|
||||||
|
Icons.space_dashboard_rounded,
|
||||||
|
color: context.primaryColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
final multiselectEnabled = ref.watch(multiselectProvider);
|
final multiselectEnabled = ref.watch(multiselectProvider);
|
||||||
return AutoTabsRouter(
|
return AutoTabsRouter(
|
||||||
routes: const [
|
routes: [
|
||||||
PhotosRoute(),
|
const PhotosRoute(),
|
||||||
SearchRoute(),
|
SearchInputRoute(),
|
||||||
SharingRoute(),
|
const AlbumsRoute(),
|
||||||
LibraryRoute(),
|
const LibraryRoute(),
|
||||||
],
|
],
|
||||||
duration: const Duration(milliseconds: 600),
|
duration: const Duration(milliseconds: 600),
|
||||||
transitionBuilder: (context, child, animation) => FadeTransition(
|
transitionBuilder: (context, child, animation) => FadeTransition(
|
||||||
|
@ -69,7 +69,7 @@ class EditImagePage extends ConsumerWidget {
|
|||||||
imageData,
|
imageData,
|
||||||
title: "${p.withoutExtension(asset.fileName)}_edited.jpg",
|
title: "${p.withoutExtension(asset.fileName)}_edited.jpg",
|
||||||
);
|
);
|
||||||
await ref.read(albumProvider.notifier).getDeviceAlbums();
|
await ref.read(albumProvider.notifier).refreshDeviceAlbums();
|
||||||
Navigator.of(context).popUntil((route) => route.isFirst);
|
Navigator.of(context).popUntil((route) => route.isFirst);
|
||||||
ImmichToast.show(
|
ImmichToast.show(
|
||||||
durationInSecond: 3,
|
durationInSecond: 3,
|
||||||
|
@ -1,175 +1,401 @@
|
|||||||
import 'package:auto_route/auto_route.dart';
|
import 'package:auto_route/auto_route.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/entities/user.entity.dart';
|
||||||
|
import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
|
||||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
import 'package:immich_mobile/providers/album/album.provider.dart';
|
import 'package:immich_mobile/providers/album/album.provider.dart';
|
||||||
import 'package:immich_mobile/providers/album/album_sort_by_options.provider.dart';
|
import 'package:immich_mobile/providers/partner.provider.dart';
|
||||||
import 'package:immich_mobile/widgets/album/album_thumbnail_card.dart';
|
import 'package:immich_mobile/providers/search/people.provider.dart';
|
||||||
import 'package:immich_mobile/routing/router.dart';
|
|
||||||
import 'package:immich_mobile/providers/server_info.provider.dart';
|
import 'package:immich_mobile/providers/server_info.provider.dart';
|
||||||
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
|
import 'package:immich_mobile/services/api.service.dart';
|
||||||
|
import 'package:immich_mobile/utils/image_url_builder.dart';
|
||||||
|
import 'package:immich_mobile/widgets/album/album_thumbnail_card.dart';
|
||||||
import 'package:immich_mobile/widgets/common/immich_app_bar.dart';
|
import 'package:immich_mobile/widgets/common/immich_app_bar.dart';
|
||||||
|
import 'package:immich_mobile/widgets/common/user_avatar.dart';
|
||||||
|
import 'package:immich_mobile/widgets/map/map_thumbnail.dart';
|
||||||
|
import 'package:maplibre_gl/maplibre_gl.dart';
|
||||||
|
|
||||||
@RoutePage()
|
@RoutePage()
|
||||||
class LibraryPage extends HookConsumerWidget {
|
class LibraryPage extends ConsumerWidget {
|
||||||
const LibraryPage({super.key});
|
const LibraryPage({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final trashEnabled =
|
final trashEnabled =
|
||||||
ref.watch(serverInfoProvider.select((v) => v.serverFeatures.trash));
|
ref.watch(serverInfoProvider.select((v) => v.serverFeatures.trash));
|
||||||
final albums = ref.watch(albumProvider);
|
|
||||||
final albumSortOption = ref.watch(albumSortByOptionsProvider);
|
|
||||||
final albumSortIsReverse = ref.watch(albumSortOrderProvider);
|
|
||||||
|
|
||||||
useEffect(
|
return Scaffold(
|
||||||
() {
|
appBar: ImmichAppBar(),
|
||||||
ref.read(albumProvider.notifier).getAllAlbums();
|
body: Padding(
|
||||||
return null;
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
},
|
child: ListView(
|
||||||
[],
|
shrinkWrap: true,
|
||||||
);
|
|
||||||
|
|
||||||
Widget buildSortButton() {
|
|
||||||
return PopupMenuButton(
|
|
||||||
position: PopupMenuPosition.over,
|
|
||||||
itemBuilder: (BuildContext context) {
|
|
||||||
return AlbumSortMode.values
|
|
||||||
.map<PopupMenuEntry<AlbumSortMode>>((option) {
|
|
||||||
final selected = albumSortOption == option;
|
|
||||||
return PopupMenuItem(
|
|
||||||
value: option,
|
|
||||||
child: Row(
|
|
||||||
children: [
|
children: [
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(right: 12.0),
|
padding: const EdgeInsets.only(top: 16.0),
|
||||||
child: Icon(
|
child: Row(
|
||||||
Icons.check,
|
children: [
|
||||||
color:
|
ActionButton(
|
||||||
selected ? context.primaryColor : Colors.transparent,
|
onPressed: () => context.pushRoute(const FavoritesRoute()),
|
||||||
|
icon: Icons.favorite_outline_rounded,
|
||||||
|
label: 'favorites'.tr(),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
ActionButton(
|
||||||
|
onPressed: () => context.pushRoute(const ArchiveRoute()),
|
||||||
|
icon: Icons.archive_outlined,
|
||||||
|
label: 'archived'.tr(),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Text(
|
const SizedBox(height: 8),
|
||||||
option.label.tr(),
|
Row(
|
||||||
|
children: [
|
||||||
|
ActionButton(
|
||||||
|
onPressed: () => context.pushRoute(const SharedLinkRoute()),
|
||||||
|
icon: Icons.link_outlined,
|
||||||
|
label: 'shared_links'.tr(),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
trashEnabled
|
||||||
|
? ActionButton(
|
||||||
|
onPressed: () => context.pushRoute(const TrashRoute()),
|
||||||
|
icon: Icons.delete_outline_rounded,
|
||||||
|
label: 'trash'.tr(),
|
||||||
|
)
|
||||||
|
: const SizedBox.shrink(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
const Wrap(
|
||||||
|
spacing: 8,
|
||||||
|
runSpacing: 8,
|
||||||
|
children: [
|
||||||
|
PeopleCollectionCard(),
|
||||||
|
PlacesCollectionCard(),
|
||||||
|
LocalAlbumsCollectionCard(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
QuickAccessButtons(),
|
||||||
|
const SizedBox(
|
||||||
|
height: 32,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class QuickAccessButtons extends ConsumerWidget {
|
||||||
|
const QuickAccessButtons({super.key});
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final partners = ref.watch(partnerSharedWithProvider);
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
border: Border.all(
|
||||||
|
color: context.colorScheme.onSurface.withAlpha(10),
|
||||||
|
width: 1,
|
||||||
|
),
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
gradient: LinearGradient(
|
||||||
|
colors: [
|
||||||
|
context.colorScheme.primary.withAlpha(10),
|
||||||
|
context.colorScheme.primary.withAlpha(15),
|
||||||
|
],
|
||||||
|
begin: Alignment.topCenter,
|
||||||
|
end: Alignment.bottomCenter,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: ListView(
|
||||||
|
shrinkWrap: true,
|
||||||
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
|
children: [
|
||||||
|
ListTile(
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.only(
|
||||||
|
topLeft: Radius.circular(20),
|
||||||
|
topRight: Radius.circular(20),
|
||||||
|
bottomLeft: Radius.circular(partners.isEmpty ? 20 : 0),
|
||||||
|
bottomRight: Radius.circular(partners.isEmpty ? 20 : 0),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
leading: const Icon(
|
||||||
|
Icons.group_outlined,
|
||||||
|
size: 26,
|
||||||
|
),
|
||||||
|
title: Text(
|
||||||
|
'partners'.tr(),
|
||||||
|
style: context.textTheme.titleSmall?.copyWith(
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onTap: () => context.pushRoute(const PartnerRoute()),
|
||||||
|
),
|
||||||
|
PartnerList(partners: partners),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class PartnerList extends ConsumerWidget {
|
||||||
|
const PartnerList({super.key, required this.partners});
|
||||||
|
|
||||||
|
final List<User> partners;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
return ListView.builder(
|
||||||
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
|
itemCount: partners.length,
|
||||||
|
shrinkWrap: true,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final partner = partners[index];
|
||||||
|
final isLastItem = index == partners.length - 1;
|
||||||
|
return ListTile(
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.only(
|
||||||
|
bottomLeft: Radius.circular(isLastItem ? 20 : 0),
|
||||||
|
bottomRight: Radius.circular(isLastItem ? 20 : 0),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
contentPadding: const EdgeInsets.only(
|
||||||
|
left: 12.0,
|
||||||
|
right: 18.0,
|
||||||
|
),
|
||||||
|
leading: userAvatar(context, partner, radius: 16),
|
||||||
|
title: Text(
|
||||||
|
"partner_list_user_photos",
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: selected ? context.primaryColor : null,
|
fontWeight: FontWeight.w500,
|
||||||
fontSize: 14.0,
|
|
||||||
),
|
),
|
||||||
|
).tr(
|
||||||
|
namedArgs: {
|
||||||
|
'user': partner.name,
|
||||||
|
},
|
||||||
),
|
),
|
||||||
],
|
onTap: () => context.pushRoute(
|
||||||
|
(PartnerDetailRoute(partner: partner)),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}).toList();
|
|
||||||
},
|
},
|
||||||
onSelected: (AlbumSortMode value) {
|
|
||||||
final selected = albumSortOption == value;
|
|
||||||
// Switch direction
|
|
||||||
if (selected) {
|
|
||||||
ref
|
|
||||||
.read(albumSortOrderProvider.notifier)
|
|
||||||
.changeSortDirection(!albumSortIsReverse);
|
|
||||||
} else {
|
|
||||||
ref.read(albumSortByOptionsProvider.notifier).changeSortMode(value);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.only(right: 5),
|
|
||||||
child: Icon(
|
|
||||||
albumSortIsReverse
|
|
||||||
? Icons.arrow_downward_rounded
|
|
||||||
: Icons.arrow_upward_rounded,
|
|
||||||
size: 14,
|
|
||||||
color: context.primaryColor,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
albumSortOption.label.tr(),
|
|
||||||
style: context.textTheme.labelLarge?.copyWith(
|
|
||||||
color: context.primaryColor,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Widget buildCreateAlbumButton() {
|
class PeopleCollectionCard extends ConsumerWidget {
|
||||||
return LayoutBuilder(
|
const PeopleCollectionCard({super.key});
|
||||||
builder: (context, constraints) {
|
|
||||||
var cardSize = constraints.maxWidth;
|
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final people = ref.watch(getAllPeopleProvider);
|
||||||
|
final size = MediaQuery.of(context).size.width * 0.5 - 20;
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onTap: () =>
|
onTap: () => context.pushRoute(const PeopleCollectionRoute()),
|
||||||
context.pushRoute(CreateAlbumRoute(isSharedAlbum: false)),
|
|
||||||
child: Padding(
|
|
||||||
padding:
|
|
||||||
const EdgeInsets.only(bottom: 32), // Adjust padding to suit
|
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.start,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Container(
|
Container(
|
||||||
width: cardSize,
|
height: size,
|
||||||
height: cardSize,
|
width: size,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: context.colorScheme.surfaceContainer,
|
borderRadius: BorderRadius.circular(20),
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(20)),
|
gradient: LinearGradient(
|
||||||
|
colors: [
|
||||||
|
context.colorScheme.primary.withAlpha(30),
|
||||||
|
context.colorScheme.primary.withAlpha(25),
|
||||||
|
],
|
||||||
|
begin: Alignment.topCenter,
|
||||||
|
end: Alignment.bottomCenter,
|
||||||
),
|
),
|
||||||
child: Center(
|
),
|
||||||
child: Icon(
|
child: people.widgetWhen(
|
||||||
Icons.add_rounded,
|
onData: (people) {
|
||||||
size: 28,
|
return GridView.count(
|
||||||
color: context.primaryColor,
|
crossAxisCount: 2,
|
||||||
|
padding: const EdgeInsets.all(12),
|
||||||
|
crossAxisSpacing: 8,
|
||||||
|
mainAxisSpacing: 8,
|
||||||
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
|
children: people.take(4).map((person) {
|
||||||
|
return CircleAvatar(
|
||||||
|
backgroundImage: NetworkImage(
|
||||||
|
getFaceThumbnailUrl(person.id),
|
||||||
|
headers: ApiService.getRequestHeaders(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(8.0),
|
||||||
|
child: Text(
|
||||||
|
'people'.tr(),
|
||||||
|
style: context.textTheme.titleSmall?.copyWith(
|
||||||
|
color: context.colorScheme.onSurface,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class LocalAlbumsCollectionCard extends HookConsumerWidget {
|
||||||
|
const LocalAlbumsCollectionCard({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final albums = ref.watch(localAlbumsProvider);
|
||||||
|
|
||||||
|
final size = MediaQuery.of(context).size.width * 0.5 - 20;
|
||||||
|
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: () => context.pushRoute(
|
||||||
|
const LocalAlbumsRoute(),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
height: size,
|
||||||
|
width: size,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
gradient: LinearGradient(
|
||||||
|
colors: [
|
||||||
|
context.colorScheme.primary.withAlpha(30),
|
||||||
|
context.colorScheme.primary.withAlpha(25),
|
||||||
|
],
|
||||||
|
begin: Alignment.topCenter,
|
||||||
|
end: Alignment.bottomCenter,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: GridView.count(
|
||||||
|
crossAxisCount: 2,
|
||||||
|
padding: const EdgeInsets.all(12),
|
||||||
|
crossAxisSpacing: 8,
|
||||||
|
mainAxisSpacing: 8,
|
||||||
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
|
children: albums.take(4).map((album) {
|
||||||
|
return AlbumThumbnailCard(
|
||||||
|
album: album,
|
||||||
|
showTitle: false,
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(8.0),
|
||||||
|
child: Text(
|
||||||
|
'on_this_device'.tr(),
|
||||||
|
style: context.textTheme.titleSmall?.copyWith(
|
||||||
|
color: context.colorScheme.onSurface,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class PlacesCollectionCard extends StatelessWidget {
|
||||||
|
const PlacesCollectionCard({super.key});
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final size = MediaQuery.of(context).size.width * 0.5 - 20;
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: () => context.pushRoute(const PlacesCollectionRoute()),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
height: size,
|
||||||
|
width: size,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
color: context.colorScheme.secondaryContainer.withAlpha(100),
|
||||||
|
),
|
||||||
|
child: IgnorePointer(
|
||||||
|
child: MapThumbnail(
|
||||||
|
zoom: 8,
|
||||||
|
centre: const LatLng(
|
||||||
|
21.44950,
|
||||||
|
-157.91959,
|
||||||
|
),
|
||||||
|
showAttribution: false,
|
||||||
|
themeMode:
|
||||||
|
context.isDarkTheme ? ThemeMode.dark : ThemeMode.light,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(
|
padding: const EdgeInsets.all(8.0),
|
||||||
top: 8.0,
|
|
||||||
bottom: 16,
|
|
||||||
),
|
|
||||||
child: Text(
|
child: Text(
|
||||||
'library_page_new_album',
|
'places'.tr(),
|
||||||
style: context.textTheme.labelLarge?.copyWith(
|
style: context.textTheme.titleSmall?.copyWith(
|
||||||
color: context.colorScheme.onSurface,
|
color: context.colorScheme.onSurface,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
).tr(),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Widget buildLibraryNavButton(
|
class ActionButton extends StatelessWidget {
|
||||||
String label,
|
final VoidCallback onPressed;
|
||||||
IconData icon,
|
final IconData icon;
|
||||||
Function() onClick,
|
final String label;
|
||||||
) {
|
|
||||||
|
const ActionButton({
|
||||||
|
super.key,
|
||||||
|
required this.onPressed,
|
||||||
|
required this.icon,
|
||||||
|
required this.label,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
return Expanded(
|
return Expanded(
|
||||||
child: FilledButton.icon(
|
child: FilledButton.icon(
|
||||||
onPressed: onClick,
|
onPressed: onPressed,
|
||||||
label: Padding(
|
label: Padding(
|
||||||
padding: const EdgeInsets.only(left: 8.0),
|
padding: const EdgeInsets.only(left: 4.0),
|
||||||
child: Text(
|
child: Text(
|
||||||
label,
|
label,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: context.colorScheme.onSurface,
|
color: context.colorScheme.onSurface,
|
||||||
|
fontSize: 15,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
style: FilledButton.styleFrom(
|
style: FilledButton.styleFrom(
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 16),
|
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16),
|
||||||
backgroundColor: context.colorScheme.surfaceContainer,
|
backgroundColor: context.colorScheme.surfaceContainerLow,
|
||||||
alignment: Alignment.centerLeft,
|
alignment: Alignment.centerLeft,
|
||||||
shape: const RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.all(Radius.circular(20)),
|
borderRadius: const BorderRadius.all(Radius.circular(25)),
|
||||||
|
side: BorderSide(
|
||||||
|
color: context.colorScheme.onSurface.withAlpha(10),
|
||||||
|
width: 1,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
icon: Icon(
|
icon: Icon(
|
||||||
@ -179,151 +405,4 @@ class LibraryPage extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
final remote = albums.where((a) => a.isRemote).toList();
|
|
||||||
final sorted = albumSortOption.sortFn(remote, albumSortIsReverse);
|
|
||||||
final local = albums.where((a) => a.isLocal).toList();
|
|
||||||
|
|
||||||
Widget? shareTrashButton() {
|
|
||||||
return trashEnabled
|
|
||||||
? InkWell(
|
|
||||||
onTap: () => context.pushRoute(const TrashRoute()),
|
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(12)),
|
|
||||||
child: Icon(
|
|
||||||
Icons.delete_rounded,
|
|
||||||
size: 25,
|
|
||||||
semanticLabel: 'profile_drawer_trash'.tr(),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
: null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return Scaffold(
|
|
||||||
appBar: ImmichAppBar(
|
|
||||||
action: shareTrashButton(),
|
|
||||||
),
|
|
||||||
body: CustomScrollView(
|
|
||||||
slivers: [
|
|
||||||
SliverToBoxAdapter(
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.only(
|
|
||||||
left: 12.0,
|
|
||||||
right: 12.0,
|
|
||||||
top: 24.0,
|
|
||||||
bottom: 12.0,
|
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
|
||||||
children: [
|
|
||||||
buildLibraryNavButton(
|
|
||||||
"library_page_favorites".tr(), Icons.favorite_border, () {
|
|
||||||
context.navigateTo(const FavoritesRoute());
|
|
||||||
}),
|
|
||||||
const SizedBox(width: 12.0),
|
|
||||||
buildLibraryNavButton(
|
|
||||||
"library_page_archive".tr(), Icons.archive_outlined, () {
|
|
||||||
context.navigateTo(const ArchiveRoute());
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SliverToBoxAdapter(
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.only(
|
|
||||||
top: 12.0,
|
|
||||||
left: 12.0,
|
|
||||||
right: 12.0,
|
|
||||||
bottom: 20.0,
|
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
'library_page_albums',
|
|
||||||
style: context.textTheme.bodyLarge?.copyWith(
|
|
||||||
color: context.colorScheme.onSurface,
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
),
|
|
||||||
).tr(),
|
|
||||||
buildSortButton(),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SliverPadding(
|
|
||||||
padding: const EdgeInsets.all(12.0),
|
|
||||||
sliver: SliverGrid(
|
|
||||||
gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
|
|
||||||
maxCrossAxisExtent: 250,
|
|
||||||
mainAxisSpacing: 12,
|
|
||||||
crossAxisSpacing: 12,
|
|
||||||
childAspectRatio: .7,
|
|
||||||
),
|
|
||||||
delegate: SliverChildBuilderDelegate(
|
|
||||||
childCount: sorted.length + 1,
|
|
||||||
(context, index) {
|
|
||||||
if (index == 0) {
|
|
||||||
return buildCreateAlbumButton();
|
|
||||||
}
|
|
||||||
|
|
||||||
return AlbumThumbnailCard(
|
|
||||||
album: sorted[index - 1],
|
|
||||||
onTap: () => context.pushRoute(
|
|
||||||
AlbumViewerRoute(
|
|
||||||
albumId: sorted[index - 1].id,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SliverToBoxAdapter(
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.only(
|
|
||||||
top: 12.0,
|
|
||||||
left: 12.0,
|
|
||||||
right: 12.0,
|
|
||||||
bottom: 20.0,
|
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
'library_page_device_albums',
|
|
||||||
style: context.textTheme.bodyLarge?.copyWith(
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
),
|
|
||||||
).tr(),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SliverPadding(
|
|
||||||
padding: const EdgeInsets.all(12.0),
|
|
||||||
sliver: SliverGrid(
|
|
||||||
gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
|
|
||||||
maxCrossAxisExtent: 250,
|
|
||||||
mainAxisSpacing: 12,
|
|
||||||
crossAxisSpacing: 12,
|
|
||||||
childAspectRatio: .7,
|
|
||||||
),
|
|
||||||
delegate: SliverChildBuilderDelegate(
|
|
||||||
childCount: local.length,
|
|
||||||
(context, index) => AlbumThumbnailCard(
|
|
||||||
album: local[index],
|
|
||||||
onTap: () => context.pushRoute(
|
|
||||||
AlbumViewerRoute(
|
|
||||||
albumId: local[index].id,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
55
mobile/lib/pages/library/local_albums.page.dart
Normal file
55
mobile/lib/pages/library/local_albums.page.dart
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
import 'package:auto_route/auto_route.dart';
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
|
import 'package:immich_mobile/pages/common/large_leading_tile.dart';
|
||||||
|
import 'package:immich_mobile/providers/album/album.provider.dart';
|
||||||
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
|
import 'package:immich_mobile/widgets/common/immich_thumbnail.dart';
|
||||||
|
|
||||||
|
@RoutePage()
|
||||||
|
class LocalAlbumsPage extends HookConsumerWidget {
|
||||||
|
const LocalAlbumsPage({super.key});
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final albums = ref.watch(localAlbumsProvider);
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: Text('on_this_device'.tr()),
|
||||||
|
),
|
||||||
|
body: ListView.builder(
|
||||||
|
padding: const EdgeInsets.all(18.0),
|
||||||
|
itemCount: albums.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 8.0),
|
||||||
|
child: LargeLeadingTile(
|
||||||
|
leadingPadding: const EdgeInsets.only(
|
||||||
|
right: 16,
|
||||||
|
),
|
||||||
|
leading: ClipRRect(
|
||||||
|
borderRadius: const BorderRadius.all(Radius.circular(15)),
|
||||||
|
child: ImmichThumbnail(
|
||||||
|
asset: albums[index].thumbnail.value,
|
||||||
|
width: 80,
|
||||||
|
height: 80,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
title: Text(
|
||||||
|
albums[index].name,
|
||||||
|
style: context.textTheme.titleSmall?.copyWith(
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
subtitle: Text('${albums[index].assetCount} items'),
|
||||||
|
onTap: () => context
|
||||||
|
.pushRoute(AlbumViewerRoute(albumId: albums[index].id)),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -86,12 +86,10 @@ class PartnerPage extends HookConsumerWidget {
|
|||||||
children: [
|
children: [
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(left: 16.0, top: 16.0),
|
padding: const EdgeInsets.only(left: 16.0, top: 16.0),
|
||||||
child: const Text(
|
child: Text(
|
||||||
"partner_page_shared_to_title",
|
"partner_page_shared_to_title",
|
||||||
style: TextStyle(
|
style: context.textTheme.titleSmall?.copyWith(
|
||||||
fontSize: 14,
|
color: context.colorScheme.onSurface.withAlpha(200),
|
||||||
color: Colors.grey,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
),
|
||||||
).tr(),
|
).tr(),
|
||||||
),
|
),
|
||||||
@ -104,10 +102,7 @@ class PartnerPage extends HookConsumerWidget {
|
|||||||
leading: userAvatar(context, users[index]),
|
leading: userAvatar(context, users[index]),
|
||||||
title: Text(
|
title: Text(
|
||||||
users[index].email,
|
users[index].email,
|
||||||
style: const TextStyle(
|
style: context.textTheme.bodyLarge,
|
||||||
fontSize: 14,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
trailing: IconButton(
|
trailing: IconButton(
|
||||||
icon: const Icon(Icons.person_remove),
|
icon: const Icon(Icons.person_remove),
|
||||||
@ -148,7 +143,7 @@ class PartnerPage extends HookConsumerWidget {
|
|||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: const Text("partner_page_title").tr(),
|
title: const Text("partners").tr(),
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
centerTitle: false,
|
centerTitle: false,
|
||||||
actions: [
|
actions: [
|
@ -2,6 +2,7 @@ import 'package:auto_route/auto_route.dart';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
import 'package:immich_mobile/providers/multiselect.provider.dart';
|
import 'package:immich_mobile/providers/multiselect.provider.dart';
|
||||||
import 'package:immich_mobile/providers/partner.provider.dart';
|
import 'package:immich_mobile/providers/partner.provider.dart';
|
||||||
import 'package:immich_mobile/entities/user.entity.dart';
|
import 'package:immich_mobile/entities/user.entity.dart';
|
||||||
@ -22,7 +23,11 @@ class PartnerDetailPage extends HookConsumerWidget {
|
|||||||
|
|
||||||
useEffect(
|
useEffect(
|
||||||
() {
|
() {
|
||||||
ref.read(assetProvider.notifier).getAllAsset();
|
Future.microtask(
|
||||||
|
() async => {
|
||||||
|
await ref.read(assetProvider.notifier).getAllAsset(),
|
||||||
|
},
|
||||||
|
);
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
[],
|
[],
|
||||||
@ -64,19 +69,47 @@ class PartnerDetailPage extends HookConsumerWidget {
|
|||||||
title: Text(partner.name),
|
title: Text(partner.name),
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
centerTitle: false,
|
centerTitle: false,
|
||||||
actions: [
|
|
||||||
IconButton(
|
|
||||||
onPressed: toggleInTimeline,
|
|
||||||
icon: Icon(
|
|
||||||
inTimeline.value
|
|
||||||
? Icons.collections
|
|
||||||
: Icons.collections_outlined,
|
|
||||||
),
|
|
||||||
tooltip: "Show/hide photos on your main timeline",
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
body: MultiselectGrid(
|
body: MultiselectGrid(
|
||||||
|
topWidget: Padding(
|
||||||
|
padding: const EdgeInsets.only(left: 8.0, right: 8.0, top: 16.0),
|
||||||
|
child: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
border: Border.all(
|
||||||
|
color: context.colorScheme.onSurface.withAlpha(10),
|
||||||
|
width: 1,
|
||||||
|
),
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
gradient: LinearGradient(
|
||||||
|
colors: [
|
||||||
|
context.colorScheme.primary.withAlpha(10),
|
||||||
|
context.colorScheme.primary.withAlpha(15),
|
||||||
|
],
|
||||||
|
begin: Alignment.topCenter,
|
||||||
|
end: Alignment.bottomCenter,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(8.0),
|
||||||
|
child: ListTile(
|
||||||
|
title: Text(
|
||||||
|
"Show in timeline",
|
||||||
|
style: context.textTheme.titleSmall?.copyWith(
|
||||||
|
color: context.colorScheme.primary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
subtitle: Text(
|
||||||
|
"Show photos and videos from this user in your timeline",
|
||||||
|
style: context.textTheme.bodyMedium,
|
||||||
|
),
|
||||||
|
trailing: Switch(
|
||||||
|
value: inTimeline.value,
|
||||||
|
onChanged: (_) => toggleInTimeline(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
renderListProvider: assetsProvider(partner.isarId),
|
renderListProvider: assetsProvider(partner.isarId),
|
||||||
onRefresh: () => ref.read(assetProvider.notifier).getAllAsset(),
|
onRefresh: () => ref.read(assetProvider.notifier).getAllAsset(),
|
||||||
deleteEnabled: false,
|
deleteEnabled: false,
|
104
mobile/lib/pages/library/people/people_collection.page.dart
Normal file
104
mobile/lib/pages/library/people/people_collection.page.dart
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
import 'package:auto_route/auto_route.dart';
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
|
import 'package:immich_mobile/providers/search/people.provider.dart';
|
||||||
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
|
import 'package:immich_mobile/services/api.service.dart';
|
||||||
|
import 'package:immich_mobile/utils/image_url_builder.dart';
|
||||||
|
import 'package:immich_mobile/widgets/search/person_name_edit_form.dart';
|
||||||
|
|
||||||
|
@RoutePage()
|
||||||
|
class PeopleCollectionPage extends HookConsumerWidget {
|
||||||
|
const PeopleCollectionPage({super.key});
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final people = ref.watch(getAllPeopleProvider);
|
||||||
|
final headers = ApiService.getRequestHeaders();
|
||||||
|
|
||||||
|
showNameEditModel(
|
||||||
|
String personId,
|
||||||
|
String personName,
|
||||||
|
) {
|
||||||
|
return showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (BuildContext context) {
|
||||||
|
return PersonNameEditForm(personId: personId, personName: personName);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: Text('people'.tr()),
|
||||||
|
),
|
||||||
|
body: people.when(
|
||||||
|
data: (people) {
|
||||||
|
return GridView.builder(
|
||||||
|
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||||
|
crossAxisCount: 3,
|
||||||
|
childAspectRatio: 0.85,
|
||||||
|
),
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 32),
|
||||||
|
itemCount: people.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final person = people[index];
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
GestureDetector(
|
||||||
|
onTap: () {
|
||||||
|
context.pushRoute(
|
||||||
|
PersonResultRoute(
|
||||||
|
personId: person.id,
|
||||||
|
personName: person.name,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: Material(
|
||||||
|
shape: const CircleBorder(side: BorderSide.none),
|
||||||
|
elevation: 3,
|
||||||
|
child: CircleAvatar(
|
||||||
|
maxRadius: 96 / 2,
|
||||||
|
backgroundImage: NetworkImage(
|
||||||
|
getFaceThumbnailUrl(person.id),
|
||||||
|
headers: headers,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
GestureDetector(
|
||||||
|
onTap: () => showNameEditModel(person.id, person.name),
|
||||||
|
child: person.name.isEmpty
|
||||||
|
? Text(
|
||||||
|
'add_a_name'.tr(),
|
||||||
|
style: context.textTheme.titleSmall?.copyWith(
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
color: context.colorScheme.primary,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: Padding(
|
||||||
|
padding:
|
||||||
|
const EdgeInsets.symmetric(horizontal: 16.0),
|
||||||
|
child: Text(
|
||||||
|
person.name,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: context.textTheme.titleSmall?.copyWith(
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
error: (error, stack) => const Text("error"),
|
||||||
|
loading: () => const CircularProgressIndicator(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
125
mobile/lib/pages/library/places/places_collection.part.dart
Normal file
125
mobile/lib/pages/library/places/places_collection.part.dart
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
import 'package:auto_route/auto_route.dart';
|
||||||
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||||
|
import 'package:immich_mobile/entities/store.entity.dart';
|
||||||
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
|
import 'package:immich_mobile/models/search/search_filter.model.dart';
|
||||||
|
import 'package:immich_mobile/pages/common/large_leading_tile.dart';
|
||||||
|
import 'package:immich_mobile/providers/search/search_page_state.provider.dart';
|
||||||
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
|
import 'package:immich_mobile/services/api.service.dart';
|
||||||
|
import 'package:immich_mobile/widgets/map/map_thumbnail.dart';
|
||||||
|
import 'package:maplibre_gl/maplibre_gl.dart';
|
||||||
|
|
||||||
|
@RoutePage()
|
||||||
|
class PlacesCollectionPage extends HookConsumerWidget {
|
||||||
|
const PlacesCollectionPage({super.key});
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final places = ref.watch(getAllPlacesProvider);
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: Text('places'.tr()),
|
||||||
|
),
|
||||||
|
body: ListView(
|
||||||
|
shrinkWrap: true,
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(16.0),
|
||||||
|
child: SizedBox(
|
||||||
|
height: 200,
|
||||||
|
width: context.width,
|
||||||
|
child: MapThumbnail(
|
||||||
|
onTap: (_, __) => context.pushRoute(const MapRoute()),
|
||||||
|
zoom: 8,
|
||||||
|
centre: const LatLng(
|
||||||
|
21.44950,
|
||||||
|
-157.91959,
|
||||||
|
),
|
||||||
|
showAttribution: false,
|
||||||
|
themeMode:
|
||||||
|
context.isDarkTheme ? ThemeMode.dark : ThemeMode.light,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
places.when(
|
||||||
|
data: (places) {
|
||||||
|
return ListView.builder(
|
||||||
|
shrinkWrap: true,
|
||||||
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
|
itemCount: places.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final place = places[index];
|
||||||
|
|
||||||
|
return PlaceTile(id: place.id, name: place.label);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
error: (error, stask) => const Text('Error getting places'),
|
||||||
|
loading: () => Center(child: const CircularProgressIndicator()),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class PlaceTile extends StatelessWidget {
|
||||||
|
const PlaceTile({super.key, required this.id, required this.name});
|
||||||
|
|
||||||
|
final String id;
|
||||||
|
final String name;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final thumbnailUrl =
|
||||||
|
'${Store.get(StoreKey.serverEndpoint)}/assets/$id/thumbnail';
|
||||||
|
|
||||||
|
void navigateToPlace() {
|
||||||
|
context.pushRoute(
|
||||||
|
SearchInputRoute(
|
||||||
|
prefilter: SearchFilter(
|
||||||
|
people: {},
|
||||||
|
location: SearchLocationFilter(
|
||||||
|
city: name,
|
||||||
|
),
|
||||||
|
camera: SearchCameraFilter(),
|
||||||
|
date: SearchDateFilter(),
|
||||||
|
display: SearchDisplayFilters(
|
||||||
|
isNotInAlbum: false,
|
||||||
|
isArchive: false,
|
||||||
|
isFavorite: false,
|
||||||
|
),
|
||||||
|
mediaType: AssetType.other,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return LargeLeadingTile(
|
||||||
|
onTap: () => navigateToPlace(),
|
||||||
|
title: Text(
|
||||||
|
name,
|
||||||
|
style: context.textTheme.titleMedium?.copyWith(
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
leading: ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
child: CachedNetworkImage(
|
||||||
|
width: 80,
|
||||||
|
height: 80,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
imageUrl: thumbnailUrl,
|
||||||
|
httpHeaders: ApiService.getRequestHeaders(),
|
||||||
|
errorWidget: (context, url, error) =>
|
||||||
|
const Icon(Icons.image_not_supported_outlined),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -7,7 +7,6 @@ import 'package:flutter_hooks/flutter_hooks.dart';
|
|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
import 'package:immich_mobile/providers/album/album.provider.dart';
|
import 'package:immich_mobile/providers/album/album.provider.dart';
|
||||||
import 'package:immich_mobile/providers/album/shared_album.provider.dart';
|
|
||||||
import 'package:immich_mobile/providers/multiselect.provider.dart';
|
import 'package:immich_mobile/providers/multiselect.provider.dart';
|
||||||
import 'package:immich_mobile/widgets/memories/memory_lane.dart';
|
import 'package:immich_mobile/widgets/memories/memory_lane.dart';
|
||||||
import 'package:immich_mobile/providers/asset.provider.dart';
|
import 'package:immich_mobile/providers/asset.provider.dart';
|
||||||
@ -33,8 +32,7 @@ class PhotosPage extends HookConsumerWidget {
|
|||||||
() {
|
() {
|
||||||
ref.read(websocketProvider.notifier).connect();
|
ref.read(websocketProvider.notifier).connect();
|
||||||
Future(() => ref.read(assetProvider.notifier).getAllAsset());
|
Future(() => ref.read(assetProvider.notifier).getAllAsset());
|
||||||
ref.read(albumProvider.notifier).getAllAlbums();
|
Future(() => ref.read(albumProvider.notifier).refreshRemoteAlbums());
|
||||||
ref.read(sharedAlbumProvider.notifier).getAllSharedAlbums();
|
|
||||||
ref.read(serverInfoProvider.notifier).getServerInfo();
|
ref.read(serverInfoProvider.notifier).getServerInfo();
|
||||||
return;
|
return;
|
||||||
},
|
},
|
||||||
|
@ -92,6 +92,7 @@ class PersonResultPage extends HookConsumerWidget {
|
|||||||
Text(
|
Text(
|
||||||
name.value,
|
name.value,
|
||||||
style: context.textTheme.titleLarge,
|
style: context.textTheme.titleLarge,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@ -125,10 +126,12 @@ class PersonResultPage extends HookConsumerWidget {
|
|||||||
headers: ApiService.getRequestHeaders(),
|
headers: ApiService.getRequestHeaders(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Padding(
|
Expanded(
|
||||||
padding: const EdgeInsets.only(left: 16.0),
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.only(left: 16.0, right: 16.0),
|
||||||
child: buildTitleBlock(),
|
child: buildTitleBlock(),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -1,25 +1,11 @@
|
|||||||
import 'dart:math' as math;
|
|
||||||
|
|
||||||
import 'package:auto_route/auto_route.dart';
|
import 'package:auto_route/auto_route.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
|
|
||||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
import 'package:immich_mobile/extensions/theme_extensions.dart';
|
import 'package:immich_mobile/extensions/theme_extensions.dart';
|
||||||
import 'package:immich_mobile/models/search/search_curated_content.model.dart';
|
|
||||||
import 'package:immich_mobile/models/search/search_filter.model.dart';
|
|
||||||
import 'package:immich_mobile/providers/search/people.provider.dart';
|
|
||||||
import 'package:immich_mobile/providers/search/search_page_state.provider.dart';
|
|
||||||
import 'package:immich_mobile/widgets/search/curated_people_row.dart';
|
|
||||||
import 'package:immich_mobile/widgets/search/curated_places_row.dart';
|
|
||||||
import 'package:immich_mobile/widgets/search/person_name_edit_form.dart';
|
|
||||||
import 'package:immich_mobile/widgets/search/search_row_section.dart';
|
|
||||||
import 'package:immich_mobile/routing/router.dart';
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
|
||||||
import 'package:immich_mobile/providers/server_info.provider.dart';
|
|
||||||
import 'package:immich_mobile/widgets/common/immich_app_bar.dart';
|
import 'package:immich_mobile/widgets/common/immich_app_bar.dart';
|
||||||
import 'package:immich_mobile/widgets/common/scaffold_error_body.dart';
|
|
||||||
|
|
||||||
@RoutePage()
|
@RoutePage()
|
||||||
// ignore: must_be_immutable
|
// ignore: must_be_immutable
|
||||||
@ -28,12 +14,6 @@ class SearchPage extends HookConsumerWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final places = ref.watch(getPreviewPlacesProvider);
|
|
||||||
final curatedPeople = ref.watch(getAllPeopleProvider);
|
|
||||||
final isMapEnabled =
|
|
||||||
ref.watch(serverInfoProvider.select((v) => v.serverFeatures.map));
|
|
||||||
final double imageSize = math.min(context.width / 3, 150);
|
|
||||||
|
|
||||||
TextStyle categoryTitleStyle = const TextStyle(
|
TextStyle categoryTitleStyle = const TextStyle(
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
fontSize: 15.0,
|
fontSize: 15.0,
|
||||||
@ -41,87 +21,6 @@ class SearchPage extends HookConsumerWidget {
|
|||||||
|
|
||||||
Color categoryIconColor = context.colorScheme.onSurface;
|
Color categoryIconColor = context.colorScheme.onSurface;
|
||||||
|
|
||||||
showNameEditModel(
|
|
||||||
String personId,
|
|
||||||
String personName,
|
|
||||||
) {
|
|
||||||
return showDialog(
|
|
||||||
context: context,
|
|
||||||
builder: (BuildContext context) {
|
|
||||||
return PersonNameEditForm(personId: personId, personName: personName);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
buildPeople() {
|
|
||||||
return curatedPeople.widgetWhen(
|
|
||||||
onError: (error, stack) => const ScaffoldErrorBody(withIcon: false),
|
|
||||||
onData: (people) {
|
|
||||||
return SearchRowSection(
|
|
||||||
onViewAllPressed: () => context.pushRoute(const AllPeopleRoute()),
|
|
||||||
title: "search_page_people".tr(),
|
|
||||||
isEmpty: people.isEmpty,
|
|
||||||
child: CuratedPeopleRow(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
|
||||||
content: people
|
|
||||||
.map((e) => SearchCuratedContent(label: e.name, id: e.id))
|
|
||||||
.take(12)
|
|
||||||
.toList(),
|
|
||||||
onTap: (content, index) {
|
|
||||||
context.pushRoute(
|
|
||||||
PersonResultRoute(
|
|
||||||
personId: content.id,
|
|
||||||
personName: content.label,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
onNameTap: (person, index) => {
|
|
||||||
showNameEditModel(person.id, person.label),
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
buildPlaces() {
|
|
||||||
return places.widgetWhen(
|
|
||||||
onError: (error, stack) => const ScaffoldErrorBody(withIcon: false),
|
|
||||||
onData: (data) {
|
|
||||||
return SearchRowSection(
|
|
||||||
onViewAllPressed: () => context.pushRoute(const AllPlacesRoute()),
|
|
||||||
title: "search_page_places".tr(),
|
|
||||||
isEmpty: !isMapEnabled && data.isEmpty,
|
|
||||||
child: CuratedPlacesRow(
|
|
||||||
isMapEnabled: isMapEnabled,
|
|
||||||
content: data,
|
|
||||||
imageSize: imageSize,
|
|
||||||
onTap: (content, index) {
|
|
||||||
context.pushRoute(
|
|
||||||
SearchInputRoute(
|
|
||||||
prefilter: SearchFilter(
|
|
||||||
people: {},
|
|
||||||
location: SearchLocationFilter(
|
|
||||||
city: content.label,
|
|
||||||
),
|
|
||||||
camera: SearchCameraFilter(),
|
|
||||||
date: SearchDateFilter(),
|
|
||||||
display: SearchDisplayFilters(
|
|
||||||
isNotInAlbum: false,
|
|
||||||
isArchive: false,
|
|
||||||
isFavorite: false,
|
|
||||||
),
|
|
||||||
mediaType: AssetType.other,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
buildSearchButton() {
|
buildSearchButton() {
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
@ -165,20 +64,17 @@ class SearchPage extends HookConsumerWidget {
|
|||||||
body: ListView(
|
body: ListView(
|
||||||
children: [
|
children: [
|
||||||
buildSearchButton(),
|
buildSearchButton(),
|
||||||
const SizedBox(height: 8.0),
|
|
||||||
buildPeople(),
|
|
||||||
const SizedBox(height: 8.0),
|
|
||||||
buildPlaces(),
|
|
||||||
const SizedBox(height: 24.0),
|
const SizedBox(height: 24.0),
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||||
child: Text(
|
child: Text(
|
||||||
'search_page_your_activity',
|
'search_page_categories',
|
||||||
style: context.textTheme.bodyLarge?.copyWith(
|
style: context.textTheme.bodyLarge?.copyWith(
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
),
|
),
|
||||||
).tr(),
|
).tr(),
|
||||||
),
|
),
|
||||||
|
const SizedBox(height: 12.0),
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: Icon(
|
leading: Icon(
|
||||||
Icons.favorite_border_rounded,
|
Icons.favorite_border_rounded,
|
||||||
@ -200,16 +96,7 @@ class SearchPage extends HookConsumerWidget {
|
|||||||
).tr(),
|
).tr(),
|
||||||
onTap: () => context.pushRoute(const RecentlyAddedRoute()),
|
onTap: () => context.pushRoute(const RecentlyAddedRoute()),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 24.0),
|
const CategoryDivider(),
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
|
||||||
child: Text(
|
|
||||||
'search_page_categories',
|
|
||||||
style: context.textTheme.bodyLarge?.copyWith(
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
),
|
|
||||||
).tr(),
|
|
||||||
),
|
|
||||||
ListTile(
|
ListTile(
|
||||||
title: Text('search_page_videos', style: categoryTitleStyle).tr(),
|
title: Text('search_page_videos', style: categoryTitleStyle).tr(),
|
||||||
leading: Icon(
|
leading: Icon(
|
||||||
|
@ -31,6 +31,7 @@ class SearchInputPage extends HookConsumerWidget {
|
|||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final isContextualSearch = useState(true);
|
final isContextualSearch = useState(true);
|
||||||
final textSearchController = useTextEditingController();
|
final textSearchController = useTextEditingController();
|
||||||
|
final focusNode = useFocusNode();
|
||||||
final filter = useState<SearchFilter>(
|
final filter = useState<SearchFilter>(
|
||||||
SearchFilter(
|
SearchFilter(
|
||||||
people: prefilter?.people ?? {},
|
people: prefilter?.people ?? {},
|
||||||
@ -440,6 +441,10 @@ class SearchInputPage extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleTextSubmitted(String value) {
|
handleTextSubmitted(String value) {
|
||||||
|
if (value.isEmpty) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (isContextualSearch.value) {
|
if (isContextualSearch.value) {
|
||||||
filter.value = filter.value.copyWith(
|
filter.value = filter.value.copyWith(
|
||||||
context: value,
|
context: value,
|
||||||
@ -489,7 +494,9 @@ class SearchInputPage extends HookConsumerWidget {
|
|||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
automaticallyImplyLeading: true,
|
automaticallyImplyLeading: true,
|
||||||
actions: [
|
actions: [
|
||||||
IconButton(
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(right: 14.0),
|
||||||
|
child: IconButton(
|
||||||
icon: isContextualSearch.value
|
icon: isContextualSearch.value
|
||||||
? const Icon(Icons.abc_rounded)
|
? const Icon(Icons.abc_rounded)
|
||||||
: const Icon(Icons.image_search_rounded),
|
: const Icon(Icons.image_search_rounded),
|
||||||
@ -498,14 +505,35 @@ class SearchInputPage extends HookConsumerWidget {
|
|||||||
textSearchController.clear();
|
textSearchController.clear();
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
],
|
|
||||||
leading: IconButton(
|
|
||||||
icon: const Icon(Icons.arrow_back_ios_new_rounded),
|
|
||||||
onPressed: () => context.router.maybePop(),
|
|
||||||
),
|
),
|
||||||
title: TextField(
|
],
|
||||||
|
title: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
border: Border.all(
|
||||||
|
color: context.colorScheme.onSurface.withAlpha(0),
|
||||||
|
width: 0,
|
||||||
|
),
|
||||||
|
borderRadius: BorderRadius.circular(24),
|
||||||
|
gradient: LinearGradient(
|
||||||
|
colors: [
|
||||||
|
context.colorScheme.primary.withOpacity(0.075),
|
||||||
|
context.colorScheme.primary.withOpacity(0.09),
|
||||||
|
context.colorScheme.primary.withOpacity(0.075),
|
||||||
|
],
|
||||||
|
begin: Alignment.topLeft,
|
||||||
|
end: Alignment.bottomRight,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: TextField(
|
||||||
controller: textSearchController,
|
controller: textSearchController,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
|
contentPadding: EdgeInsets.all(8),
|
||||||
|
prefixIcon: prefilter != null
|
||||||
|
? null
|
||||||
|
: Icon(
|
||||||
|
Icons.search_rounded,
|
||||||
|
color: context.colorScheme.primary,
|
||||||
|
),
|
||||||
hintText: isContextualSearch.value
|
hintText: isContextualSearch.value
|
||||||
? 'contextual_search'.tr()
|
? 'contextual_search'.tr()
|
||||||
: 'filename_search'.tr(),
|
: 'filename_search'.tr(),
|
||||||
@ -513,14 +541,35 @@ class SearchInputPage extends HookConsumerWidget {
|
|||||||
color: context.themeData.colorScheme.onSurfaceSecondary,
|
color: context.themeData.colorScheme.onSurfaceSecondary,
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
),
|
),
|
||||||
enabledBorder: const UnderlineInputBorder(
|
border: OutlineInputBorder(
|
||||||
borderSide: BorderSide(color: Colors.transparent),
|
borderRadius: BorderRadius.circular(25),
|
||||||
|
borderSide: BorderSide(
|
||||||
|
color: context.colorScheme.surfaceDim,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
enabledBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(25),
|
||||||
|
borderSide: BorderSide(
|
||||||
|
color: context.colorScheme.surfaceContainer,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
disabledBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(25),
|
||||||
|
borderSide: BorderSide(
|
||||||
|
color: context.colorScheme.surfaceDim,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
focusedBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(25),
|
||||||
|
borderSide: BorderSide(
|
||||||
|
color: context.colorScheme.primary.withAlpha(100),
|
||||||
),
|
),
|
||||||
focusedBorder: const UnderlineInputBorder(
|
|
||||||
borderSide: BorderSide(color: Colors.transparent),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
onSubmitted: handleTextSubmitted,
|
onSubmitted: handleTextSubmitted,
|
||||||
|
focusNode: focusNode,
|
||||||
|
onTapOutside: (_) => focusNode.unfocus(),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
body: Column(
|
body: Column(
|
||||||
|
@ -1,283 +0,0 @@
|
|||||||
import 'package:auto_route/auto_route.dart';
|
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|
||||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
|
||||||
import 'package:immich_mobile/extensions/theme_extensions.dart';
|
|
||||||
import 'package:immich_mobile/providers/album/album_sort_by_options.provider.dart';
|
|
||||||
import 'package:immich_mobile/providers/album/shared_album.provider.dart';
|
|
||||||
import 'package:immich_mobile/widgets/album/album_thumbnail_card.dart';
|
|
||||||
import 'package:immich_mobile/providers/partner.provider.dart';
|
|
||||||
import 'package:immich_mobile/widgets/partner/partner_list.dart';
|
|
||||||
import 'package:immich_mobile/routing/router.dart';
|
|
||||||
import 'package:immich_mobile/providers/user.provider.dart';
|
|
||||||
import 'package:immich_mobile/widgets/common/immich_app_bar.dart';
|
|
||||||
import 'package:immich_mobile/widgets/common/immich_thumbnail.dart';
|
|
||||||
|
|
||||||
@RoutePage()
|
|
||||||
class SharingPage extends HookConsumerWidget {
|
|
||||||
const SharingPage({super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
|
||||||
final albumSortOption = ref.watch(albumSortByOptionsProvider);
|
|
||||||
final albumSortIsReverse = ref.watch(albumSortOrderProvider);
|
|
||||||
final albums = ref.watch(sharedAlbumProvider);
|
|
||||||
final sharedAlbums = albumSortOption.sortFn(albums, albumSortIsReverse);
|
|
||||||
final userId = ref.watch(currentUserProvider)?.id;
|
|
||||||
final partner = ref.watch(partnerSharedWithProvider);
|
|
||||||
|
|
||||||
useEffect(
|
|
||||||
() {
|
|
||||||
ref.read(sharedAlbumProvider.notifier).getAllSharedAlbums();
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
[],
|
|
||||||
);
|
|
||||||
|
|
||||||
buildAlbumGrid() {
|
|
||||||
return SliverPadding(
|
|
||||||
padding: const EdgeInsets.all(18.0),
|
|
||||||
sliver: SliverGrid(
|
|
||||||
gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
|
|
||||||
maxCrossAxisExtent: 250,
|
|
||||||
mainAxisSpacing: 12,
|
|
||||||
crossAxisSpacing: 12,
|
|
||||||
childAspectRatio: .7,
|
|
||||||
),
|
|
||||||
delegate: SliverChildBuilderDelegate(
|
|
||||||
(context, index) {
|
|
||||||
return AlbumThumbnailCard(
|
|
||||||
album: sharedAlbums[index],
|
|
||||||
showOwner: true,
|
|
||||||
onTap: () => context.pushRoute(
|
|
||||||
AlbumViewerRoute(albumId: sharedAlbums[index].id),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
childCount: sharedAlbums.length,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
buildAlbumList() {
|
|
||||||
return SliverList(
|
|
||||||
delegate: SliverChildBuilderDelegate(
|
|
||||||
(BuildContext context, int index) {
|
|
||||||
final album = sharedAlbums[index];
|
|
||||||
final isOwner = album.ownerId == userId;
|
|
||||||
|
|
||||||
return ListTile(
|
|
||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 12),
|
|
||||||
leading: ClipRRect(
|
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
|
||||||
child: ImmichThumbnail(
|
|
||||||
asset: album.thumbnail.value,
|
|
||||||
width: 60,
|
|
||||||
height: 60,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
title: Text(
|
|
||||||
album.name,
|
|
||||||
maxLines: 1,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
style: context.textTheme.bodyMedium?.copyWith(
|
|
||||||
color: context.colorScheme.onSurface,
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
subtitle: isOwner
|
|
||||||
? Text(
|
|
||||||
'album_thumbnail_owned'.tr(),
|
|
||||||
style: context.textTheme.bodyMedium?.copyWith(
|
|
||||||
color: context.colorScheme.onSurfaceSecondary,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
: album.ownerName != null
|
|
||||||
? Text(
|
|
||||||
'album_thumbnail_shared_by'
|
|
||||||
.tr(args: [album.ownerName!]),
|
|
||||||
style: context.textTheme.bodyMedium?.copyWith(
|
|
||||||
color: context.colorScheme.onSurfaceSecondary,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
: null,
|
|
||||||
onTap: () => context
|
|
||||||
.pushRoute(AlbumViewerRoute(albumId: sharedAlbums[index].id)),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
childCount: sharedAlbums.length,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
buildTopBottons() {
|
|
||||||
return Padding(
|
|
||||||
padding: const EdgeInsets.only(
|
|
||||||
left: 12.0,
|
|
||||||
right: 12.0,
|
|
||||||
top: 24.0,
|
|
||||||
bottom: 12.0,
|
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: ElevatedButton.icon(
|
|
||||||
onPressed: () =>
|
|
||||||
context.pushRoute(CreateAlbumRoute(isSharedAlbum: true)),
|
|
||||||
icon: const Icon(
|
|
||||||
Icons.photo_album_outlined,
|
|
||||||
size: 20,
|
|
||||||
),
|
|
||||||
label: const Text(
|
|
||||||
"sharing_silver_appbar_create_shared_album",
|
|
||||||
maxLines: 1,
|
|
||||||
style: TextStyle(
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
fontSize: 12,
|
|
||||||
),
|
|
||||||
).tr(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 12.0),
|
|
||||||
Expanded(
|
|
||||||
child: ElevatedButton.icon(
|
|
||||||
onPressed: () => context.pushRoute(const SharedLinkRoute()),
|
|
||||||
icon: const Icon(
|
|
||||||
Icons.link,
|
|
||||||
size: 20,
|
|
||||||
),
|
|
||||||
label: const Text(
|
|
||||||
"sharing_silver_appbar_shared_links",
|
|
||||||
style: TextStyle(
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
fontSize: 12,
|
|
||||||
),
|
|
||||||
maxLines: 1,
|
|
||||||
).tr(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
buildEmptyListIndication() {
|
|
||||||
return SliverToBoxAdapter(
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(8.0),
|
|
||||||
child: Card(
|
|
||||||
elevation: 0,
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(20)),
|
|
||||||
side: BorderSide(
|
|
||||||
color: context.isDarkTheme
|
|
||||||
? const Color(0xFF383838)
|
|
||||||
: Colors.black12,
|
|
||||||
width: 1,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(18.0),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.only(left: 5.0, bottom: 5),
|
|
||||||
child: Icon(
|
|
||||||
Icons.insert_photo_rounded,
|
|
||||||
size: 50,
|
|
||||||
color: context.primaryColor,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.all(8.0),
|
|
||||||
child: Text(
|
|
||||||
'sharing_page_empty_list',
|
|
||||||
style: context.textTheme.displaySmall,
|
|
||||||
).tr(),
|
|
||||||
),
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.all(8.0),
|
|
||||||
child: Text(
|
|
||||||
'sharing_page_description',
|
|
||||||
style: context.textTheme.bodyMedium,
|
|
||||||
).tr(),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget sharePartnerButton() {
|
|
||||||
return InkWell(
|
|
||||||
onTap: () => context.pushRoute(const PartnerRoute()),
|
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(12)),
|
|
||||||
child: Icon(
|
|
||||||
Icons.swap_horizontal_circle_rounded,
|
|
||||||
size: 25,
|
|
||||||
semanticLabel: 'partner_page_title'.tr(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return RefreshIndicator(
|
|
||||||
onRefresh: () async {
|
|
||||||
ref.read(sharedAlbumProvider.notifier).getAllSharedAlbums();
|
|
||||||
},
|
|
||||||
child: Scaffold(
|
|
||||||
appBar: ImmichAppBar(
|
|
||||||
action: sharePartnerButton(),
|
|
||||||
),
|
|
||||||
body: CustomScrollView(
|
|
||||||
slivers: [
|
|
||||||
SliverToBoxAdapter(child: buildTopBottons()),
|
|
||||||
if (partner.isNotEmpty)
|
|
||||||
SliverPadding(
|
|
||||||
padding: const EdgeInsets.all(12),
|
|
||||||
sliver: SliverToBoxAdapter(
|
|
||||||
child: Text(
|
|
||||||
"partner_page_title",
|
|
||||||
style: context.textTheme.bodyLarge?.copyWith(
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
),
|
|
||||||
).tr(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (partner.isNotEmpty) PartnerList(partner: partner),
|
|
||||||
SliverPadding(
|
|
||||||
padding: const EdgeInsets.all(12),
|
|
||||||
sliver: SliverToBoxAdapter(
|
|
||||||
child: Text(
|
|
||||||
"sharing_page_album",
|
|
||||||
style: context.textTheme.bodyLarge?.copyWith(
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
),
|
|
||||||
).tr(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SliverLayoutBuilder(
|
|
||||||
builder: (context, constraints) {
|
|
||||||
if (sharedAlbums.isEmpty) {
|
|
||||||
return buildEmptyListIndication();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (constraints.crossAxisExtent < 600) {
|
|
||||||
return buildAlbumList();
|
|
||||||
} else {
|
|
||||||
return buildAlbumGrid();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,21 +1,21 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/entities/user.entity.dart';
|
||||||
|
import 'package:immich_mobile/models/albums/album_search.model.dart';
|
||||||
import 'package:immich_mobile/services/album.service.dart';
|
import 'package:immich_mobile/services/album.service.dart';
|
||||||
import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart';
|
import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart';
|
||||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||||
import 'package:immich_mobile/entities/album.entity.dart';
|
import 'package:immich_mobile/entities/album.entity.dart';
|
||||||
import 'package:immich_mobile/entities/store.entity.dart';
|
|
||||||
import 'package:immich_mobile/entities/user.entity.dart';
|
|
||||||
import 'package:immich_mobile/providers/db.provider.dart';
|
import 'package:immich_mobile/providers/db.provider.dart';
|
||||||
import 'package:immich_mobile/utils/renderlist_generator.dart';
|
import 'package:immich_mobile/utils/renderlist_generator.dart';
|
||||||
import 'package:isar/isar.dart';
|
import 'package:isar/isar.dart';
|
||||||
|
|
||||||
|
final isRefreshingRemoteAlbumProvider = StateProvider<bool>((ref) => false);
|
||||||
|
|
||||||
class AlbumNotifier extends StateNotifier<List<Album>> {
|
class AlbumNotifier extends StateNotifier<List<Album>> {
|
||||||
AlbumNotifier(this._albumService, Isar db) : super([]) {
|
AlbumNotifier(this._albumService, this.db, this.ref) : super([]) {
|
||||||
final query = db.albums
|
final query = db.albums.filter().remoteIdIsNotNull();
|
||||||
.filter()
|
|
||||||
.owner((q) => q.isarIdEqualTo(Store.get(StoreKey.currentUser).isarId));
|
|
||||||
query.findAll().then((value) {
|
query.findAll().then((value) {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
state = value;
|
state = value;
|
||||||
@ -25,14 +25,17 @@ class AlbumNotifier extends StateNotifier<List<Album>> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final AlbumService _albumService;
|
final AlbumService _albumService;
|
||||||
|
final Isar db;
|
||||||
|
final Ref ref;
|
||||||
late final StreamSubscription<List<Album>> _streamSub;
|
late final StreamSubscription<List<Album>> _streamSub;
|
||||||
|
|
||||||
Future<void> getAllAlbums() => Future.wait([
|
Future<void> refreshRemoteAlbums() async {
|
||||||
_albumService.refreshDeviceAlbums(),
|
ref.read(isRefreshingRemoteAlbumProvider.notifier).state = true;
|
||||||
_albumService.refreshRemoteAlbums(isShared: false),
|
await _albumService.refreshRemoteAlbums();
|
||||||
]);
|
ref.read(isRefreshingRemoteAlbumProvider.notifier).state = false;
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> getDeviceAlbums() => _albumService.refreshDeviceAlbums();
|
Future<void> refreshDeviceAlbums() => _albumService.refreshDeviceAlbums();
|
||||||
|
|
||||||
Future<bool> deleteAlbum(Album album) => _albumService.deleteAlbum(album);
|
Future<bool> deleteAlbum(Album album) => _albumService.deleteAlbum(album);
|
||||||
|
|
||||||
@ -59,6 +62,50 @@ class AlbumNotifier extends StateNotifier<List<Album>> {
|
|||||||
await createAlbum(albumName, {});
|
await createAlbum(albumName, {});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<bool> leaveAlbum(Album album) async {
|
||||||
|
var res = await _albumService.leaveAlbum(album);
|
||||||
|
|
||||||
|
if (res) {
|
||||||
|
await deleteAlbum(album);
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void searchAlbums(String searchTerm, QuickFilterMode filterMode) async {
|
||||||
|
state = await _albumService.search(searchTerm, filterMode);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> addUsers(Album album, List<String> userIds) async {
|
||||||
|
await _albumService.addUsers(album, userIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> removeUser(Album album, User user) async {
|
||||||
|
final isRemoved = await _albumService.removeUser(album, user);
|
||||||
|
|
||||||
|
if (isRemoved && album.sharedUsers.isEmpty) {
|
||||||
|
state = state.where((element) => element.id != album.id).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
return isRemoved;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> addAssets(Album album, Iterable<Asset> assets) async {
|
||||||
|
await _albumService.addAssets(album, assets);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> removeAsset(Album album, Iterable<Asset> assets) async {
|
||||||
|
return await _albumService.removeAsset(album, assets);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> setActivitystatus(
|
||||||
|
Album album,
|
||||||
|
bool enabled,
|
||||||
|
) {
|
||||||
|
return _albumService.setActivityStatus(album, enabled);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_streamSub.cancel();
|
_streamSub.cancel();
|
||||||
@ -71,6 +118,7 @@ final albumProvider =
|
|||||||
return AlbumNotifier(
|
return AlbumNotifier(
|
||||||
ref.watch(albumServiceProvider),
|
ref.watch(albumServiceProvider),
|
||||||
ref.watch(dbProvider),
|
ref.watch(dbProvider),
|
||||||
|
ref,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -94,3 +142,31 @@ final albumRenderlistProvider =
|
|||||||
}
|
}
|
||||||
return const Stream.empty();
|
return const Stream.empty();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
class LocalAlbumsNotifier extends StateNotifier<List<Album>> {
|
||||||
|
LocalAlbumsNotifier(this.db) : super([]) {
|
||||||
|
final query = db.albums.where().remoteIdIsNull();
|
||||||
|
|
||||||
|
query.findAll().then((value) {
|
||||||
|
if (mounted) {
|
||||||
|
state = value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
_streamSub = query.watch().listen((data) => state = data);
|
||||||
|
}
|
||||||
|
|
||||||
|
final Isar db;
|
||||||
|
late final StreamSubscription<List<Album>> _streamSub;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_streamSub.cancel();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final localAlbumsProvider =
|
||||||
|
StateNotifierProvider.autoDispose<LocalAlbumsNotifier, List<Album>>((ref) {
|
||||||
|
return LocalAlbumsNotifier(ref.watch(dbProvider));
|
||||||
|
});
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/models/albums/album_viewer_page_state.model.dart';
|
import 'package:immich_mobile/models/albums/album_viewer_page_state.model.dart';
|
||||||
import 'package:immich_mobile/providers/album/shared_album.provider.dart';
|
|
||||||
import 'package:immich_mobile/services/album.service.dart';
|
import 'package:immich_mobile/services/album.service.dart';
|
||||||
import 'package:immich_mobile/entities/album.entity.dart';
|
import 'package:immich_mobile/entities/album.entity.dart';
|
||||||
|
|
||||||
@ -40,7 +39,6 @@ class AlbumViewerNotifier extends StateNotifier<AlbumViewerPageState> {
|
|||||||
|
|
||||||
if (isSuccess) {
|
if (isSuccess) {
|
||||||
state = state.copyWith(editTitleText: "", isEditAlbum: false);
|
state = state.copyWith(editTitleText: "", isEditAlbum: false);
|
||||||
ref.read(sharedAlbumProvider.notifier).getAllSharedAlbums();
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@ -1,90 +0,0 @@
|
|||||||
import 'dart:async';
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|
||||||
import 'package:immich_mobile/services/album.service.dart';
|
|
||||||
import 'package:immich_mobile/entities/album.entity.dart';
|
|
||||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
|
||||||
import 'package:immich_mobile/entities/user.entity.dart';
|
|
||||||
import 'package:immich_mobile/providers/db.provider.dart';
|
|
||||||
import 'package:isar/isar.dart';
|
|
||||||
|
|
||||||
class SharedAlbumNotifier extends StateNotifier<List<Album>> {
|
|
||||||
SharedAlbumNotifier(this._albumService, Isar db) : super([]) {
|
|
||||||
final query = db.albums.filter().sharedEqualTo(true).sortByCreatedAtDesc();
|
|
||||||
query.findAll().then((value) {
|
|
||||||
if (mounted) {
|
|
||||||
state = value;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
_streamSub = query.watch().listen((data) => state = data);
|
|
||||||
}
|
|
||||||
|
|
||||||
final AlbumService _albumService;
|
|
||||||
late final StreamSubscription<List<Album>> _streamSub;
|
|
||||||
|
|
||||||
Future<Album?> createSharedAlbum(
|
|
||||||
String albumName,
|
|
||||||
Iterable<Asset> assets,
|
|
||||||
Iterable<User> sharedUsers,
|
|
||||||
) async {
|
|
||||||
try {
|
|
||||||
return await _albumService.createAlbum(
|
|
||||||
albumName,
|
|
||||||
assets,
|
|
||||||
sharedUsers,
|
|
||||||
);
|
|
||||||
} catch (e) {
|
|
||||||
debugPrint("Error createSharedAlbum ${e.toString()}");
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> getAllSharedAlbums() =>
|
|
||||||
_albumService.refreshRemoteAlbums(isShared: true);
|
|
||||||
|
|
||||||
Future<bool> deleteAlbum(Album album) => _albumService.deleteAlbum(album);
|
|
||||||
|
|
||||||
Future<bool> leaveAlbum(Album album) async {
|
|
||||||
var res = await _albumService.leaveAlbum(album);
|
|
||||||
|
|
||||||
if (res) {
|
|
||||||
await deleteAlbum(album);
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<bool> removeAssetFromAlbum(Album album, Iterable<Asset> assets) {
|
|
||||||
return _albumService.removeAssetFromAlbum(album, assets);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<bool> removeUserFromAlbum(Album album, User user) async {
|
|
||||||
final result = await _albumService.removeUserFromAlbum(album, user);
|
|
||||||
|
|
||||||
if (result && album.sharedUsers.isEmpty) {
|
|
||||||
state = state.where((element) => element.id != album.id).toList();
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<bool> setActivityEnabled(Album album, bool activityEnabled) {
|
|
||||||
return _albumService.setActivityEnabled(album, activityEnabled);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
_streamSub.cancel();
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
final sharedAlbumProvider =
|
|
||||||
StateNotifierProvider.autoDispose<SharedAlbumNotifier, List<Album>>((ref) {
|
|
||||||
return SharedAlbumNotifier(
|
|
||||||
ref.watch(albumServiceProvider),
|
|
||||||
ref.watch(dbProvider),
|
|
||||||
);
|
|
||||||
});
|
|
@ -1,6 +1,5 @@
|
|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/providers/album/album.provider.dart';
|
import 'package:immich_mobile/providers/album/album.provider.dart';
|
||||||
import 'package:immich_mobile/providers/album/shared_album.provider.dart';
|
|
||||||
import 'package:immich_mobile/services/background.service.dart';
|
import 'package:immich_mobile/services/background.service.dart';
|
||||||
import 'package:immich_mobile/models/backup/backup_state.model.dart';
|
import 'package:immich_mobile/models/backup/backup_state.model.dart';
|
||||||
import 'package:immich_mobile/providers/backup/backup.provider.dart';
|
import 'package:immich_mobile/providers/backup/backup.provider.dart';
|
||||||
@ -58,11 +57,10 @@ class AppLifeCycleNotifier extends StateNotifier<AppLifeCycleEnum> {
|
|||||||
_ref.read(assetProvider.notifier).getAllAsset();
|
_ref.read(assetProvider.notifier).getAllAsset();
|
||||||
case TabEnum.search:
|
case TabEnum.search:
|
||||||
// nothing to do
|
// nothing to do
|
||||||
case TabEnum.sharing:
|
case TabEnum.albums:
|
||||||
_ref.read(assetProvider.notifier).getAllAsset();
|
_ref.read(albumProvider.notifier).refreshRemoteAlbums();
|
||||||
_ref.read(sharedAlbumProvider.notifier).getAllSharedAlbums();
|
|
||||||
case TabEnum.library:
|
case TabEnum.library:
|
||||||
_ref.read(albumProvider.notifier).getAllAlbums();
|
// nothing to do
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -5,7 +5,6 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_udid/flutter_udid.dart';
|
import 'package:flutter_udid/flutter_udid.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/providers/album/album.provider.dart';
|
import 'package:immich_mobile/providers/album/album.provider.dart';
|
||||||
import 'package:immich_mobile/providers/album/shared_album.provider.dart';
|
|
||||||
import 'package:immich_mobile/entities/store.entity.dart';
|
import 'package:immich_mobile/entities/store.entity.dart';
|
||||||
import 'package:immich_mobile/models/authentication/authentication_state.model.dart';
|
import 'package:immich_mobile/models/authentication/authentication_state.model.dart';
|
||||||
import 'package:immich_mobile/entities/user.entity.dart';
|
import 'package:immich_mobile/entities/user.entity.dart';
|
||||||
@ -115,7 +114,6 @@ class AuthenticationNotifier extends StateNotifier<AuthenticationState> {
|
|||||||
Store.delete(StoreKey.accessToken),
|
Store.delete(StoreKey.accessToken),
|
||||||
]);
|
]);
|
||||||
_ref.invalidate(albumProvider);
|
_ref.invalidate(albumProvider);
|
||||||
_ref.invalidate(sharedAlbumProvider);
|
|
||||||
|
|
||||||
state = state.copyWith(
|
state = state.copyWith(
|
||||||
deviceId: "",
|
deviceId: "",
|
||||||
|
@ -7,7 +7,7 @@ part of 'backup_verification.provider.dart';
|
|||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
String _$backupVerificationHash() =>
|
String _$backupVerificationHash() =>
|
||||||
r'b691e0cc27856eef189258d3c102cc73ce4812a4';
|
r'021dfdf65e1903c932e4a1c14967b786dd3516fb';
|
||||||
|
|
||||||
/// See also [BackupVerification].
|
/// See also [BackupVerification].
|
||||||
@ProviderFor(BackupVerification)
|
@ProviderFor(BackupVerification)
|
||||||
|
@ -1,11 +1,6 @@
|
|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
|
||||||
enum TabEnum {
|
enum TabEnum { home, search, albums, library }
|
||||||
home,
|
|
||||||
search,
|
|
||||||
sharing,
|
|
||||||
library,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Provides the currently active tab
|
/// Provides the currently active tab
|
||||||
final tabProvider = StateProvider<TabEnum>(
|
final tabProvider = StateProvider<TabEnum>(
|
||||||
|
@ -167,6 +167,6 @@ final trashedAssetsProvider = StreamProvider<RenderList>((ref) {
|
|||||||
.filter()
|
.filter()
|
||||||
.ownerIdEqualTo(user.isarId)
|
.ownerIdEqualTo(user.isarId)
|
||||||
.isTrashedEqualTo(true)
|
.isTrashedEqualTo(true)
|
||||||
.sortByFileCreatedAt();
|
.sortByFileCreatedAtDesc();
|
||||||
return renderListGeneratorWithGroupBy(query, GroupAssetsBy.none);
|
return renderListGeneratorWithGroupBy(query, GroupAssetsBy.none);
|
||||||
});
|
});
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/entities/album.entity.dart';
|
import 'package:immich_mobile/entities/album.entity.dart';
|
||||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||||
|
import 'package:immich_mobile/entities/store.entity.dart';
|
||||||
import 'package:immich_mobile/entities/user.entity.dart';
|
import 'package:immich_mobile/entities/user.entity.dart';
|
||||||
import 'package:immich_mobile/interfaces/album.interface.dart';
|
import 'package:immich_mobile/interfaces/album.interface.dart';
|
||||||
|
import 'package:immich_mobile/models/albums/album_search.model.dart';
|
||||||
import 'package:immich_mobile/providers/db.provider.dart';
|
import 'package:immich_mobile/providers/db.provider.dart';
|
||||||
import 'package:immich_mobile/repositories/database.repository.dart';
|
import 'package:immich_mobile/repositories/database.repository.dart';
|
||||||
import 'package:isar/isar.dart';
|
import 'package:isar/isar.dart';
|
||||||
@ -118,4 +120,33 @@ class AlbumRepository extends DatabaseRepository implements IAlbumRepository {
|
|||||||
@override
|
@override
|
||||||
Future<void> deleteAllLocal() =>
|
Future<void> deleteAllLocal() =>
|
||||||
txn(() => db.albums.where().localIdIsNotNull().deleteAll());
|
txn(() => db.albums.where().localIdIsNotNull().deleteAll());
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<List<Album>> search(
|
||||||
|
String searchTerm,
|
||||||
|
QuickFilterMode filterMode,
|
||||||
|
) async {
|
||||||
|
var query = db.albums
|
||||||
|
.filter()
|
||||||
|
.nameContains(searchTerm, caseSensitive: false)
|
||||||
|
.remoteIdIsNotNull();
|
||||||
|
|
||||||
|
switch (filterMode) {
|
||||||
|
case QuickFilterMode.sharedWithMe:
|
||||||
|
query = query.owner(
|
||||||
|
(q) => q.not().isarIdEqualTo(Store.get(StoreKey.currentUser).isarId),
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case QuickFilterMode.myAlbums:
|
||||||
|
query = query.owner(
|
||||||
|
(q) => q.isarIdEqualTo(Store.get(StoreKey.currentUser).isarId),
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case QuickFilterMode.all:
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return await query.findAll();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -36,7 +36,7 @@ class PartnerApiRepository extends ApiRepository
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> delete(String id) => checkNull(_api.removePartner(id));
|
Future<void> delete(String id) => _api.removePartner(id);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<User> update(String id, {required bool inTimeline}) async {
|
Future<User> update(String id, {required bool inTimeline}) async {
|
||||||
|
@ -13,6 +13,11 @@ import 'package:immich_mobile/pages/backup/backup_album_selection.page.dart';
|
|||||||
import 'package:immich_mobile/pages/backup/backup_controller.page.dart';
|
import 'package:immich_mobile/pages/backup/backup_controller.page.dart';
|
||||||
import 'package:immich_mobile/pages/backup/backup_options.page.dart';
|
import 'package:immich_mobile/pages/backup/backup_options.page.dart';
|
||||||
import 'package:immich_mobile/pages/backup/failed_backup_status.page.dart';
|
import 'package:immich_mobile/pages/backup/failed_backup_status.page.dart';
|
||||||
|
import 'package:immich_mobile/pages/albums/albums.page.dart';
|
||||||
|
import 'package:immich_mobile/pages/library/local_albums.page.dart';
|
||||||
|
import 'package:immich_mobile/pages/library/people/people_collection.page.dart';
|
||||||
|
import 'package:immich_mobile/pages/library/places/places_collection.part.dart';
|
||||||
|
import 'package:immich_mobile/pages/library/library.page.dart';
|
||||||
import 'package:immich_mobile/pages/common/activities.page.dart';
|
import 'package:immich_mobile/pages/common/activities.page.dart';
|
||||||
import 'package:immich_mobile/pages/common/album_additional_shared_user_selection.page.dart';
|
import 'package:immich_mobile/pages/common/album_additional_shared_user_selection.page.dart';
|
||||||
import 'package:immich_mobile/pages/common/album_asset_selection.page.dart';
|
import 'package:immich_mobile/pages/common/album_asset_selection.page.dart';
|
||||||
@ -32,7 +37,6 @@ import 'package:immich_mobile/pages/editing/crop.page.dart';
|
|||||||
import 'package:immich_mobile/pages/editing/filter.page.dart';
|
import 'package:immich_mobile/pages/editing/filter.page.dart';
|
||||||
import 'package:immich_mobile/pages/library/archive.page.dart';
|
import 'package:immich_mobile/pages/library/archive.page.dart';
|
||||||
import 'package:immich_mobile/pages/library/favorite.page.dart';
|
import 'package:immich_mobile/pages/library/favorite.page.dart';
|
||||||
import 'package:immich_mobile/pages/library/library.page.dart';
|
|
||||||
import 'package:immich_mobile/pages/library/trash.page.dart';
|
import 'package:immich_mobile/pages/library/trash.page.dart';
|
||||||
import 'package:immich_mobile/pages/login/change_password.page.dart';
|
import 'package:immich_mobile/pages/login/change_password.page.dart';
|
||||||
import 'package:immich_mobile/pages/login/login.page.dart';
|
import 'package:immich_mobile/pages/login/login.page.dart';
|
||||||
@ -49,11 +53,10 @@ import 'package:immich_mobile/pages/search/person_result.page.dart';
|
|||||||
import 'package:immich_mobile/pages/search/recently_added.page.dart';
|
import 'package:immich_mobile/pages/search/recently_added.page.dart';
|
||||||
import 'package:immich_mobile/pages/search/search.page.dart';
|
import 'package:immich_mobile/pages/search/search.page.dart';
|
||||||
import 'package:immich_mobile/pages/search/search_input.page.dart';
|
import 'package:immich_mobile/pages/search/search_input.page.dart';
|
||||||
import 'package:immich_mobile/pages/sharing/partner/partner.page.dart';
|
import 'package:immich_mobile/pages/library/partner/partner.page.dart';
|
||||||
import 'package:immich_mobile/pages/sharing/partner/partner_detail.page.dart';
|
import 'package:immich_mobile/pages/library/partner/partner_detail.page.dart';
|
||||||
import 'package:immich_mobile/pages/sharing/shared_link/shared_link.page.dart';
|
import 'package:immich_mobile/pages/library/shared_link/shared_link.page.dart';
|
||||||
import 'package:immich_mobile/pages/sharing/shared_link/shared_link_edit.page.dart';
|
import 'package:immich_mobile/pages/library/shared_link/shared_link_edit.page.dart';
|
||||||
import 'package:immich_mobile/pages/sharing/sharing.page.dart';
|
|
||||||
import 'package:immich_mobile/providers/api.provider.dart';
|
import 'package:immich_mobile/providers/api.provider.dart';
|
||||||
import 'package:immich_mobile/providers/gallery_permission.provider.dart';
|
import 'package:immich_mobile/providers/gallery_permission.provider.dart';
|
||||||
import 'package:immich_mobile/routing/auth_guard.dart';
|
import 'package:immich_mobile/routing/auth_guard.dart';
|
||||||
@ -103,17 +106,18 @@ class AppRouter extends RootStackRouter {
|
|||||||
guards: [_authGuard, _duplicateGuard],
|
guards: [_authGuard, _duplicateGuard],
|
||||||
),
|
),
|
||||||
AutoRoute(
|
AutoRoute(
|
||||||
page: SearchRoute.page,
|
page: SearchInputRoute.page,
|
||||||
guards: [_authGuard, _duplicateGuard],
|
|
||||||
),
|
|
||||||
AutoRoute(
|
|
||||||
page: SharingRoute.page,
|
|
||||||
guards: [_authGuard, _duplicateGuard],
|
guards: [_authGuard, _duplicateGuard],
|
||||||
|
maintainState: false,
|
||||||
),
|
),
|
||||||
AutoRoute(
|
AutoRoute(
|
||||||
page: LibraryRoute.page,
|
page: LibraryRoute.page,
|
||||||
guards: [_authGuard, _duplicateGuard],
|
guards: [_authGuard, _duplicateGuard],
|
||||||
),
|
),
|
||||||
|
AutoRoute(
|
||||||
|
page: AlbumsRoute.page,
|
||||||
|
guards: [_authGuard, _duplicateGuard],
|
||||||
|
),
|
||||||
],
|
],
|
||||||
transitionsBuilder: TransitionsBuilders.fadeIn,
|
transitionsBuilder: TransitionsBuilders.fadeIn,
|
||||||
),
|
),
|
||||||
@ -137,7 +141,11 @@ class AppRouter extends RootStackRouter {
|
|||||||
AutoRoute(page: EditImageRoute.page),
|
AutoRoute(page: EditImageRoute.page),
|
||||||
AutoRoute(page: CropImageRoute.page),
|
AutoRoute(page: CropImageRoute.page),
|
||||||
AutoRoute(page: FilterImageRoute.page),
|
AutoRoute(page: FilterImageRoute.page),
|
||||||
AutoRoute(page: FavoritesRoute.page, guards: [_authGuard, _duplicateGuard]),
|
CustomRoute(
|
||||||
|
page: FavoritesRoute.page,
|
||||||
|
guards: [_authGuard, _duplicateGuard],
|
||||||
|
transitionsBuilder: TransitionsBuilders.slideLeft,
|
||||||
|
),
|
||||||
AutoRoute(page: AllVideosRoute.page, guards: [_authGuard, _duplicateGuard]),
|
AutoRoute(page: AllVideosRoute.page, guards: [_authGuard, _duplicateGuard]),
|
||||||
AutoRoute(
|
AutoRoute(
|
||||||
page: AllMotionPhotosRoute.page,
|
page: AllMotionPhotosRoute.page,
|
||||||
@ -183,8 +191,16 @@ class AppRouter extends RootStackRouter {
|
|||||||
AutoRoute(page: SettingsSubRoute.page, guards: [_duplicateGuard]),
|
AutoRoute(page: SettingsSubRoute.page, guards: [_duplicateGuard]),
|
||||||
AutoRoute(page: AppLogRoute.page, guards: [_duplicateGuard]),
|
AutoRoute(page: AppLogRoute.page, guards: [_duplicateGuard]),
|
||||||
AutoRoute(page: AppLogDetailRoute.page, guards: [_duplicateGuard]),
|
AutoRoute(page: AppLogDetailRoute.page, guards: [_duplicateGuard]),
|
||||||
AutoRoute(page: ArchiveRoute.page, guards: [_authGuard, _duplicateGuard]),
|
CustomRoute(
|
||||||
AutoRoute(page: PartnerRoute.page, guards: [_authGuard, _duplicateGuard]),
|
page: ArchiveRoute.page,
|
||||||
|
guards: [_authGuard, _duplicateGuard],
|
||||||
|
transitionsBuilder: TransitionsBuilders.slideLeft,
|
||||||
|
),
|
||||||
|
CustomRoute(
|
||||||
|
page: PartnerRoute.page,
|
||||||
|
guards: [_authGuard, _duplicateGuard],
|
||||||
|
transitionsBuilder: TransitionsBuilders.slideLeft,
|
||||||
|
),
|
||||||
AutoRoute(
|
AutoRoute(
|
||||||
page: PartnerDetailRoute.page,
|
page: PartnerDetailRoute.page,
|
||||||
guards: [_authGuard, _duplicateGuard],
|
guards: [_authGuard, _duplicateGuard],
|
||||||
@ -200,10 +216,15 @@ class AppRouter extends RootStackRouter {
|
|||||||
page: AlbumOptionsRoute.page,
|
page: AlbumOptionsRoute.page,
|
||||||
guards: [_authGuard, _duplicateGuard],
|
guards: [_authGuard, _duplicateGuard],
|
||||||
),
|
),
|
||||||
AutoRoute(page: TrashRoute.page, guards: [_authGuard, _duplicateGuard]),
|
CustomRoute(
|
||||||
AutoRoute(
|
page: TrashRoute.page,
|
||||||
|
guards: [_authGuard, _duplicateGuard],
|
||||||
|
transitionsBuilder: TransitionsBuilders.slideLeft,
|
||||||
|
),
|
||||||
|
CustomRoute(
|
||||||
page: SharedLinkRoute.page,
|
page: SharedLinkRoute.page,
|
||||||
guards: [_authGuard, _duplicateGuard],
|
guards: [_authGuard, _duplicateGuard],
|
||||||
|
transitionsBuilder: TransitionsBuilders.slideLeft,
|
||||||
),
|
),
|
||||||
AutoRoute(
|
AutoRoute(
|
||||||
page: SharedLinkEditRoute.page,
|
page: SharedLinkEditRoute.page,
|
||||||
@ -232,6 +253,26 @@ class AppRouter extends RootStackRouter {
|
|||||||
page: HeaderSettingsRoute.page,
|
page: HeaderSettingsRoute.page,
|
||||||
guards: [_duplicateGuard],
|
guards: [_duplicateGuard],
|
||||||
),
|
),
|
||||||
|
CustomRoute(
|
||||||
|
page: PeopleCollectionRoute.page,
|
||||||
|
guards: [_authGuard, _duplicateGuard],
|
||||||
|
transitionsBuilder: TransitionsBuilders.slideLeft,
|
||||||
|
),
|
||||||
|
CustomRoute(
|
||||||
|
page: AlbumsRoute.page,
|
||||||
|
guards: [_authGuard, _duplicateGuard],
|
||||||
|
transitionsBuilder: TransitionsBuilders.slideLeft,
|
||||||
|
),
|
||||||
|
CustomRoute(
|
||||||
|
page: LocalAlbumsRoute.page,
|
||||||
|
guards: [_authGuard, _duplicateGuard],
|
||||||
|
transitionsBuilder: TransitionsBuilders.slideLeft,
|
||||||
|
),
|
||||||
|
CustomRoute(
|
||||||
|
page: PlacesCollectionRoute.page,
|
||||||
|
guards: [_authGuard, _duplicateGuard],
|
||||||
|
transitionsBuilder: TransitionsBuilders.slideLeft,
|
||||||
|
),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -319,6 +319,25 @@ class AlbumViewerRouteArgs {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// generated route for
|
||||||
|
/// [AlbumsPage]
|
||||||
|
class AlbumsRoute extends PageRouteInfo<void> {
|
||||||
|
const AlbumsRoute({List<PageRouteInfo>? children})
|
||||||
|
: super(
|
||||||
|
AlbumsRoute.name,
|
||||||
|
initialChildren: children,
|
||||||
|
);
|
||||||
|
|
||||||
|
static const String name = 'AlbumsRoute';
|
||||||
|
|
||||||
|
static PageInfo page = PageInfo(
|
||||||
|
name,
|
||||||
|
builder: (data) {
|
||||||
|
return const AlbumsPage();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/// generated route for
|
/// generated route for
|
||||||
/// [AllMotionPhotosPage]
|
/// [AllMotionPhotosPage]
|
||||||
class AllMotionPhotosRoute extends PageRouteInfo<void> {
|
class AllMotionPhotosRoute extends PageRouteInfo<void> {
|
||||||
@ -560,15 +579,13 @@ class ChangePasswordRoute extends PageRouteInfo<void> {
|
|||||||
class CreateAlbumRoute extends PageRouteInfo<CreateAlbumRouteArgs> {
|
class CreateAlbumRoute extends PageRouteInfo<CreateAlbumRouteArgs> {
|
||||||
CreateAlbumRoute({
|
CreateAlbumRoute({
|
||||||
Key? key,
|
Key? key,
|
||||||
required bool isSharedAlbum,
|
List<Asset>? assets,
|
||||||
List<Asset>? initialAssets,
|
|
||||||
List<PageRouteInfo>? children,
|
List<PageRouteInfo>? children,
|
||||||
}) : super(
|
}) : super(
|
||||||
CreateAlbumRoute.name,
|
CreateAlbumRoute.name,
|
||||||
args: CreateAlbumRouteArgs(
|
args: CreateAlbumRouteArgs(
|
||||||
key: key,
|
key: key,
|
||||||
isSharedAlbum: isSharedAlbum,
|
assets: assets,
|
||||||
initialAssets: initialAssets,
|
|
||||||
),
|
),
|
||||||
initialChildren: children,
|
initialChildren: children,
|
||||||
);
|
);
|
||||||
@ -578,11 +595,11 @@ class CreateAlbumRoute extends PageRouteInfo<CreateAlbumRouteArgs> {
|
|||||||
static PageInfo page = PageInfo(
|
static PageInfo page = PageInfo(
|
||||||
name,
|
name,
|
||||||
builder: (data) {
|
builder: (data) {
|
||||||
final args = data.argsAs<CreateAlbumRouteArgs>();
|
final args = data.argsAs<CreateAlbumRouteArgs>(
|
||||||
|
orElse: () => const CreateAlbumRouteArgs());
|
||||||
return CreateAlbumPage(
|
return CreateAlbumPage(
|
||||||
key: args.key,
|
key: args.key,
|
||||||
isSharedAlbum: args.isSharedAlbum,
|
assets: args.assets,
|
||||||
initialAssets: args.initialAssets,
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@ -591,19 +608,16 @@ class CreateAlbumRoute extends PageRouteInfo<CreateAlbumRouteArgs> {
|
|||||||
class CreateAlbumRouteArgs {
|
class CreateAlbumRouteArgs {
|
||||||
const CreateAlbumRouteArgs({
|
const CreateAlbumRouteArgs({
|
||||||
this.key,
|
this.key,
|
||||||
required this.isSharedAlbum,
|
this.assets,
|
||||||
this.initialAssets,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
final Key? key;
|
final Key? key;
|
||||||
|
|
||||||
final bool isSharedAlbum;
|
final List<Asset>? assets;
|
||||||
|
|
||||||
final List<Asset>? initialAssets;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return 'CreateAlbumRouteArgs{key: $key, isSharedAlbum: $isSharedAlbum, initialAssets: $initialAssets}';
|
return 'CreateAlbumRouteArgs{key: $key, assets: $assets}';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -909,6 +923,25 @@ class LibraryRoute extends PageRouteInfo<void> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// generated route for
|
||||||
|
/// [LocalAlbumsPage]
|
||||||
|
class LocalAlbumsRoute extends PageRouteInfo<void> {
|
||||||
|
const LocalAlbumsRoute({List<PageRouteInfo>? children})
|
||||||
|
: super(
|
||||||
|
LocalAlbumsRoute.name,
|
||||||
|
initialChildren: children,
|
||||||
|
);
|
||||||
|
|
||||||
|
static const String name = 'LocalAlbumsRoute';
|
||||||
|
|
||||||
|
static PageInfo page = PageInfo(
|
||||||
|
name,
|
||||||
|
builder: (data) {
|
||||||
|
return const LocalAlbumsPage();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/// generated route for
|
/// generated route for
|
||||||
/// [LoginPage]
|
/// [LoginPage]
|
||||||
class LoginRoute extends PageRouteInfo<void> {
|
class LoginRoute extends PageRouteInfo<void> {
|
||||||
@ -1111,6 +1144,25 @@ class PartnerRoute extends PageRouteInfo<void> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// generated route for
|
||||||
|
/// [PeopleCollectionPage]
|
||||||
|
class PeopleCollectionRoute extends PageRouteInfo<void> {
|
||||||
|
const PeopleCollectionRoute({List<PageRouteInfo>? children})
|
||||||
|
: super(
|
||||||
|
PeopleCollectionRoute.name,
|
||||||
|
initialChildren: children,
|
||||||
|
);
|
||||||
|
|
||||||
|
static const String name = 'PeopleCollectionRoute';
|
||||||
|
|
||||||
|
static PageInfo page = PageInfo(
|
||||||
|
name,
|
||||||
|
builder: (data) {
|
||||||
|
return const PeopleCollectionPage();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/// generated route for
|
/// generated route for
|
||||||
/// [PermissionOnboardingPage]
|
/// [PermissionOnboardingPage]
|
||||||
class PermissionOnboardingRoute extends PageRouteInfo<void> {
|
class PermissionOnboardingRoute extends PageRouteInfo<void> {
|
||||||
@ -1201,6 +1253,25 @@ class PhotosRoute extends PageRouteInfo<void> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// generated route for
|
||||||
|
/// [PlacesCollectionPage]
|
||||||
|
class PlacesCollectionRoute extends PageRouteInfo<void> {
|
||||||
|
const PlacesCollectionRoute({List<PageRouteInfo>? children})
|
||||||
|
: super(
|
||||||
|
PlacesCollectionRoute.name,
|
||||||
|
initialChildren: children,
|
||||||
|
);
|
||||||
|
|
||||||
|
static const String name = 'PlacesCollectionRoute';
|
||||||
|
|
||||||
|
static PageInfo page = PageInfo(
|
||||||
|
name,
|
||||||
|
builder: (data) {
|
||||||
|
return const PlacesCollectionPage();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/// generated route for
|
/// generated route for
|
||||||
/// [RecentlyAddedPage]
|
/// [RecentlyAddedPage]
|
||||||
class RecentlyAddedRoute extends PageRouteInfo<void> {
|
class RecentlyAddedRoute extends PageRouteInfo<void> {
|
||||||
@ -1429,25 +1500,6 @@ class SharedLinkRoute extends PageRouteInfo<void> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// generated route for
|
|
||||||
/// [SharingPage]
|
|
||||||
class SharingRoute extends PageRouteInfo<void> {
|
|
||||||
const SharingRoute({List<PageRouteInfo>? children})
|
|
||||||
: super(
|
|
||||||
SharingRoute.name,
|
|
||||||
initialChildren: children,
|
|
||||||
);
|
|
||||||
|
|
||||||
static const String name = 'SharingRoute';
|
|
||||||
|
|
||||||
static PageInfo page = PageInfo(
|
|
||||||
name,
|
|
||||||
builder: (data) {
|
|
||||||
return const SharingPage();
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// generated route for
|
/// generated route for
|
||||||
/// [SplashScreenPage]
|
/// [SplashScreenPage]
|
||||||
class SplashScreenRoute extends PageRouteInfo<void> {
|
class SplashScreenRoute extends PageRouteInfo<void> {
|
||||||
|
@ -1,12 +1,10 @@
|
|||||||
import 'package:auto_route/auto_route.dart';
|
import 'package:auto_route/auto_route.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/providers/album/album.provider.dart';
|
|
||||||
import 'package:immich_mobile/providers/memory.provider.dart';
|
import 'package:immich_mobile/providers/memory.provider.dart';
|
||||||
import 'package:immich_mobile/providers/search/people.provider.dart';
|
import 'package:immich_mobile/providers/search/people.provider.dart';
|
||||||
|
|
||||||
import 'package:immich_mobile/providers/search/search_page_state.provider.dart';
|
import 'package:immich_mobile/providers/search/search_page_state.provider.dart';
|
||||||
import 'package:immich_mobile/providers/album/shared_album.provider.dart';
|
|
||||||
import 'package:immich_mobile/entities/store.entity.dart';
|
import 'package:immich_mobile/entities/store.entity.dart';
|
||||||
import 'package:immich_mobile/entities/user.entity.dart';
|
import 'package:immich_mobile/entities/user.entity.dart';
|
||||||
import 'package:immich_mobile/providers/api.provider.dart';
|
import 'package:immich_mobile/providers/api.provider.dart';
|
||||||
@ -21,14 +19,6 @@ class TabNavigationObserver extends AutoRouterObserver {
|
|||||||
required this.ref,
|
required this.ref,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
|
||||||
void didInitTabRoute(TabPageRoute route, TabPageRoute? previousRoute) {
|
|
||||||
// Perform tasks on first navigation to SearchRoute
|
|
||||||
if (route.name == 'SearchRoute') {
|
|
||||||
// ref.refresh(getCuratedLocationProvider);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> didChangeTabRoute(
|
Future<void> didChangeTabRoute(
|
||||||
TabPageRoute route,
|
TabPageRoute route,
|
||||||
@ -41,15 +31,6 @@ class TabNavigationObserver extends AutoRouterObserver {
|
|||||||
ref.invalidate(getAllPeopleProvider);
|
ref.invalidate(getAllPeopleProvider);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (route.name == 'SharingRoute') {
|
|
||||||
ref.read(sharedAlbumProvider.notifier).getAllSharedAlbums();
|
|
||||||
Future(() => ref.read(assetProvider.notifier).getAllAsset());
|
|
||||||
}
|
|
||||||
|
|
||||||
if (route.name == 'LibraryRoute') {
|
|
||||||
ref.read(albumProvider.notifier).getAllAlbums();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (route.name == 'HomeRoute') {
|
if (route.name == 'HomeRoute') {
|
||||||
ref.invalidate(memoryFutureProvider);
|
ref.invalidate(memoryFutureProvider);
|
||||||
Future(() => ref.read(assetProvider.notifier).getAllAsset());
|
Future(() => ref.read(assetProvider.notifier).getAllAsset());
|
||||||
|
@ -16,6 +16,7 @@ import 'package:immich_mobile/entities/album.entity.dart';
|
|||||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||||
import 'package:immich_mobile/entities/store.entity.dart';
|
import 'package:immich_mobile/entities/store.entity.dart';
|
||||||
import 'package:immich_mobile/entities/user.entity.dart';
|
import 'package:immich_mobile/entities/user.entity.dart';
|
||||||
|
import 'package:immich_mobile/models/albums/album_search.model.dart';
|
||||||
import 'package:immich_mobile/repositories/album.repository.dart';
|
import 'package:immich_mobile/repositories/album.repository.dart';
|
||||||
import 'package:immich_mobile/repositories/album_api.repository.dart';
|
import 'package:immich_mobile/repositories/album_api.repository.dart';
|
||||||
import 'package:immich_mobile/repositories/asset.repository.dart';
|
import 'package:immich_mobile/repositories/asset.repository.dart';
|
||||||
@ -152,7 +153,7 @@ class AlbumService {
|
|||||||
|
|
||||||
/// Checks remote albums (owned if `isShared` is false) for changes,
|
/// Checks remote albums (owned if `isShared` is false) for changes,
|
||||||
/// updates the local database and returns `true` if there were any changes
|
/// updates the local database and returns `true` if there were any changes
|
||||||
Future<bool> refreshRemoteAlbums({required bool isShared}) async {
|
Future<bool> refreshRemoteAlbums() async {
|
||||||
if (!_remoteCompleter.isCompleted) {
|
if (!_remoteCompleter.isCompleted) {
|
||||||
// guard against concurrent calls
|
// guard against concurrent calls
|
||||||
return _remoteCompleter.future;
|
return _remoteCompleter.future;
|
||||||
@ -162,12 +163,21 @@ class AlbumService {
|
|||||||
bool changes = false;
|
bool changes = false;
|
||||||
try {
|
try {
|
||||||
await _userService.refreshUsers();
|
await _userService.refreshUsers();
|
||||||
final List<Album> serverAlbums =
|
final List<Album> sharedAlbum =
|
||||||
await _albumApiRepository.getAll(shared: isShared ? true : null);
|
await _albumApiRepository.getAll(shared: true);
|
||||||
changes = await _syncService.syncRemoteAlbumsToDb(
|
|
||||||
serverAlbums,
|
final List<Album> ownedAlbum =
|
||||||
isShared: isShared,
|
await _albumApiRepository.getAll(shared: null);
|
||||||
|
|
||||||
|
final albums = HashSet<Album>(
|
||||||
|
equals: (a, b) => a.remoteId == b.remoteId,
|
||||||
|
hashCode: (a) => a.remoteId.hashCode,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
albums.addAll(sharedAlbum);
|
||||||
|
albums.addAll(ownedAlbum);
|
||||||
|
|
||||||
|
changes = await _syncService.syncRemoteAlbumsToDb(albums.toList());
|
||||||
} finally {
|
} finally {
|
||||||
_remoteCompleter.complete(changes);
|
_remoteCompleter.complete(changes);
|
||||||
}
|
}
|
||||||
@ -213,9 +223,9 @@ class AlbumService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<AlbumAddAssetsResponse?> addAdditionalAssetToAlbum(
|
Future<AlbumAddAssetsResponse?> addAssets(
|
||||||
Iterable<Asset> assets,
|
|
||||||
Album album,
|
Album album,
|
||||||
|
Iterable<Asset> assets,
|
||||||
) async {
|
) async {
|
||||||
try {
|
try {
|
||||||
final result = await _albumApiRepository.addAssets(
|
final result = await _albumApiRepository.addAssets(
|
||||||
@ -234,7 +244,7 @@ class AlbumService {
|
|||||||
successfullyAdded: addedAssets.length,
|
successfullyAdded: addedAssets.length,
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint("Error addAdditionalAssetToAlbum ${e.toString()}");
|
debugPrint("Error addAssets ${e.toString()}");
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -253,30 +263,14 @@ class AlbumService {
|
|||||||
await _albumRepository.update(album);
|
await _albumRepository.update(album);
|
||||||
});
|
});
|
||||||
|
|
||||||
Future<bool> addAdditionalUserToAlbum(
|
Future<bool> setActivityStatus(Album album, bool enabled) async {
|
||||||
List<String> sharedUserIds,
|
|
||||||
Album album,
|
|
||||||
) async {
|
|
||||||
try {
|
|
||||||
final updatedAlbum =
|
|
||||||
await _albumApiRepository.addUsers(album.remoteId!, sharedUserIds);
|
|
||||||
await _entityService.fillAlbumWithDatabaseEntities(updatedAlbum);
|
|
||||||
await _albumRepository.update(updatedAlbum);
|
|
||||||
return true;
|
|
||||||
} catch (e) {
|
|
||||||
debugPrint("Error addAdditionalUserToAlbum ${e.toString()}");
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<bool> setActivityEnabled(Album album, bool enabled) async {
|
|
||||||
try {
|
try {
|
||||||
final updatedAlbum = await _albumApiRepository.update(
|
final updatedAlbum = await _albumApiRepository.update(
|
||||||
album.remoteId!,
|
album.remoteId!,
|
||||||
activityEnabled: enabled,
|
activityEnabled: enabled,
|
||||||
);
|
);
|
||||||
await _entityService.fillAlbumWithDatabaseEntities(updatedAlbum);
|
album.activityEnabled = updatedAlbum.activityEnabled;
|
||||||
await _albumRepository.update(updatedAlbum);
|
await _albumRepository.update(album);
|
||||||
return true;
|
return true;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint("Error setActivityEnabled ${e.toString()}");
|
debugPrint("Error setActivityEnabled ${e.toString()}");
|
||||||
@ -327,7 +321,7 @@ class AlbumService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> removeAssetFromAlbum(
|
Future<bool> removeAsset(
|
||||||
Album album,
|
Album album,
|
||||||
Iterable<Asset> assets,
|
Iterable<Asset> assets,
|
||||||
) async {
|
) async {
|
||||||
@ -346,7 +340,7 @@ class AlbumService {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> removeUserFromAlbum(
|
Future<bool> removeUser(
|
||||||
Album album,
|
Album album,
|
||||||
User user,
|
User user,
|
||||||
) async {
|
) async {
|
||||||
@ -363,22 +357,44 @@ class AlbumService {
|
|||||||
await _albumRepository.update(a!);
|
await _albumRepository.update(a!);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
} catch (e) {
|
} catch (error) {
|
||||||
debugPrint("Error removeUserFromAlbum ${e.toString()}");
|
debugPrint("Error removeUser ${error.toString()}");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<bool> addUsers(
|
||||||
|
Album album,
|
||||||
|
List<String> userIds,
|
||||||
|
) async {
|
||||||
|
try {
|
||||||
|
final updatedAlbum =
|
||||||
|
await _albumApiRepository.addUsers(album.remoteId!, userIds);
|
||||||
|
|
||||||
|
album.sharedUsers.addAll(updatedAlbum.remoteUsers);
|
||||||
|
album.shared = true;
|
||||||
|
|
||||||
|
await _albumRepository.addUsers(album, album.sharedUsers.toList());
|
||||||
|
await _albumRepository.update(album);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
debugPrint("Error addUsers ${error.toString()}");
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
Future<bool> changeTitleAlbum(
|
Future<bool> changeTitleAlbum(
|
||||||
Album album,
|
Album album,
|
||||||
String newAlbumTitle,
|
String newAlbumTitle,
|
||||||
) async {
|
) async {
|
||||||
try {
|
try {
|
||||||
album = await _albumApiRepository.update(
|
final updatedAlbum = await _albumApiRepository.update(
|
||||||
album.remoteId!,
|
album.remoteId!,
|
||||||
name: newAlbumTitle,
|
name: newAlbumTitle,
|
||||||
);
|
);
|
||||||
await _entityService.fillAlbumWithDatabaseEntities(album);
|
|
||||||
|
album.name = updatedAlbum.name;
|
||||||
await _albumRepository.update(album);
|
await _albumRepository.update(album);
|
||||||
return true;
|
return true;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -405,4 +421,15 @@ class AlbumService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<List<Album>> getAll() async {
|
||||||
|
return _albumRepository.getAll(remote: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<Album>> search(
|
||||||
|
String searchTerm,
|
||||||
|
QuickFilterMode filterMode,
|
||||||
|
) async {
|
||||||
|
return _albumRepository.search(searchTerm, filterMode);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -32,6 +32,7 @@ class EntityService {
|
|||||||
.getByIds(album.remoteUsers.map((user) => user.id).toList());
|
.getByIds(album.remoteUsers.map((user) => user.id).toList());
|
||||||
album.sharedUsers.clear();
|
album.sharedUsers.clear();
|
||||||
album.sharedUsers.addAll(users);
|
album.sharedUsers.addAll(users);
|
||||||
|
album.shared = true;
|
||||||
}
|
}
|
||||||
if (album.remoteAssets.isNotEmpty) {
|
if (album.remoteAssets.isNotEmpty) {
|
||||||
// replace all assets with assets from database
|
// replace all assets with assets from database
|
||||||
|
@ -95,10 +95,9 @@ class SyncService {
|
|||||||
/// Syncs remote albums to the database
|
/// Syncs remote albums to the database
|
||||||
/// returns `true` if there were any changes
|
/// returns `true` if there were any changes
|
||||||
Future<bool> syncRemoteAlbumsToDb(
|
Future<bool> syncRemoteAlbumsToDb(
|
||||||
List<Album> remote, {
|
List<Album> remote,
|
||||||
required bool isShared,
|
) =>
|
||||||
}) =>
|
_lock.run(() => _syncRemoteAlbumsToDb(remote));
|
||||||
_lock.run(() => _syncRemoteAlbumsToDb(remote, isShared));
|
|
||||||
|
|
||||||
/// Syncs all device albums and their assets to the database
|
/// Syncs all device albums and their assets to the database
|
||||||
/// Returns `true` if there were any changes
|
/// Returns `true` if there were any changes
|
||||||
@ -310,17 +309,14 @@ class SyncService {
|
|||||||
/// returns `true` if there were any changes
|
/// returns `true` if there were any changes
|
||||||
Future<bool> _syncRemoteAlbumsToDb(
|
Future<bool> _syncRemoteAlbumsToDb(
|
||||||
List<Album> remoteAlbums,
|
List<Album> remoteAlbums,
|
||||||
bool isShared,
|
|
||||||
) async {
|
) async {
|
||||||
remoteAlbums.sortBy((e) => e.remoteId!);
|
remoteAlbums.sortBy((e) => e.remoteId!);
|
||||||
|
|
||||||
final User me = await _userRepository.me();
|
|
||||||
final List<Album> dbAlbums = await _albumRepository.getAll(
|
final List<Album> dbAlbums = await _albumRepository.getAll(
|
||||||
remote: true,
|
remote: true,
|
||||||
shared: isShared ? true : null,
|
|
||||||
ownerId: isShared ? null : me.isarId,
|
|
||||||
sortBy: AlbumSort.remoteId,
|
sortBy: AlbumSort.remoteId,
|
||||||
);
|
);
|
||||||
|
|
||||||
final List<Asset> toDelete = [];
|
final List<Asset> toDelete = [];
|
||||||
final List<Asset> existing = [];
|
final List<Asset> existing = [];
|
||||||
|
|
||||||
@ -335,7 +331,7 @@ class SyncService {
|
|||||||
onlySecond: (dbAlbum) => _removeAlbumFromDb(dbAlbum, toDelete),
|
onlySecond: (dbAlbum) => _removeAlbumFromDb(dbAlbum, toDelete),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (isShared && toDelete.isNotEmpty) {
|
if (toDelete.isNotEmpty) {
|
||||||
final List<int> idsToRemove = sharedAssetsToRemove(toDelete, existing);
|
final List<int> idsToRemove = sharedAssetsToRemove(toDelete, existing);
|
||||||
if (idsToRemove.isNotEmpty) {
|
if (idsToRemove.isNotEmpty) {
|
||||||
await _assetRepository.deleteById(idsToRemove);
|
await _assetRepository.deleteById(idsToRemove);
|
||||||
@ -801,8 +797,7 @@ class SyncService {
|
|||||||
assets.sort(Asset.compareByOwnerChecksumCreatedModified);
|
assets.sort(Asset.compareByOwnerChecksumCreatedModified);
|
||||||
assets.uniqueConsecutive(
|
assets.uniqueConsecutive(
|
||||||
compare: Asset.compareByOwnerChecksum,
|
compare: Asset.compareByOwnerChecksum,
|
||||||
onDuplicate: (a, b) =>
|
onDuplicate: (a, b) => {},
|
||||||
_log.info("Ignoring duplicate assets on device:\n$a\n$b"),
|
|
||||||
);
|
);
|
||||||
final int duplicates = before - assets.length;
|
final int duplicates = before - assets.length;
|
||||||
if (duplicates > 0) {
|
if (duplicates > 0) {
|
||||||
|
@ -190,17 +190,14 @@ ThemeData getThemeData({required ColorScheme colorScheme}) {
|
|||||||
displayLarge: TextStyle(
|
displayLarge: TextStyle(
|
||||||
fontSize: 26,
|
fontSize: 26,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
color: isDark ? Colors.white : primaryColor,
|
|
||||||
),
|
),
|
||||||
displayMedium: TextStyle(
|
displayMedium: TextStyle(
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
color: isDark ? Colors.white : Colors.black87,
|
|
||||||
),
|
),
|
||||||
displaySmall: TextStyle(
|
displaySmall: TextStyle(
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
color: primaryColor,
|
|
||||||
),
|
),
|
||||||
titleSmall: const TextStyle(
|
titleSmall: const TextStyle(
|
||||||
fontSize: 16.0,
|
fontSize: 16.0,
|
||||||
@ -241,7 +238,7 @@ ThemeData getThemeData({required ColorScheme colorScheme}) {
|
|||||||
isDark ? colorScheme.surfaceContainer : colorScheme.surface,
|
isDark ? colorScheme.surfaceContainer : colorScheme.surface,
|
||||||
labelTextStyle: const WidgetStatePropertyAll(
|
labelTextStyle: const WidgetStatePropertyAll(
|
||||||
TextStyle(
|
TextStyle(
|
||||||
fontSize: 13,
|
fontSize: 14,
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -5,7 +5,6 @@ import 'package:flutter_hooks/flutter_hooks.dart';
|
|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
import 'package:immich_mobile/providers/album/album.provider.dart';
|
import 'package:immich_mobile/providers/album/album.provider.dart';
|
||||||
import 'package:immich_mobile/providers/album/shared_album.provider.dart';
|
|
||||||
import 'package:immich_mobile/services/album.service.dart';
|
import 'package:immich_mobile/services/album.service.dart';
|
||||||
import 'package:immich_mobile/widgets/album/add_to_album_sliverlist.dart';
|
import 'package:immich_mobile/widgets/album/add_to_album_sliverlist.dart';
|
||||||
import 'package:immich_mobile/routing/router.dart';
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
@ -27,13 +26,11 @@ class AddToAlbumBottomSheet extends HookConsumerWidget {
|
|||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final albums = ref.watch(albumProvider).where((a) => a.isRemote).toList();
|
final albums = ref.watch(albumProvider).where((a) => a.isRemote).toList();
|
||||||
final albumService = ref.watch(albumServiceProvider);
|
final albumService = ref.watch(albumServiceProvider);
|
||||||
final sharedAlbums = ref.watch(sharedAlbumProvider);
|
|
||||||
|
|
||||||
useEffect(
|
useEffect(
|
||||||
() {
|
() {
|
||||||
// Fetch album updates, e.g., cover image
|
// Fetch album updates, e.g., cover image
|
||||||
ref.read(albumProvider.notifier).getAllAlbums();
|
ref.read(albumProvider.notifier).refreshRemoteAlbums();
|
||||||
ref.read(sharedAlbumProvider.notifier).getAllSharedAlbums();
|
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
@ -41,9 +38,9 @@ class AddToAlbumBottomSheet extends HookConsumerWidget {
|
|||||||
);
|
);
|
||||||
|
|
||||||
void addToAlbum(Album album) async {
|
void addToAlbum(Album album) async {
|
||||||
final result = await albumService.addAdditionalAssetToAlbum(
|
final result = await albumService.addAssets(
|
||||||
assets,
|
|
||||||
album,
|
album,
|
||||||
|
assets,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (result != null) {
|
if (result != null) {
|
||||||
@ -107,8 +104,7 @@ class AddToAlbumBottomSheet extends HookConsumerWidget {
|
|||||||
onPressed: () {
|
onPressed: () {
|
||||||
context.pushRoute(
|
context.pushRoute(
|
||||||
CreateAlbumRoute(
|
CreateAlbumRoute(
|
||||||
isSharedAlbum: false,
|
assets: assets,
|
||||||
initialAssets: assets,
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@ -123,7 +119,7 @@ class AddToAlbumBottomSheet extends HookConsumerWidget {
|
|||||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
sliver: AddToAlbumSliverList(
|
sliver: AddToAlbumSliverList(
|
||||||
albums: albums,
|
albums: albums,
|
||||||
sharedAlbums: sharedAlbums,
|
sharedAlbums: albums.where((a) => a.shared).toList(),
|
||||||
onAddToAlbum: addToAlbum,
|
onAddToAlbum: addToAlbum,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -12,12 +12,14 @@ class AlbumThumbnailCard extends StatelessWidget {
|
|||||||
/// Whether or not to show the owner of the album (or "Owned")
|
/// Whether or not to show the owner of the album (or "Owned")
|
||||||
/// in the subtitle of the album
|
/// in the subtitle of the album
|
||||||
final bool showOwner;
|
final bool showOwner;
|
||||||
|
final bool showTitle;
|
||||||
|
|
||||||
const AlbumThumbnailCard({
|
const AlbumThumbnailCard({
|
||||||
super.key,
|
super.key,
|
||||||
required this.album,
|
required this.album,
|
||||||
this.onTap,
|
this.onTap,
|
||||||
this.showOwner = false,
|
this.showOwner = false,
|
||||||
|
this.showTitle = true,
|
||||||
});
|
});
|
||||||
|
|
||||||
final Album album;
|
final Album album;
|
||||||
@ -76,7 +78,7 @@ class AlbumThumbnailCard extends StatelessWidget {
|
|||||||
: 'album_thumbnail_card_items'
|
: 'album_thumbnail_card_items'
|
||||||
.tr(args: ['${album.assetCount}']),
|
.tr(args: ['${album.assetCount}']),
|
||||||
),
|
),
|
||||||
if (owner != null) const TextSpan(text: ' · '),
|
if (owner != null) const TextSpan(text: ' • '),
|
||||||
if (owner != null) TextSpan(text: owner),
|
if (owner != null) TextSpan(text: owner),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@ -102,6 +104,7 @@ class AlbumThumbnailCard extends StatelessWidget {
|
|||||||
: buildAlbumThumbnail(),
|
: buildAlbumThumbnail(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
if (showTitle) ...[
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(top: 8.0),
|
padding: const EdgeInsets.only(top: 8.0),
|
||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
@ -109,7 +112,7 @@ class AlbumThumbnailCard extends StatelessWidget {
|
|||||||
child: Text(
|
child: Text(
|
||||||
album.name,
|
album.name,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
style: context.textTheme.bodyMedium?.copyWith(
|
style: context.textTheme.titleSmall?.copyWith(
|
||||||
color: context.colorScheme.onSurface,
|
color: context.colorScheme.onSurface,
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
),
|
),
|
||||||
@ -118,6 +121,7 @@ class AlbumThumbnailCard extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
buildAlbumTextRow(),
|
buildAlbumTextRow(),
|
||||||
],
|
],
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
@ -7,7 +7,6 @@ import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
|||||||
import 'package:immich_mobile/providers/activity_statistics.provider.dart';
|
import 'package:immich_mobile/providers/activity_statistics.provider.dart';
|
||||||
import 'package:immich_mobile/providers/album/album.provider.dart';
|
import 'package:immich_mobile/providers/album/album.provider.dart';
|
||||||
import 'package:immich_mobile/providers/album/album_viewer.provider.dart';
|
import 'package:immich_mobile/providers/album/album_viewer.provider.dart';
|
||||||
import 'package:immich_mobile/providers/album/shared_album.provider.dart';
|
|
||||||
import 'package:immich_mobile/utils/immich_loading_overlay.dart';
|
import 'package:immich_mobile/utils/immich_loading_overlay.dart';
|
||||||
import 'package:immich_mobile/routing/router.dart';
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
import 'package:immich_mobile/entities/album.entity.dart';
|
import 'package:immich_mobile/entities/album.entity.dart';
|
||||||
@ -46,10 +45,8 @@ class AlbumViewerAppbar extends HookConsumerWidget
|
|||||||
|
|
||||||
final bool success;
|
final bool success;
|
||||||
if (album.shared) {
|
if (album.shared) {
|
||||||
success =
|
success = await ref.watch(albumProvider.notifier).deleteAlbum(album);
|
||||||
await ref.watch(sharedAlbumProvider.notifier).deleteAlbum(album);
|
context.navigateTo(TabControllerRoute(children: [AlbumsRoute()]));
|
||||||
context
|
|
||||||
.navigateTo(const TabControllerRoute(children: [SharingRoute()]));
|
|
||||||
} else {
|
} else {
|
||||||
success = await ref.watch(albumProvider.notifier).deleteAlbum(album);
|
success = await ref.watch(albumProvider.notifier).deleteAlbum(album);
|
||||||
context
|
context
|
||||||
@ -113,11 +110,10 @@ class AlbumViewerAppbar extends HookConsumerWidget
|
|||||||
isProcessing.value = true;
|
isProcessing.value = true;
|
||||||
|
|
||||||
bool isSuccess =
|
bool isSuccess =
|
||||||
await ref.watch(sharedAlbumProvider.notifier).leaveAlbum(album);
|
await ref.watch(albumProvider.notifier).leaveAlbum(album);
|
||||||
|
|
||||||
if (isSuccess) {
|
if (isSuccess) {
|
||||||
context
|
context.navigateTo(TabControllerRoute(children: [AlbumsRoute()]));
|
||||||
.navigateTo(const TabControllerRoute(children: [SharingRoute()]));
|
|
||||||
} else {
|
} else {
|
||||||
context.pop();
|
context.pop();
|
||||||
ImmichToast.show(
|
ImmichToast.show(
|
||||||
|
@ -4,7 +4,6 @@ import 'package:flutter_hooks/flutter_hooks.dart';
|
|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
import 'package:immich_mobile/providers/album/album.provider.dart';
|
import 'package:immich_mobile/providers/album/album.provider.dart';
|
||||||
import 'package:immich_mobile/providers/album/shared_album.provider.dart';
|
|
||||||
import 'package:immich_mobile/widgets/album/add_to_album_sliverlist.dart';
|
import 'package:immich_mobile/widgets/album/add_to_album_sliverlist.dart';
|
||||||
import 'package:immich_mobile/models/asset_selection_state.dart';
|
import 'package:immich_mobile/models/asset_selection_state.dart';
|
||||||
import 'package:immich_mobile/widgets/asset_grid/delete_dialog.dart';
|
import 'package:immich_mobile/widgets/asset_grid/delete_dialog.dart';
|
||||||
@ -72,7 +71,8 @@ class ControlBottomAppBar extends HookConsumerWidget {
|
|||||||
final trashEnabled =
|
final trashEnabled =
|
||||||
ref.watch(serverInfoProvider.select((v) => v.serverFeatures.trash));
|
ref.watch(serverInfoProvider.select((v) => v.serverFeatures.trash));
|
||||||
final albums = ref.watch(albumProvider).where((a) => a.isRemote).toList();
|
final albums = ref.watch(albumProvider).where((a) => a.isRemote).toList();
|
||||||
final sharedAlbums = ref.watch(sharedAlbumProvider);
|
final sharedAlbums =
|
||||||
|
ref.watch(albumProvider).where((a) => a.shared).toList();
|
||||||
const bottomPadding = 0.20;
|
const bottomPadding = 0.20;
|
||||||
final scrollController = useDraggableScrollController();
|
final scrollController = useDraggableScrollController();
|
||||||
|
|
||||||
|
@ -9,7 +9,6 @@ import 'package:fluttertoast/fluttertoast.dart';
|
|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/extensions/collection_extensions.dart';
|
import 'package:immich_mobile/extensions/collection_extensions.dart';
|
||||||
import 'package:immich_mobile/providers/album/album.provider.dart';
|
import 'package:immich_mobile/providers/album/album.provider.dart';
|
||||||
import 'package:immich_mobile/providers/album/shared_album.provider.dart';
|
|
||||||
import 'package:immich_mobile/services/album.service.dart';
|
import 'package:immich_mobile/services/album.service.dart';
|
||||||
import 'package:immich_mobile/services/stack.service.dart';
|
import 'package:immich_mobile/services/stack.service.dart';
|
||||||
import 'package:immich_mobile/providers/backup/manual_upload.provider.dart';
|
import 'package:immich_mobile/providers/backup/manual_upload.provider.dart';
|
||||||
@ -272,10 +271,9 @@ class MultiselectGrid extends HookConsumerWidget {
|
|||||||
if (assets.isEmpty) {
|
if (assets.isEmpty) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final result =
|
final result = await ref.read(albumServiceProvider).addAssets(
|
||||||
await ref.read(albumServiceProvider).addAdditionalAssetToAlbum(
|
|
||||||
assets,
|
|
||||||
album,
|
album,
|
||||||
|
assets,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (result != null) {
|
if (result != null) {
|
||||||
@ -323,8 +321,7 @@ class MultiselectGrid extends HookConsumerWidget {
|
|||||||
.createAlbumWithGeneratedName(assets);
|
.createAlbumWithGeneratedName(assets);
|
||||||
|
|
||||||
if (result != null) {
|
if (result != null) {
|
||||||
ref.watch(albumProvider.notifier).getAllAlbums();
|
ref.watch(albumProvider.notifier).refreshRemoteAlbums();
|
||||||
ref.watch(sharedAlbumProvider.notifier).getAllSharedAlbums();
|
|
||||||
selectionEnabledHook.value = false;
|
selectionEnabledHook.value = false;
|
||||||
|
|
||||||
context.pushRoute(AlbumViewerRoute(albumId: result.id));
|
context.pushRoute(AlbumViewerRoute(albumId: result.id));
|
||||||
|
@ -6,8 +6,8 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:fluttertoast/fluttertoast.dart';
|
import 'package:fluttertoast/fluttertoast.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
|
import 'package:immich_mobile/providers/album/album.provider.dart';
|
||||||
import 'package:immich_mobile/providers/album/current_album.provider.dart';
|
import 'package:immich_mobile/providers/album/current_album.provider.dart';
|
||||||
import 'package:immich_mobile/providers/album/shared_album.provider.dart';
|
|
||||||
import 'package:immich_mobile/providers/asset_viewer/asset_stack.provider.dart';
|
import 'package:immich_mobile/providers/asset_viewer/asset_stack.provider.dart';
|
||||||
import 'package:immich_mobile/providers/asset_viewer/download.provider.dart';
|
import 'package:immich_mobile/providers/asset_viewer/download.provider.dart';
|
||||||
import 'package:immich_mobile/providers/asset_viewer/show_controls.provider.dart';
|
import 'package:immich_mobile/providers/asset_viewer/show_controls.provider.dart';
|
||||||
@ -230,9 +230,7 @@ class BottomGalleryBar extends ConsumerWidget {
|
|||||||
handleRemoveFromAlbum() async {
|
handleRemoveFromAlbum() async {
|
||||||
final album = ref.read(currentAlbumProvider);
|
final album = ref.read(currentAlbumProvider);
|
||||||
final bool isSuccess = album != null &&
|
final bool isSuccess = album != null &&
|
||||||
await ref
|
await ref.read(albumProvider.notifier).removeAsset(album, [asset]);
|
||||||
.read(sharedAlbumProvider.notifier)
|
|
||||||
.removeAssetFromAlbum(album, [asset]);
|
|
||||||
|
|
||||||
if (isSuccess) {
|
if (isSuccess) {
|
||||||
// Workaround for asset remaining in the gallery
|
// Workaround for asset remaining in the gallery
|
||||||
|
@ -18,9 +18,10 @@ import 'package:immich_mobile/providers/server_info.provider.dart';
|
|||||||
class ImmichAppBar extends ConsumerWidget implements PreferredSizeWidget {
|
class ImmichAppBar extends ConsumerWidget implements PreferredSizeWidget {
|
||||||
@override
|
@override
|
||||||
Size get preferredSize => const Size.fromHeight(kToolbarHeight);
|
Size get preferredSize => const Size.fromHeight(kToolbarHeight);
|
||||||
final Widget? action;
|
final List<Widget>? actions;
|
||||||
|
final bool showUploadButton;
|
||||||
|
|
||||||
const ImmichAppBar({super.key, this.action});
|
const ImmichAppBar({super.key, this.actions, this.showUploadButton = true});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
@ -184,8 +185,14 @@ class ImmichAppBar extends ConsumerWidget implements PreferredSizeWidget {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
if (action != null)
|
if (actions != null)
|
||||||
Padding(padding: const EdgeInsets.only(right: 20), child: action!),
|
...actions!.map(
|
||||||
|
(action) => Padding(
|
||||||
|
padding: const EdgeInsets.only(right: 16),
|
||||||
|
child: action,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (showUploadButton)
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(right: 20),
|
padding: const EdgeInsets.only(right: 20),
|
||||||
child: buildBackupIndicator(),
|
child: buildBackupIndicator(),
|
||||||
|
@ -176,7 +176,7 @@ class LoginForm extends HookConsumerWidget {
|
|||||||
populateTestLoginInfo1() {
|
populateTestLoginInfo1() {
|
||||||
usernameController.text = 'testuser@email.com';
|
usernameController.text = 'testuser@email.com';
|
||||||
passwordController.text = 'password';
|
passwordController.text = 'password';
|
||||||
serverEndpointController.text = 'http://192.168.1.16:2283/api';
|
serverEndpointController.text = 'http://192.168.1.118:2283/api';
|
||||||
}
|
}
|
||||||
|
|
||||||
login() async {
|
login() async {
|
||||||
|
@ -1,48 +0,0 @@
|
|||||||
import 'package:auto_route/auto_route.dart';
|
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|
||||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
|
||||||
import 'package:immich_mobile/routing/router.dart';
|
|
||||||
import 'package:immich_mobile/entities/user.entity.dart';
|
|
||||||
import 'package:immich_mobile/widgets/common/user_avatar.dart';
|
|
||||||
|
|
||||||
class PartnerList extends HookConsumerWidget {
|
|
||||||
const PartnerList({super.key, required this.partner});
|
|
||||||
|
|
||||||
final List<User> partner;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
|
||||||
return SliverList(
|
|
||||||
delegate:
|
|
||||||
SliverChildBuilderDelegate(listEntry, childCount: partner.length),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget listEntry(BuildContext context, int index) {
|
|
||||||
final User p = partner[index];
|
|
||||||
return ListTile(
|
|
||||||
contentPadding: const EdgeInsets.only(
|
|
||||||
left: 12.0,
|
|
||||||
right: 18.0,
|
|
||||||
),
|
|
||||||
leading: userAvatar(context, p, radius: 24),
|
|
||||||
title: Text(
|
|
||||||
"partner_list_user_photos",
|
|
||||||
style: context.textTheme.labelLarge,
|
|
||||||
).tr(
|
|
||||||
namedArgs: {
|
|
||||||
'user': p.name,
|
|
||||||
},
|
|
||||||
),
|
|
||||||
trailing: Text(
|
|
||||||
"partner_list_view_all",
|
|
||||||
style: context.textTheme.labelLarge?.copyWith(
|
|
||||||
color: context.primaryColor,
|
|
||||||
),
|
|
||||||
).tr(),
|
|
||||||
onTap: () => context.pushRoute((PartnerDetailRoute(partner: p))),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -13,6 +13,7 @@ class SearchMapThumbnail extends StatelessWidget {
|
|||||||
});
|
});
|
||||||
|
|
||||||
final double size;
|
final double size;
|
||||||
|
final bool showTitle = true;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
@ -1,18 +1,24 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
import json
|
import json
|
||||||
import subprocess
|
import subprocess
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
with open('assets/i18n/en-US.json', 'r') as f:
|
with open('assets/i18n/en-US.json', 'r+') as f:
|
||||||
data = json.load(f)
|
data = json.load(f)
|
||||||
|
keys_to_delete = []
|
||||||
|
|
||||||
for k in data.keys():
|
for k in data.keys():
|
||||||
print(k)
|
sp = subprocess.run(['sh', '-c', f'grep -q -r --include="*.dart" "{k}"'])
|
||||||
sp = subprocess.run(['sh', '-c', f'grep -r --include="*.dart" "{k}"'])
|
|
||||||
|
|
||||||
if sp.returncode != 0:
|
if sp.returncode != 0:
|
||||||
print("Not found in source code!")
|
print("Not found in source code, key:", k)
|
||||||
return 1
|
keys_to_delete.append(k)
|
||||||
|
|
||||||
|
for k in keys_to_delete:
|
||||||
|
del data[k]
|
||||||
|
|
||||||
|
f.seek(0)
|
||||||
|
f.truncate()
|
||||||
|
json.dump(data, f, indent=4)
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
main()
|
main()
|
@ -1,31 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
import json
|
|
||||||
import subprocess
|
|
||||||
|
|
||||||
def main():
|
|
||||||
print("CHECK GERMAN TRANSLATIONS")
|
|
||||||
with open('assets/i18n/de-DE.json', 'r') as f:
|
|
||||||
data = json.load(f)
|
|
||||||
|
|
||||||
for k in data.keys():
|
|
||||||
print(k)
|
|
||||||
sp = subprocess.run(['sh', '-c', f'grep -r --include="./assets/i18n/en-US.json" "{k}"'])
|
|
||||||
|
|
||||||
if sp.returncode != 0:
|
|
||||||
print(f"Outdated Key! {k}")
|
|
||||||
return 1
|
|
||||||
|
|
||||||
print("CHECK FRENCH TRANSLATIONS")
|
|
||||||
with open('assets/i18n/fr-FR.json', 'r') as f:
|
|
||||||
data = json.load(f)
|
|
||||||
|
|
||||||
for k in data.keys():
|
|
||||||
print(k)
|
|
||||||
sp = subprocess.run(['sh', '-c', f'grep -r --include="./assets/i18n/en-US.json" "{k}"'])
|
|
||||||
|
|
||||||
if sp.returncode != 0:
|
|
||||||
print(f"Outdated Key! {k}")
|
|
||||||
return 1
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
main()
|
|
@ -79,25 +79,35 @@ void main() {
|
|||||||
verifyNoMoreInteractions(syncService);
|
verifyNoMoreInteractions(syncService);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
group('refreshRemoteAlbums', () {
|
group('refreshRemoteAlbums', () {
|
||||||
test('isShared: false', () async {
|
test('is working', () async {
|
||||||
when(() => userService.refreshUsers()).thenAnswer((_) async => true);
|
when(() => userService.refreshUsers()).thenAnswer((_) async => true);
|
||||||
|
when(() => albumApiRepository.getAll(shared: true))
|
||||||
|
.thenAnswer((_) async => [AlbumStub.sharedWithUser]);
|
||||||
|
|
||||||
when(() => albumApiRepository.getAll(shared: null))
|
when(() => albumApiRepository.getAll(shared: null))
|
||||||
.thenAnswer((_) async => [AlbumStub.oneAsset, AlbumStub.twoAsset]);
|
.thenAnswer((_) async => [AlbumStub.oneAsset, AlbumStub.twoAsset]);
|
||||||
|
|
||||||
when(
|
when(
|
||||||
() => syncService.syncRemoteAlbumsToDb(
|
() => syncService.syncRemoteAlbumsToDb([
|
||||||
[AlbumStub.oneAsset, AlbumStub.twoAsset],
|
AlbumStub.twoAsset,
|
||||||
isShared: false,
|
AlbumStub.oneAsset,
|
||||||
),
|
AlbumStub.sharedWithUser,
|
||||||
|
]),
|
||||||
).thenAnswer((_) async => true);
|
).thenAnswer((_) async => true);
|
||||||
final result = await sut.refreshRemoteAlbums(isShared: false);
|
final result = await sut.refreshRemoteAlbums();
|
||||||
expect(result, true);
|
expect(result, true);
|
||||||
verify(() => userService.refreshUsers()).called(1);
|
verify(() => userService.refreshUsers()).called(1);
|
||||||
|
verify(() => albumApiRepository.getAll(shared: true)).called(1);
|
||||||
verify(() => albumApiRepository.getAll(shared: null)).called(1);
|
verify(() => albumApiRepository.getAll(shared: null)).called(1);
|
||||||
verify(
|
verify(
|
||||||
() => syncService.syncRemoteAlbumsToDb(
|
() => syncService.syncRemoteAlbumsToDb(
|
||||||
[AlbumStub.oneAsset, AlbumStub.twoAsset],
|
[
|
||||||
isShared: false,
|
AlbumStub.twoAsset,
|
||||||
|
AlbumStub.oneAsset,
|
||||||
|
AlbumStub.sharedWithUser,
|
||||||
|
],
|
||||||
),
|
),
|
||||||
).called(1);
|
).called(1);
|
||||||
verifyNoMoreInteractions(userService);
|
verifyNoMoreInteractions(userService);
|
||||||
@ -166,9 +176,9 @@ void main() {
|
|||||||
() => albumRepository.update(AlbumStub.oneAsset),
|
() => albumRepository.update(AlbumStub.oneAsset),
|
||||||
).thenAnswer((_) async => AlbumStub.oneAsset);
|
).thenAnswer((_) async => AlbumStub.oneAsset);
|
||||||
|
|
||||||
final result = await sut.addAdditionalAssetToAlbum(
|
final result = await sut.addAssets(
|
||||||
[AssetStub.image1, AssetStub.image2],
|
|
||||||
AlbumStub.oneAsset,
|
AlbumStub.oneAsset,
|
||||||
|
[AssetStub.image1, AssetStub.image2],
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(result != null, true);
|
expect(result != null, true);
|
||||||
@ -185,18 +195,23 @@ void main() {
|
|||||||
).thenAnswer(
|
).thenAnswer(
|
||||||
(_) async => AlbumStub.sharedWithUser,
|
(_) async => AlbumStub.sharedWithUser,
|
||||||
);
|
);
|
||||||
when(
|
|
||||||
() => entityService
|
|
||||||
.fillAlbumWithDatabaseEntities(AlbumStub.sharedWithUser),
|
|
||||||
).thenAnswer((_) async => AlbumStub.sharedWithUser);
|
|
||||||
when(
|
|
||||||
() => albumRepository.update(AlbumStub.sharedWithUser),
|
|
||||||
).thenAnswer((_) async => AlbumStub.sharedWithUser);
|
|
||||||
|
|
||||||
final result = await sut.addAdditionalUserToAlbum(
|
when(
|
||||||
[UserStub.user2.id],
|
() => albumRepository.addUsers(
|
||||||
AlbumStub.emptyAlbum,
|
AlbumStub.emptyAlbum,
|
||||||
|
AlbumStub.emptyAlbum.sharedUsers.toList(),
|
||||||
|
),
|
||||||
|
).thenAnswer((_) async => AlbumStub.emptyAlbum);
|
||||||
|
|
||||||
|
when(
|
||||||
|
() => albumRepository.update(AlbumStub.emptyAlbum),
|
||||||
|
).thenAnswer((_) async => AlbumStub.emptyAlbum);
|
||||||
|
|
||||||
|
final result = await sut.addUsers(
|
||||||
|
AlbumStub.emptyAlbum,
|
||||||
|
[UserStub.user2.id],
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(result, true);
|
expect(result, true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -15,7 +15,7 @@
|
|||||||
"groupName": "typescript-projects",
|
"groupName": "typescript-projects",
|
||||||
"matchUpdateTypes": ["minor", "patch"],
|
"matchUpdateTypes": ["minor", "patch"],
|
||||||
"excludePackagePrefixes": ["exiftool", "reflect-metadata"],
|
"excludePackagePrefixes": ["exiftool", "reflect-metadata"],
|
||||||
"excludePackageNames": ["node", "@types/node"],
|
"excludePackageNames": ["node", "@types/node", "@mapbox/mapbox-gl-rtl-text"],
|
||||||
"schedule": "on tuesday"
|
"schedule": "on tuesday"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -15,9 +15,6 @@ import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interf
|
|||||||
import { getAssetFiles } from 'src/utils/asset.util';
|
import { getAssetFiles } from 'src/utils/asset.util';
|
||||||
import { getConfig } from 'src/utils/config';
|
import { getConfig } from 'src/utils/config';
|
||||||
|
|
||||||
export const THUMBNAIL_DIR = resolve(join(APP_MEDIA_LOCATION, StorageFolder.THUMBNAILS));
|
|
||||||
export const ENCODED_VIDEO_DIR = resolve(join(APP_MEDIA_LOCATION, StorageFolder.ENCODED_VIDEO));
|
|
||||||
|
|
||||||
export interface MoveRequest {
|
export interface MoveRequest {
|
||||||
entityId: string;
|
entityId: string;
|
||||||
pathType: PathType;
|
pathType: PathType;
|
||||||
@ -118,10 +115,6 @@ export class StorageCore {
|
|||||||
return normalizedPath.startsWith(normalizedAppMediaLocation);
|
return normalizedPath.startsWith(normalizedAppMediaLocation);
|
||||||
}
|
}
|
||||||
|
|
||||||
static isGeneratedAsset(path: string) {
|
|
||||||
return path.startsWith(THUMBNAIL_DIR) || path.startsWith(ENCODED_VIDEO_DIR);
|
|
||||||
}
|
|
||||||
|
|
||||||
async moveAssetImage(asset: AssetEntity, pathType: GeneratedImageType, format: ImageFormat) {
|
async moveAssetImage(asset: AssetEntity, pathType: GeneratedImageType, format: ImageFormat) {
|
||||||
const { id: entityId, files } = asset;
|
const { id: entityId, files } = asset;
|
||||||
const { thumbnailFile, previewFile } = getAssetFiles(files);
|
const { thumbnailFile, previewFile } = getAssetFiles(files);
|
||||||
|
@ -36,7 +36,7 @@ export class AssetFileEntity {
|
|||||||
@Column()
|
@Column()
|
||||||
path!: string;
|
path!: string;
|
||||||
|
|
||||||
@Column({ type: 'bigint' })
|
@Column({ type: 'bytea' })
|
||||||
@Index()
|
@Index()
|
||||||
checksum!: BigInt | null;
|
checksum!: Buffer | null;
|
||||||
}
|
}
|
||||||
|
@ -145,7 +145,7 @@ export interface UpsertFileOptions {
|
|||||||
assetId: string;
|
assetId: string;
|
||||||
type: AssetFileType;
|
type: AssetFileType;
|
||||||
path: string;
|
path: string;
|
||||||
checksum?: BigInt;
|
checksum?: Buffer;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type AssetPathEntity = Pick<AssetEntity, 'id' | 'originalPath' | 'isOffline'>;
|
export type AssetPathEntity = Pick<AssetEntity, 'id' | 'originalPath' | 'isOffline'>;
|
||||||
@ -173,12 +173,6 @@ export interface IAssetRepository {
|
|||||||
order?: FindOptionsOrder<AssetEntity>,
|
order?: FindOptionsOrder<AssetEntity>,
|
||||||
): Promise<AssetEntity | null>;
|
): Promise<AssetEntity | null>;
|
||||||
getWithout(pagination: PaginationOptions, property: WithoutProperty): Paginated<AssetEntity>;
|
getWithout(pagination: PaginationOptions, property: WithoutProperty): Paginated<AssetEntity>;
|
||||||
getWith(
|
|
||||||
pagination: PaginationOptions,
|
|
||||||
property: WithProperty,
|
|
||||||
libraryId?: string,
|
|
||||||
withDeleted?: boolean,
|
|
||||||
): Paginated<AssetEntity>;
|
|
||||||
getRandom(userIds: string[], count: number): Promise<AssetEntity[]>;
|
getRandom(userIds: string[], count: number): Promise<AssetEntity[]>;
|
||||||
getLastUpdatedAssetForAlbumId(albumId: string): Promise<AssetEntity | null>;
|
getLastUpdatedAssetForAlbumId(albumId: string): Promise<AssetEntity | null>;
|
||||||
getByLibraryIdAndOriginalPath(libraryId: string, originalPath: string): Promise<AssetEntity | null>;
|
getByLibraryIdAndOriginalPath(libraryId: string, originalPath: string): Promise<AssetEntity | null>;
|
||||||
|
@ -5,7 +5,7 @@ export interface ICryptoRepository {
|
|||||||
randomUUID(): string;
|
randomUUID(): string;
|
||||||
hashFile(filePath: string | Buffer): Promise<Buffer>;
|
hashFile(filePath: string | Buffer): Promise<Buffer>;
|
||||||
hashSha256(data: string): string;
|
hashSha256(data: string): string;
|
||||||
xxHash(value: string): BigInt;
|
xxHash(value: string): Buffer;
|
||||||
verifySha256(data: string, encrypted: string, publicKey: string): boolean;
|
verifySha256(data: string, encrypted: string, publicKey: string): boolean;
|
||||||
hashSha1(data: string | Buffer): Buffer;
|
hashSha1(data: string | Buffer): Buffer;
|
||||||
hashBcrypt(data: string | Buffer, saltOrRounds: string | number): Promise<string>;
|
hashBcrypt(data: string | Buffer, saltOrRounds: string | number): Promise<string>;
|
||||||
|
@ -48,7 +48,6 @@ export interface IDatabaseRepository {
|
|||||||
getPostgresVersion(): Promise<string>;
|
getPostgresVersion(): Promise<string>;
|
||||||
getPostgresVersionRange(): string;
|
getPostgresVersionRange(): string;
|
||||||
createExtension(extension: DatabaseExtension): Promise<void>;
|
createExtension(extension: DatabaseExtension): Promise<void>;
|
||||||
updateExtension(extension: DatabaseExtension, version?: string): Promise<void>;
|
|
||||||
updateVectorExtension(extension: VectorExtension, version?: string): Promise<VectorUpdateResult>;
|
updateVectorExtension(extension: VectorExtension, version?: string): Promise<VectorUpdateResult>;
|
||||||
reindex(index: VectorIndex): Promise<void>;
|
reindex(index: VectorIndex): Promise<void>;
|
||||||
shouldReindex(name: VectorIndex): Promise<boolean>;
|
shouldReindex(name: VectorIndex): Promise<boolean>;
|
||||||
|
@ -28,5 +28,4 @@ export interface IMapRepository {
|
|||||||
init(): Promise<void>;
|
init(): Promise<void>;
|
||||||
reverseGeocode(point: GeoPoint): Promise<ReverseGeocodeResult>;
|
reverseGeocode(point: GeoPoint): Promise<ReverseGeocodeResult>;
|
||||||
getMapMarkers(ownerIds: string[], albumIds: string[], options?: MapMarkerSearchOptions): Promise<MapMarker[]>;
|
getMapMarkers(ownerIds: string[], albumIds: string[], options?: MapMarkerSearchOptions): Promise<MapMarker[]>;
|
||||||
fetchStyle(url: string): Promise<any>;
|
|
||||||
}
|
}
|
||||||
|
@ -57,9 +57,7 @@ export interface IPersonRepository {
|
|||||||
|
|
||||||
create(person: Partial<PersonEntity>): Promise<PersonEntity>;
|
create(person: Partial<PersonEntity>): Promise<PersonEntity>;
|
||||||
createAll(people: Partial<PersonEntity>[]): Promise<string[]>;
|
createAll(people: Partial<PersonEntity>[]): Promise<string[]>;
|
||||||
createFaces(entities: Partial<AssetFaceEntity>[]): Promise<string[]>;
|
|
||||||
delete(entities: PersonEntity[]): Promise<void>;
|
delete(entities: PersonEntity[]): Promise<void>;
|
||||||
deleteAll(): Promise<void>;
|
|
||||||
deleteFaces(options: DeleteFacesOptions): Promise<void>;
|
deleteFaces(options: DeleteFacesOptions): Promise<void>;
|
||||||
refreshFaces(
|
refreshFaces(
|
||||||
facesToAdd: Partial<AssetFaceEntity>[],
|
facesToAdd: Partial<AssetFaceEntity>[],
|
||||||
|
@ -34,7 +34,7 @@ export interface ImmichFile extends Express.Multer.File {
|
|||||||
/** sha1 hash of file */
|
/** sha1 hash of file */
|
||||||
uuid: string;
|
uuid: string;
|
||||||
checksum: Buffer;
|
checksum: Buffer;
|
||||||
xxhash: BigInt;
|
xxhash: Buffer;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function mapToUploadFile(file: ImmichFile): UploadFile {
|
export function mapToUploadFile(file: ImmichFile): UploadFile {
|
||||||
@ -150,7 +150,7 @@ export class FileUploadInterceptor implements NestInterceptor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.logger.debug(`Handling asset upload file: ${file.originalname}`);
|
this.logger.debug(`Handling asset upload file: ${file.originalname}`);
|
||||||
const xxhash = new xxh3.Xxh3();
|
const xxhash = xxh3.Xxh3.withSeed();
|
||||||
const sha1hash = createHash('sha1');
|
const sha1hash = createHash('sha1');
|
||||||
|
|
||||||
file.stream.on('data', (chunk) => {
|
file.stream.on('data', (chunk) => {
|
||||||
@ -164,7 +164,11 @@ export class FileUploadInterceptor implements NestInterceptor {
|
|||||||
xxhash.reset();
|
xxhash.reset();
|
||||||
callback(error);
|
callback(error);
|
||||||
} else {
|
} else {
|
||||||
callback(null, { ...info, checksum: sha1hash.digest(), xxhash: xxhash.digest() });
|
callback(null, {
|
||||||
|
...info,
|
||||||
|
checksum: sha1hash.digest(),
|
||||||
|
xxhash: Buffer.from(xxhash.digest().toString(16), 'utf8'),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,7 @@ export class AssetFileChecksum1728632095015 implements MigrationInterface {
|
|||||||
name = 'AssetFileChecksum1728632095015';
|
name = 'AssetFileChecksum1728632095015';
|
||||||
|
|
||||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
await queryRunner.query(`ALTER TABLE "asset_files" ADD "checksum" bigint`);
|
await queryRunner.query(`ALTER TABLE "asset_files" ADD "checksum" bytea NULL`);
|
||||||
await queryRunner.query(`CREATE INDEX "IDX_c946066edd16cfa5c25a26aa8e" ON "asset_files" ("checksum")`);
|
await queryRunner.query(`CREATE INDEX "IDX_c946066edd16cfa5c25a26aa8e" ON "asset_files" ("checksum")`);
|
||||||
}
|
}
|
||||||
|
|
@ -499,39 +499,6 @@ export class AssetRepository implements IAssetRepository {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
getWith(
|
|
||||||
pagination: PaginationOptions,
|
|
||||||
property: WithProperty,
|
|
||||||
libraryId?: string,
|
|
||||||
withDeleted = false,
|
|
||||||
): Paginated<AssetEntity> {
|
|
||||||
let where: FindOptionsWhere<AssetEntity> | FindOptionsWhere<AssetEntity>[] = {};
|
|
||||||
|
|
||||||
switch (property) {
|
|
||||||
case WithProperty.SIDECAR: {
|
|
||||||
where = [{ sidecarPath: Not(IsNull()), isVisible: true }];
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
default: {
|
|
||||||
throw new Error(`Invalid getWith property: ${property}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (libraryId) {
|
|
||||||
where = [{ ...where, libraryId }];
|
|
||||||
}
|
|
||||||
|
|
||||||
return paginate(this.repository, pagination, {
|
|
||||||
where,
|
|
||||||
withDeleted,
|
|
||||||
order: {
|
|
||||||
// Ensures correct order when paginating
|
|
||||||
createdAt: 'ASC',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
getLastUpdatedAssetForAlbumId(albumId: string): Promise<AssetEntity | null> {
|
getLastUpdatedAssetForAlbumId(albumId: string): Promise<AssetEntity | null> {
|
||||||
return this.repository.findOne({
|
return this.repository.findOne({
|
||||||
where: { albums: { id: albumId } },
|
where: { albums: { id: albumId } },
|
||||||
@ -801,7 +768,7 @@ export class AssetRepository implements IAssetRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@GenerateSql({ params: [{ assetId: DummyValue.UUID, type: AssetFileType.PREVIEW, path: '/path/to/file' }] })
|
@GenerateSql({ params: [{ assetId: DummyValue.UUID, type: AssetFileType.PREVIEW, path: '/path/to/file' }] })
|
||||||
async upsertFile(file: { assetId: string; type: AssetFileType; path: string; checksum?: BigInt }): Promise<void> {
|
async upsertFile(file: { assetId: string; type: AssetFileType; path: string; checksum?: Buffer }): Promise<void> {
|
||||||
await this.fileRepository.upsert(file, { conflictPaths: ['assetId', 'type'] });
|
await this.fileRepository.upsert(file, { conflictPaths: ['assetId', 'type'] });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -30,7 +30,7 @@ export class CryptoRepository implements ICryptoRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
xxHash(value: string) {
|
xxHash(value: string) {
|
||||||
return xxh3.Xxh3.withSeed().update(value).digest();
|
return Buffer.from(xxh3.Xxh3.withSeed().update(value).digest().toString(16), 'utf8');
|
||||||
}
|
}
|
||||||
|
|
||||||
verifySha256(value: string, encryptedValue: string, publicKey: string) {
|
verifySha256(value: string, encryptedValue: string, publicKey: string) {
|
||||||
|
@ -74,10 +74,6 @@ export class DatabaseRepository implements IDatabaseRepository {
|
|||||||
await this.dataSource.query(`CREATE EXTENSION IF NOT EXISTS ${extension}`);
|
await this.dataSource.query(`CREATE EXTENSION IF NOT EXISTS ${extension}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateExtension(extension: DatabaseExtension, version?: string): Promise<void> {
|
|
||||||
await this.dataSource.query(`ALTER EXTENSION ${extension} UPDATE${version ? ` TO '${version}'` : ''}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
async updateVectorExtension(extension: VectorExtension, targetVersion?: string): Promise<VectorUpdateResult> {
|
async updateVectorExtension(extension: VectorExtension, targetVersion?: string): Promise<VectorUpdateResult> {
|
||||||
const { availableVersion, installedVersion } = await this.getExtensionVersion(extension);
|
const { availableVersion, installedVersion } = await this.getExtensionVersion(extension);
|
||||||
if (!installedVersion) {
|
if (!installedVersion) {
|
||||||
|
@ -113,20 +113,6 @@ export class MapRepository implements IMapRepository {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
async fetchStyle(url: string) {
|
|
||||||
try {
|
|
||||||
const response = await fetch(url);
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`Failed to fetch data from ${url} with status ${response.status}: ${await response.text()}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return response.json();
|
|
||||||
} catch (error) {
|
|
||||||
throw new Error(`Failed to fetch data from ${url}: ${error}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async reverseGeocode(point: GeoPoint): Promise<ReverseGeocodeResult> {
|
async reverseGeocode(point: GeoPoint): Promise<ReverseGeocodeResult> {
|
||||||
this.logger.debug(`Request: ${point.latitude},${point.longitude}`);
|
this.logger.debug(`Request: ${point.latitude},${point.longitude}`);
|
||||||
|
|
||||||
|
@ -63,10 +63,6 @@ export class PersonRepository implements IPersonRepository {
|
|||||||
await this.personRepository.remove(entities);
|
await this.personRepository.remove(entities);
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteAll(): Promise<void> {
|
|
||||||
await this.personRepository.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
async deleteFaces({ sourceType }: DeleteFacesOptions): Promise<void> {
|
async deleteFaces({ sourceType }: DeleteFacesOptions): Promise<void> {
|
||||||
await this.assetFaceRepository
|
await this.assetFaceRepository
|
||||||
.createQueryBuilder('asset_faces')
|
.createQueryBuilder('asset_faces')
|
||||||
@ -269,11 +265,6 @@ export class PersonRepository implements IPersonRepository {
|
|||||||
return results.map((person) => person.id);
|
return results.map((person) => person.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
async createFaces(entities: AssetFaceEntity[]): Promise<string[]> {
|
|
||||||
const res = await this.assetFaceRepository.save(entities);
|
|
||||||
return res.map((row) => row.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
async refreshFaces(
|
async refreshFaces(
|
||||||
facesToAdd: Partial<AssetFaceEntity>[],
|
facesToAdd: Partial<AssetFaceEntity>[],
|
||||||
faceIdsToRemove: string[],
|
faceIdsToRemove: string[],
|
||||||
|
@ -14,12 +14,11 @@ import { AuthDto } from 'src/dtos/auth.dto';
|
|||||||
import { ActivityEntity } from 'src/entities/activity.entity';
|
import { ActivityEntity } from 'src/entities/activity.entity';
|
||||||
import { Permission } from 'src/enum';
|
import { Permission } from 'src/enum';
|
||||||
import { BaseService } from 'src/services/base.service';
|
import { BaseService } from 'src/services/base.service';
|
||||||
import { requireAccess } from 'src/utils/access';
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ActivityService extends BaseService {
|
export class ActivityService extends BaseService {
|
||||||
async getAll(auth: AuthDto, dto: ActivitySearchDto): Promise<ActivityResponseDto[]> {
|
async getAll(auth: AuthDto, dto: ActivitySearchDto): Promise<ActivityResponseDto[]> {
|
||||||
await requireAccess(this.accessRepository, { auth, permission: Permission.ALBUM_READ, ids: [dto.albumId] });
|
await this.requireAccess({ auth, permission: Permission.ALBUM_READ, ids: [dto.albumId] });
|
||||||
const activities = await this.activityRepository.search({
|
const activities = await this.activityRepository.search({
|
||||||
userId: dto.userId,
|
userId: dto.userId,
|
||||||
albumId: dto.albumId,
|
albumId: dto.albumId,
|
||||||
@ -31,12 +30,12 @@ export class ActivityService extends BaseService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getStatistics(auth: AuthDto, dto: ActivityDto): Promise<ActivityStatisticsResponseDto> {
|
async getStatistics(auth: AuthDto, dto: ActivityDto): Promise<ActivityStatisticsResponseDto> {
|
||||||
await requireAccess(this.accessRepository, { auth, permission: Permission.ALBUM_READ, ids: [dto.albumId] });
|
await this.requireAccess({ auth, permission: Permission.ALBUM_READ, ids: [dto.albumId] });
|
||||||
return { comments: await this.activityRepository.getStatistics(dto.assetId, dto.albumId) };
|
return { comments: await this.activityRepository.getStatistics(dto.assetId, dto.albumId) };
|
||||||
}
|
}
|
||||||
|
|
||||||
async create(auth: AuthDto, dto: ActivityCreateDto): Promise<MaybeDuplicate<ActivityResponseDto>> {
|
async create(auth: AuthDto, dto: ActivityCreateDto): Promise<MaybeDuplicate<ActivityResponseDto>> {
|
||||||
await requireAccess(this.accessRepository, { auth, permission: Permission.ACTIVITY_CREATE, ids: [dto.albumId] });
|
await this.requireAccess({ auth, permission: Permission.ACTIVITY_CREATE, ids: [dto.albumId] });
|
||||||
|
|
||||||
const common = {
|
const common = {
|
||||||
userId: auth.user.id,
|
userId: auth.user.id,
|
||||||
@ -70,7 +69,7 @@ export class ActivityService extends BaseService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async delete(auth: AuthDto, id: string): Promise<void> {
|
async delete(auth: AuthDto, id: string): Promise<void> {
|
||||||
await requireAccess(this.accessRepository, { auth, permission: Permission.ACTIVITY_DELETE, ids: [id] });
|
await this.requireAccess({ auth, permission: Permission.ACTIVITY_DELETE, ids: [id] });
|
||||||
await this.activityRepository.delete(id);
|
await this.activityRepository.delete(id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -19,7 +19,6 @@ import { AssetEntity } from 'src/entities/asset.entity';
|
|||||||
import { Permission } from 'src/enum';
|
import { Permission } from 'src/enum';
|
||||||
import { AlbumAssetCount, AlbumInfoOptions } from 'src/interfaces/album.interface';
|
import { AlbumAssetCount, AlbumInfoOptions } from 'src/interfaces/album.interface';
|
||||||
import { BaseService } from 'src/services/base.service';
|
import { BaseService } from 'src/services/base.service';
|
||||||
import { checkAccess, requireAccess } from 'src/utils/access';
|
|
||||||
import { addAssets, removeAssets } from 'src/utils/asset.util';
|
import { addAssets, removeAssets } from 'src/utils/asset.util';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@ -82,7 +81,7 @@ export class AlbumService extends BaseService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async get(auth: AuthDto, id: string, dto: AlbumInfoDto): Promise<AlbumResponseDto> {
|
async get(auth: AuthDto, id: string, dto: AlbumInfoDto): Promise<AlbumResponseDto> {
|
||||||
await requireAccess(this.accessRepository, { auth, permission: Permission.ALBUM_READ, ids: [id] });
|
await this.requireAccess({ auth, permission: Permission.ALBUM_READ, ids: [id] });
|
||||||
await this.albumRepository.updateThumbnails();
|
await this.albumRepository.updateThumbnails();
|
||||||
const withAssets = dto.withoutAssets === undefined ? true : !dto.withoutAssets;
|
const withAssets = dto.withoutAssets === undefined ? true : !dto.withoutAssets;
|
||||||
const album = await this.findOrFail(id, { withAssets });
|
const album = await this.findOrFail(id, { withAssets });
|
||||||
@ -106,7 +105,7 @@ export class AlbumService extends BaseService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const allowedAssetIdsSet = await checkAccess(this.accessRepository, {
|
const allowedAssetIdsSet = await this.checkAccess({
|
||||||
auth,
|
auth,
|
||||||
permission: Permission.ASSET_SHARE,
|
permission: Permission.ASSET_SHARE,
|
||||||
ids: dto.assetIds || [],
|
ids: dto.assetIds || [],
|
||||||
@ -130,7 +129,7 @@ export class AlbumService extends BaseService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async update(auth: AuthDto, id: string, dto: UpdateAlbumDto): Promise<AlbumResponseDto> {
|
async update(auth: AuthDto, id: string, dto: UpdateAlbumDto): Promise<AlbumResponseDto> {
|
||||||
await requireAccess(this.accessRepository, { auth, permission: Permission.ALBUM_UPDATE, ids: [id] });
|
await this.requireAccess({ auth, permission: Permission.ALBUM_UPDATE, ids: [id] });
|
||||||
|
|
||||||
const album = await this.findOrFail(id, { withAssets: true });
|
const album = await this.findOrFail(id, { withAssets: true });
|
||||||
|
|
||||||
@ -153,13 +152,13 @@ export class AlbumService extends BaseService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async delete(auth: AuthDto, id: string): Promise<void> {
|
async delete(auth: AuthDto, id: string): Promise<void> {
|
||||||
await requireAccess(this.accessRepository, { auth, permission: Permission.ALBUM_DELETE, ids: [id] });
|
await this.requireAccess({ auth, permission: Permission.ALBUM_DELETE, ids: [id] });
|
||||||
await this.albumRepository.delete(id);
|
await this.albumRepository.delete(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
async addAssets(auth: AuthDto, id: string, dto: BulkIdsDto): Promise<BulkIdResponseDto[]> {
|
async addAssets(auth: AuthDto, id: string, dto: BulkIdsDto): Promise<BulkIdResponseDto[]> {
|
||||||
const album = await this.findOrFail(id, { withAssets: false });
|
const album = await this.findOrFail(id, { withAssets: false });
|
||||||
await requireAccess(this.accessRepository, { auth, permission: Permission.ALBUM_ADD_ASSET, ids: [id] });
|
await this.requireAccess({ auth, permission: Permission.ALBUM_ADD_ASSET, ids: [id] });
|
||||||
|
|
||||||
const results = await addAssets(
|
const results = await addAssets(
|
||||||
auth,
|
auth,
|
||||||
@ -182,7 +181,7 @@ export class AlbumService extends BaseService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async removeAssets(auth: AuthDto, id: string, dto: BulkIdsDto): Promise<BulkIdResponseDto[]> {
|
async removeAssets(auth: AuthDto, id: string, dto: BulkIdsDto): Promise<BulkIdResponseDto[]> {
|
||||||
await requireAccess(this.accessRepository, { auth, permission: Permission.ALBUM_REMOVE_ASSET, ids: [id] });
|
await this.requireAccess({ auth, permission: Permission.ALBUM_REMOVE_ASSET, ids: [id] });
|
||||||
|
|
||||||
const album = await this.findOrFail(id, { withAssets: false });
|
const album = await this.findOrFail(id, { withAssets: false });
|
||||||
const results = await removeAssets(
|
const results = await removeAssets(
|
||||||
@ -203,7 +202,7 @@ export class AlbumService extends BaseService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async addUsers(auth: AuthDto, id: string, { albumUsers }: AddUsersDto): Promise<AlbumResponseDto> {
|
async addUsers(auth: AuthDto, id: string, { albumUsers }: AddUsersDto): Promise<AlbumResponseDto> {
|
||||||
await requireAccess(this.accessRepository, { auth, permission: Permission.ALBUM_SHARE, ids: [id] });
|
await this.requireAccess({ auth, permission: Permission.ALBUM_SHARE, ids: [id] });
|
||||||
|
|
||||||
const album = await this.findOrFail(id, { withAssets: false });
|
const album = await this.findOrFail(id, { withAssets: false });
|
||||||
|
|
||||||
@ -247,14 +246,14 @@ export class AlbumService extends BaseService {
|
|||||||
|
|
||||||
// non-admin can remove themselves
|
// non-admin can remove themselves
|
||||||
if (auth.user.id !== userId) {
|
if (auth.user.id !== userId) {
|
||||||
await requireAccess(this.accessRepository, { auth, permission: Permission.ALBUM_SHARE, ids: [id] });
|
await this.requireAccess({ auth, permission: Permission.ALBUM_SHARE, ids: [id] });
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.albumUserRepository.delete({ albumId: id, userId });
|
await this.albumUserRepository.delete({ albumId: id, userId });
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateUser(auth: AuthDto, id: string, userId: string, dto: Partial<AlbumUserEntity>): Promise<void> {
|
async updateUser(auth: AuthDto, id: string, userId: string, dto: Partial<AlbumUserEntity>): Promise<void> {
|
||||||
await requireAccess(this.accessRepository, { auth, permission: Permission.ALBUM_SHARE, ids: [id] });
|
await this.requireAccess({ auth, permission: Permission.ALBUM_SHARE, ids: [id] });
|
||||||
await this.albumUserRepository.update({ albumId: id, userId }, { role: dto.role });
|
await this.albumUserRepository.update({ albumId: id, userId }, { role: dto.role });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -539,6 +539,7 @@ describe(AssetMediaService.name, () => {
|
|||||||
path: '/path/to/preview',
|
path: '/path/to/preview',
|
||||||
type: AssetFileType.THUMBNAIL,
|
type: AssetFileType.THUMBNAIL,
|
||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
|
checksum: null,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
@ -559,6 +560,7 @@ describe(AssetMediaService.name, () => {
|
|||||||
path: '/path/to/preview.jpg',
|
path: '/path/to/preview.jpg',
|
||||||
type: AssetFileType.PREVIEW,
|
type: AssetFileType.PREVIEW,
|
||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
|
checksum: null,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
@ -24,7 +24,7 @@ import { ASSET_CHECKSUM_CONSTRAINT, AssetEntity } from 'src/entities/asset.entit
|
|||||||
import { AssetFileType, AssetStatus, AssetType, CacheControl, Permission, StorageFolder } from 'src/enum';
|
import { AssetFileType, AssetStatus, AssetType, CacheControl, Permission, StorageFolder } from 'src/enum';
|
||||||
import { JobName } from 'src/interfaces/job.interface';
|
import { JobName } from 'src/interfaces/job.interface';
|
||||||
import { BaseService } from 'src/services/base.service';
|
import { BaseService } from 'src/services/base.service';
|
||||||
import { requireAccess, requireUploadAccess } from 'src/utils/access';
|
import { requireUploadAccess } from 'src/utils/access';
|
||||||
import { getAssetFiles, onBeforeLink } from 'src/utils/asset.util';
|
import { getAssetFiles, onBeforeLink } from 'src/utils/asset.util';
|
||||||
import { ImmichFileResponse } from 'src/utils/file';
|
import { ImmichFileResponse } from 'src/utils/file';
|
||||||
import { mimeTypes } from 'src/utils/mime-types';
|
import { mimeTypes } from 'src/utils/mime-types';
|
||||||
@ -39,7 +39,7 @@ export interface UploadRequest {
|
|||||||
export interface UploadFile {
|
export interface UploadFile {
|
||||||
uuid: string;
|
uuid: string;
|
||||||
checksum: Buffer;
|
checksum: Buffer;
|
||||||
xxhash: BigInt;
|
xxhash?: Buffer;
|
||||||
originalPath: string;
|
originalPath: string;
|
||||||
originalName: string;
|
originalName: string;
|
||||||
size: number;
|
size: number;
|
||||||
@ -126,7 +126,7 @@ export class AssetMediaService extends BaseService {
|
|||||||
sidecarFile?: UploadFile,
|
sidecarFile?: UploadFile,
|
||||||
): Promise<AssetMediaResponseDto> {
|
): Promise<AssetMediaResponseDto> {
|
||||||
try {
|
try {
|
||||||
await requireAccess(this.accessRepository, {
|
await this.requireAccess({
|
||||||
auth,
|
auth,
|
||||||
permission: Permission.ASSET_UPLOAD,
|
permission: Permission.ASSET_UPLOAD,
|
||||||
// do not need an id here, but the interface requires it
|
// do not need an id here, but the interface requires it
|
||||||
@ -160,7 +160,7 @@ export class AssetMediaService extends BaseService {
|
|||||||
sidecarFile?: UploadFile,
|
sidecarFile?: UploadFile,
|
||||||
): Promise<AssetMediaResponseDto> {
|
): Promise<AssetMediaResponseDto> {
|
||||||
try {
|
try {
|
||||||
await requireAccess(this.accessRepository, { auth, permission: Permission.ASSET_UPDATE, ids: [id] });
|
await this.requireAccess({ auth, permission: Permission.ASSET_UPDATE, ids: [id] });
|
||||||
const asset = (await this.assetRepository.getById(id)) as AssetEntity;
|
const asset = (await this.assetRepository.getById(id)) as AssetEntity;
|
||||||
|
|
||||||
this.requireQuota(auth, file.size);
|
this.requireQuota(auth, file.size);
|
||||||
@ -183,7 +183,7 @@ export class AssetMediaService extends BaseService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async downloadOriginal(auth: AuthDto, id: string): Promise<ImmichFileResponse> {
|
async downloadOriginal(auth: AuthDto, id: string): Promise<ImmichFileResponse> {
|
||||||
await requireAccess(this.accessRepository, { auth, permission: Permission.ASSET_DOWNLOAD, ids: [id] });
|
await this.requireAccess({ auth, permission: Permission.ASSET_DOWNLOAD, ids: [id] });
|
||||||
|
|
||||||
const asset = await this.findOrFail(id);
|
const asset = await this.findOrFail(id);
|
||||||
|
|
||||||
@ -195,7 +195,7 @@ export class AssetMediaService extends BaseService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async viewThumbnail(auth: AuthDto, id: string, dto: AssetMediaOptionsDto): Promise<ImmichFileResponse> {
|
async viewThumbnail(auth: AuthDto, id: string, dto: AssetMediaOptionsDto): Promise<ImmichFileResponse> {
|
||||||
await requireAccess(this.accessRepository, { auth, permission: Permission.ASSET_VIEW, ids: [id] });
|
await this.requireAccess({ auth, permission: Permission.ASSET_VIEW, ids: [id] });
|
||||||
|
|
||||||
const asset = await this.findOrFail(id);
|
const asset = await this.findOrFail(id);
|
||||||
const size = dto.size ?? AssetMediaSize.THUMBNAIL;
|
const size = dto.size ?? AssetMediaSize.THUMBNAIL;
|
||||||
@ -218,7 +218,7 @@ export class AssetMediaService extends BaseService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async playbackVideo(auth: AuthDto, id: string): Promise<ImmichFileResponse> {
|
async playbackVideo(auth: AuthDto, id: string): Promise<ImmichFileResponse> {
|
||||||
await requireAccess(this.accessRepository, { auth, permission: Permission.ASSET_VIEW, ids: [id] });
|
await this.requireAccess({ auth, permission: Permission.ASSET_VIEW, ids: [id] });
|
||||||
|
|
||||||
const asset = await this.findOrFail(id);
|
const asset = await this.findOrFail(id);
|
||||||
|
|
||||||
@ -342,8 +342,6 @@ export class AssetMediaService extends BaseService {
|
|||||||
checksum: file.xxhash,
|
checksum: file.xxhash,
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log('xxhash', file.xxhash);
|
|
||||||
|
|
||||||
await this.storageRepository.utimes(file.originalPath, new Date(), new Date(dto.fileModifiedAt));
|
await this.storageRepository.utimes(file.originalPath, new Date(), new Date(dto.fileModifiedAt));
|
||||||
await this.assetRepository.upsertExif({ assetId, fileSizeInByte: file.size });
|
await this.assetRepository.upsertExif({ assetId, fileSizeInByte: file.size });
|
||||||
await this.jobRepository.queue({
|
await this.jobRepository.queue({
|
||||||
|
@ -29,7 +29,6 @@ import {
|
|||||||
JobStatus,
|
JobStatus,
|
||||||
} from 'src/interfaces/job.interface';
|
} from 'src/interfaces/job.interface';
|
||||||
import { BaseService } from 'src/services/base.service';
|
import { BaseService } from 'src/services/base.service';
|
||||||
import { requireAccess } from 'src/utils/access';
|
|
||||||
import { getAssetFiles, getMyPartnerIds, onAfterUnlink, onBeforeLink, onBeforeUnlink } from 'src/utils/asset.util';
|
import { getAssetFiles, getMyPartnerIds, onAfterUnlink, onBeforeLink, onBeforeUnlink } from 'src/utils/asset.util';
|
||||||
import { usePagination } from 'src/utils/pagination';
|
import { usePagination } from 'src/utils/pagination';
|
||||||
|
|
||||||
@ -86,7 +85,7 @@ export class AssetService extends BaseService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async get(auth: AuthDto, id: string): Promise<AssetResponseDto | SanitizedAssetResponseDto> {
|
async get(auth: AuthDto, id: string): Promise<AssetResponseDto | SanitizedAssetResponseDto> {
|
||||||
await requireAccess(this.accessRepository, { auth, permission: Permission.ASSET_READ, ids: [id] });
|
await this.requireAccess({ auth, permission: Permission.ASSET_READ, ids: [id] });
|
||||||
|
|
||||||
const asset = await this.assetRepository.getById(
|
const asset = await this.assetRepository.getById(
|
||||||
id,
|
id,
|
||||||
@ -135,7 +134,7 @@ export class AssetService extends BaseService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async update(auth: AuthDto, id: string, dto: UpdateAssetDto): Promise<AssetResponseDto> {
|
async update(auth: AuthDto, id: string, dto: UpdateAssetDto): Promise<AssetResponseDto> {
|
||||||
await requireAccess(this.accessRepository, { auth, permission: Permission.ASSET_UPDATE, ids: [id] });
|
await this.requireAccess({ auth, permission: Permission.ASSET_UPDATE, ids: [id] });
|
||||||
|
|
||||||
const { description, dateTimeOriginal, latitude, longitude, rating, ...rest } = dto;
|
const { description, dateTimeOriginal, latitude, longitude, rating, ...rest } = dto;
|
||||||
const repos = { asset: this.assetRepository, event: this.eventRepository };
|
const repos = { asset: this.assetRepository, event: this.eventRepository };
|
||||||
@ -178,7 +177,7 @@ export class AssetService extends BaseService {
|
|||||||
|
|
||||||
async updateAll(auth: AuthDto, dto: AssetBulkUpdateDto): Promise<void> {
|
async updateAll(auth: AuthDto, dto: AssetBulkUpdateDto): Promise<void> {
|
||||||
const { ids, dateTimeOriginal, latitude, longitude, ...options } = dto;
|
const { ids, dateTimeOriginal, latitude, longitude, ...options } = dto;
|
||||||
await requireAccess(this.accessRepository, { auth, permission: Permission.ASSET_UPDATE, ids });
|
await this.requireAccess({ auth, permission: Permission.ASSET_UPDATE, ids });
|
||||||
|
|
||||||
for (const id of ids) {
|
for (const id of ids) {
|
||||||
await this.updateMetadata({ id, dateTimeOriginal, latitude, longitude });
|
await this.updateMetadata({ id, dateTimeOriginal, latitude, longitude });
|
||||||
@ -275,7 +274,7 @@ export class AssetService extends BaseService {
|
|||||||
async deleteAll(auth: AuthDto, dto: AssetBulkDeleteDto): Promise<void> {
|
async deleteAll(auth: AuthDto, dto: AssetBulkDeleteDto): Promise<void> {
|
||||||
const { ids, force } = dto;
|
const { ids, force } = dto;
|
||||||
|
|
||||||
await requireAccess(this.accessRepository, { auth, permission: Permission.ASSET_DELETE, ids });
|
await this.requireAccess({ auth, permission: Permission.ASSET_DELETE, ids });
|
||||||
await this.assetRepository.updateAll(ids, {
|
await this.assetRepository.updateAll(ids, {
|
||||||
deletedAt: new Date(),
|
deletedAt: new Date(),
|
||||||
status: force ? AssetStatus.DELETED : AssetStatus.TRASHED,
|
status: force ? AssetStatus.DELETED : AssetStatus.TRASHED,
|
||||||
@ -284,7 +283,7 @@ export class AssetService extends BaseService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async run(auth: AuthDto, dto: AssetJobsDto) {
|
async run(auth: AuthDto, dto: AssetJobsDto) {
|
||||||
await requireAccess(this.accessRepository, { auth, permission: Permission.ASSET_UPDATE, ids: dto.assetIds });
|
await this.requireAccess({ auth, permission: Permission.ASSET_UPDATE, ids: dto.assetIds });
|
||||||
|
|
||||||
const jobs: JobItem[] = [];
|
const jobs: JobItem[] = [];
|
||||||
|
|
||||||
|
@ -23,7 +23,6 @@ import {
|
|||||||
} from 'src/enum';
|
} from 'src/enum';
|
||||||
import { JOBS_ASSET_PAGINATION_SIZE, JobStatus } from 'src/interfaces/job.interface';
|
import { JOBS_ASSET_PAGINATION_SIZE, JobStatus } from 'src/interfaces/job.interface';
|
||||||
import { BaseService } from 'src/services/base.service';
|
import { BaseService } from 'src/services/base.service';
|
||||||
import { requireAccess } from 'src/utils/access';
|
|
||||||
import { getAssetFiles } from 'src/utils/asset.util';
|
import { getAssetFiles } from 'src/utils/asset.util';
|
||||||
import { usePagination } from 'src/utils/pagination';
|
import { usePagination } from 'src/utils/pagination';
|
||||||
|
|
||||||
@ -36,7 +35,7 @@ export class AuditService extends BaseService {
|
|||||||
|
|
||||||
async getDeletes(auth: AuthDto, dto: AuditDeletesDto): Promise<AuditDeletesResponseDto> {
|
async getDeletes(auth: AuthDto, dto: AuditDeletesDto): Promise<AuditDeletesResponseDto> {
|
||||||
const userId = dto.userId || auth.user.id;
|
const userId = dto.userId || auth.user.id;
|
||||||
await requireAccess(this.accessRepository, { auth, permission: Permission.TIMELINE_READ, ids: [userId] });
|
await this.requireAccess({ auth, permission: Permission.TIMELINE_READ, ids: [userId] });
|
||||||
|
|
||||||
const audits = await this.auditRepository.getAfter(dto.after, {
|
const audits = await this.auditRepository.getAfter(dto.after, {
|
||||||
userIds: [userId],
|
userIds: [userId],
|
||||||
|
@ -13,7 +13,7 @@ import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interf
|
|||||||
import { IUserRepository } from 'src/interfaces/user.interface';
|
import { IUserRepository } from 'src/interfaces/user.interface';
|
||||||
import { AuthService } from 'src/services/auth.service';
|
import { AuthService } from 'src/services/auth.service';
|
||||||
import { keyStub } from 'test/fixtures/api-key.stub';
|
import { keyStub } from 'test/fixtures/api-key.stub';
|
||||||
import { authStub, loginResponseStub } from 'test/fixtures/auth.stub';
|
import { authStub } from 'test/fixtures/auth.stub';
|
||||||
import { sessionStub } from 'test/fixtures/session.stub';
|
import { sessionStub } from 'test/fixtures/session.stub';
|
||||||
import { sharedLinkStub } from 'test/fixtures/shared-link.stub';
|
import { sharedLinkStub } from 'test/fixtures/shared-link.stub';
|
||||||
import { systemConfigStub } from 'test/fixtures/system-config.stub';
|
import { systemConfigStub } from 'test/fixtures/system-config.stub';
|
||||||
@ -21,6 +21,16 @@ import { userStub } from 'test/fixtures/user.stub';
|
|||||||
import { newTestService } from 'test/utils';
|
import { newTestService } from 'test/utils';
|
||||||
import { Mocked } from 'vitest';
|
import { Mocked } from 'vitest';
|
||||||
|
|
||||||
|
const oauthResponse = {
|
||||||
|
accessToken: 'cmFuZG9tLWJ5dGVz',
|
||||||
|
userId: 'user-id',
|
||||||
|
userEmail: 'immich@test.com',
|
||||||
|
name: 'immich_name',
|
||||||
|
profileImagePath: '',
|
||||||
|
isAdmin: false,
|
||||||
|
shouldChangePassword: false,
|
||||||
|
};
|
||||||
|
|
||||||
// const token = Buffer.from('my-api-key', 'utf8').toString('base64');
|
// const token = Buffer.from('my-api-key', 'utf8').toString('base64');
|
||||||
|
|
||||||
const email = 'test@immich.com';
|
const email = 'test@immich.com';
|
||||||
@ -100,7 +110,15 @@ describe('AuthService', () => {
|
|||||||
it('should successfully log the user in', async () => {
|
it('should successfully log the user in', async () => {
|
||||||
userMock.getByEmail.mockResolvedValue(userStub.user1);
|
userMock.getByEmail.mockResolvedValue(userStub.user1);
|
||||||
sessionMock.create.mockResolvedValue(sessionStub.valid);
|
sessionMock.create.mockResolvedValue(sessionStub.valid);
|
||||||
await expect(sut.login(fixtures.login, loginDetails)).resolves.toEqual(loginResponseStub.user1password);
|
await expect(sut.login(fixtures.login, loginDetails)).resolves.toEqual({
|
||||||
|
accessToken: 'cmFuZG9tLWJ5dGVz',
|
||||||
|
userId: 'user-id',
|
||||||
|
userEmail: 'immich@test.com',
|
||||||
|
name: 'immich_name',
|
||||||
|
profileImagePath: '',
|
||||||
|
isAdmin: false,
|
||||||
|
shouldChangePassword: false,
|
||||||
|
});
|
||||||
expect(userMock.getByEmail).toHaveBeenCalledTimes(1);
|
expect(userMock.getByEmail).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -469,7 +487,7 @@ describe('AuthService', () => {
|
|||||||
sessionMock.create.mockResolvedValue(sessionStub.valid);
|
sessionMock.create.mockResolvedValue(sessionStub.valid);
|
||||||
|
|
||||||
await expect(sut.callback({ url: 'http://immich/auth/login?code=abc123' }, loginDetails)).resolves.toEqual(
|
await expect(sut.callback({ url: 'http://immich/auth/login?code=abc123' }, loginDetails)).resolves.toEqual(
|
||||||
loginResponseStub.user1oauth,
|
oauthResponse,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(userMock.getByEmail).toHaveBeenCalledTimes(1);
|
expect(userMock.getByEmail).toHaveBeenCalledTimes(1);
|
||||||
@ -498,7 +516,7 @@ describe('AuthService', () => {
|
|||||||
sessionMock.create.mockResolvedValue(sessionStub.valid);
|
sessionMock.create.mockResolvedValue(sessionStub.valid);
|
||||||
|
|
||||||
await expect(sut.callback({ url: 'http://immich/auth/login?code=abc123' }, loginDetails)).resolves.toEqual(
|
await expect(sut.callback({ url: 'http://immich/auth/login?code=abc123' }, loginDetails)).resolves.toEqual(
|
||||||
loginResponseStub.user1oauth,
|
oauthResponse,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(userMock.getByEmail).toHaveBeenCalledTimes(2); // second call is for domain check before create
|
expect(userMock.getByEmail).toHaveBeenCalledTimes(2); // second call is for domain check before create
|
||||||
@ -546,7 +564,7 @@ describe('AuthService', () => {
|
|||||||
userMock.create.mockResolvedValue(userStub.user1);
|
userMock.create.mockResolvedValue(userStub.user1);
|
||||||
|
|
||||||
await expect(sut.callback({ url: 'http://immich/auth/login?code=abc123' }, loginDetails)).resolves.toEqual(
|
await expect(sut.callback({ url: 'http://immich/auth/login?code=abc123' }, loginDetails)).resolves.toEqual(
|
||||||
loginResponseStub.user1oauth,
|
oauthResponse,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(userMock.create).toHaveBeenCalledWith(oauthUserWithDefaultQuota);
|
expect(userMock.create).toHaveBeenCalledWith(oauthUserWithDefaultQuota);
|
||||||
@ -560,7 +578,7 @@ describe('AuthService', () => {
|
|||||||
oauthMock.getProfile.mockResolvedValue({ sub, email, immich_quota: 'abc' });
|
oauthMock.getProfile.mockResolvedValue({ sub, email, immich_quota: 'abc' });
|
||||||
|
|
||||||
await expect(sut.callback({ url: 'http://immich/auth/login?code=abc123' }, loginDetails)).resolves.toEqual(
|
await expect(sut.callback({ url: 'http://immich/auth/login?code=abc123' }, loginDetails)).resolves.toEqual(
|
||||||
loginResponseStub.user1oauth,
|
oauthResponse,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(userMock.create).toHaveBeenCalledWith(oauthUserWithDefaultQuota);
|
expect(userMock.create).toHaveBeenCalledWith(oauthUserWithDefaultQuota);
|
||||||
@ -574,7 +592,7 @@ describe('AuthService', () => {
|
|||||||
oauthMock.getProfile.mockResolvedValue({ sub, email, immich_quota: -5 });
|
oauthMock.getProfile.mockResolvedValue({ sub, email, immich_quota: -5 });
|
||||||
|
|
||||||
await expect(sut.callback({ url: 'http://immich/auth/login?code=abc123' }, loginDetails)).resolves.toEqual(
|
await expect(sut.callback({ url: 'http://immich/auth/login?code=abc123' }, loginDetails)).resolves.toEqual(
|
||||||
loginResponseStub.user1oauth,
|
oauthResponse,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(userMock.create).toHaveBeenCalledWith(oauthUserWithDefaultQuota);
|
expect(userMock.create).toHaveBeenCalledWith(oauthUserWithDefaultQuota);
|
||||||
@ -588,7 +606,7 @@ describe('AuthService', () => {
|
|||||||
oauthMock.getProfile.mockResolvedValue({ sub, email, immich_quota: 0 });
|
oauthMock.getProfile.mockResolvedValue({ sub, email, immich_quota: 0 });
|
||||||
|
|
||||||
await expect(sut.callback({ url: 'http://immich/auth/login?code=abc123' }, loginDetails)).resolves.toEqual(
|
await expect(sut.callback({ url: 'http://immich/auth/login?code=abc123' }, loginDetails)).resolves.toEqual(
|
||||||
loginResponseStub.user1oauth,
|
oauthResponse,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(userMock.create).toHaveBeenCalledWith({
|
expect(userMock.create).toHaveBeenCalledWith({
|
||||||
@ -608,7 +626,7 @@ describe('AuthService', () => {
|
|||||||
oauthMock.getProfile.mockResolvedValue({ sub, email, immich_quota: 5 });
|
oauthMock.getProfile.mockResolvedValue({ sub, email, immich_quota: 5 });
|
||||||
|
|
||||||
await expect(sut.callback({ url: 'http://immich/auth/login?code=abc123' }, loginDetails)).resolves.toEqual(
|
await expect(sut.callback({ url: 'http://immich/auth/login?code=abc123' }, loginDetails)).resolves.toEqual(
|
||||||
loginResponseStub.user1oauth,
|
oauthResponse,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(userMock.create).toHaveBeenCalledWith({
|
expect(userMock.create).toHaveBeenCalledWith({
|
||||||
|
@ -38,6 +38,7 @@ import { ITrashRepository } from 'src/interfaces/trash.interface';
|
|||||||
import { IUserRepository } from 'src/interfaces/user.interface';
|
import { IUserRepository } from 'src/interfaces/user.interface';
|
||||||
import { IVersionHistoryRepository } from 'src/interfaces/version-history.interface';
|
import { IVersionHistoryRepository } from 'src/interfaces/version-history.interface';
|
||||||
import { IViewRepository } from 'src/interfaces/view.interface';
|
import { IViewRepository } from 'src/interfaces/view.interface';
|
||||||
|
import { AccessRequest, checkAccess, requireAccess } from 'src/utils/access';
|
||||||
import { getConfig, updateConfig } from 'src/utils/config';
|
import { getConfig, updateConfig } from 'src/utils/config';
|
||||||
|
|
||||||
export class BaseService {
|
export class BaseService {
|
||||||
@ -95,7 +96,7 @@ export class BaseService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private get repos() {
|
private get configRepos() {
|
||||||
return {
|
return {
|
||||||
configRepo: this.configRepository,
|
configRepo: this.configRepository,
|
||||||
metadataRepo: this.systemMetadataRepository,
|
metadataRepo: this.systemMetadataRepository,
|
||||||
@ -104,10 +105,18 @@ export class BaseService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getConfig(options: { withCache: boolean }) {
|
getConfig(options: { withCache: boolean }) {
|
||||||
return getConfig(this.repos, options);
|
return getConfig(this.configRepos, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
updateConfig(newConfig: SystemConfig) {
|
updateConfig(newConfig: SystemConfig) {
|
||||||
return updateConfig(this.repos, newConfig);
|
return updateConfig(this.configRepos, newConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
requireAccess(request: AccessRequest) {
|
||||||
|
return requireAccess(this.accessRepository, request);
|
||||||
|
}
|
||||||
|
|
||||||
|
checkAccess(request: AccessRequest) {
|
||||||
|
return checkAccess(this.accessRepository, request);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
|
||||||
import { IUserRepository } from 'src/interfaces/user.interface';
|
import { IUserRepository } from 'src/interfaces/user.interface';
|
||||||
import { CliService } from 'src/services/cli.service';
|
import { CliService } from 'src/services/cli.service';
|
||||||
import { userStub } from 'test/fixtures/user.stub';
|
import { userStub } from 'test/fixtures/user.stub';
|
||||||
@ -8,9 +9,18 @@ describe(CliService.name, () => {
|
|||||||
let sut: CliService;
|
let sut: CliService;
|
||||||
|
|
||||||
let userMock: Mocked<IUserRepository>;
|
let userMock: Mocked<IUserRepository>;
|
||||||
|
let systemMock: Mocked<ISystemMetadataRepository>;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
({ sut, userMock } = newTestService(CliService));
|
({ sut, userMock, systemMock } = newTestService(CliService));
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('listUsers', () => {
|
||||||
|
it('should list users', async () => {
|
||||||
|
userMock.getList.mockResolvedValue([userStub.admin]);
|
||||||
|
await expect(sut.listUsers()).resolves.toEqual([expect.objectContaining({ isAdmin: true })]);
|
||||||
|
expect(userMock.getList).toHaveBeenCalledWith({ withDeleted: true });
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('resetAdminPassword', () => {
|
describe('resetAdminPassword', () => {
|
||||||
@ -51,4 +61,32 @@ describe(CliService.name, () => {
|
|||||||
expect(update.password).toBeDefined();
|
expect(update.password).toBeDefined();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('disablePasswordLogin', () => {
|
||||||
|
it('should disable password login', async () => {
|
||||||
|
await sut.disablePasswordLogin();
|
||||||
|
expect(systemMock.set).toHaveBeenCalledWith('system-config', { passwordLogin: { enabled: false } });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('enablePasswordLogin', () => {
|
||||||
|
it('should enable password login', async () => {
|
||||||
|
await sut.enablePasswordLogin();
|
||||||
|
expect(systemMock.set).toHaveBeenCalledWith('system-config', {});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('disableOAuthLogin', () => {
|
||||||
|
it('should disable oauth login', async () => {
|
||||||
|
await sut.disableOAuthLogin();
|
||||||
|
expect(systemMock.set).toHaveBeenCalledWith('system-config', {});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('enableOAuthLogin', () => {
|
||||||
|
it('should enable oauth login', async () => {
|
||||||
|
await sut.enableOAuthLogin();
|
||||||
|
expect(systemMock.set).toHaveBeenCalledWith('system-config', { oauth: { enabled: true } });
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -43,6 +43,7 @@ describe(DatabaseService.name, () => {
|
|||||||
expect(sut).toBeDefined();
|
expect(sut).toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('onBootstrap', () => {
|
||||||
it('should throw an error if PostgreSQL version is below minimum supported version', async () => {
|
it('should throw an error if PostgreSQL version is below minimum supported version', async () => {
|
||||||
databaseMock.getPostgresVersion.mockResolvedValueOnce('13.10.0');
|
databaseMock.getPostgresVersion.mockResolvedValueOnce('13.10.0');
|
||||||
|
|
||||||
@ -195,6 +196,21 @@ describe(DatabaseService.name, () => {
|
|||||||
expect(loggerMock.fatal).not.toHaveBeenCalled();
|
expect(loggerMock.fatal).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should throw error if installed version is not in version range', async () => {
|
||||||
|
databaseMock.getExtensionVersion.mockResolvedValue({
|
||||||
|
availableVersion: minVersionInRange,
|
||||||
|
installedVersion: versionAboveRange,
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(sut.onBootstrap()).rejects.toThrow(
|
||||||
|
`The ${extensionName} extension version is ${versionAboveRange}, but Immich only supports`,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(databaseMock.updateVectorExtension).not.toHaveBeenCalled();
|
||||||
|
expect(databaseMock.runMigrations).not.toHaveBeenCalled();
|
||||||
|
expect(loggerMock.fatal).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
it(`should raise error if ${extension} extension upgrade failed`, async () => {
|
it(`should raise error if ${extension} extension upgrade failed`, async () => {
|
||||||
databaseMock.getExtensionVersion.mockResolvedValue({
|
databaseMock.getExtensionVersion.mockResolvedValue({
|
||||||
availableVersion: updateInRange,
|
availableVersion: updateInRange,
|
||||||
@ -239,6 +255,21 @@ describe(DatabaseService.name, () => {
|
|||||||
expect(loggerMock.fatal).not.toHaveBeenCalled();
|
expect(loggerMock.fatal).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it(`should throw an error if reindexing fails`, async () => {
|
||||||
|
databaseMock.shouldReindex.mockResolvedValue(true);
|
||||||
|
databaseMock.reindex.mockRejectedValue(new Error('Error reindexing'));
|
||||||
|
|
||||||
|
await expect(sut.onBootstrap()).rejects.toBeDefined();
|
||||||
|
|
||||||
|
expect(databaseMock.shouldReindex).toHaveBeenCalledTimes(1);
|
||||||
|
expect(databaseMock.reindex).toHaveBeenCalledTimes(1);
|
||||||
|
expect(databaseMock.runMigrations).not.toHaveBeenCalled();
|
||||||
|
expect(loggerMock.fatal).not.toHaveBeenCalled();
|
||||||
|
expect(loggerMock.warn).toHaveBeenCalledWith(
|
||||||
|
expect.stringContaining('Could not run vector reindexing checks.'),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it(`should not reindex ${extension} indices if not needed`, async () => {
|
it(`should not reindex ${extension} indices if not needed`, async () => {
|
||||||
databaseMock.shouldReindex.mockResolvedValue(false);
|
databaseMock.shouldReindex.mockResolvedValue(false);
|
||||||
|
|
||||||
@ -322,3 +353,50 @@ describe(DatabaseService.name, () => {
|
|||||||
expect(databaseMock.runMigrations).not.toHaveBeenCalled();
|
expect(databaseMock.runMigrations).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('handleConnectionError', () => {
|
||||||
|
beforeAll(() => {
|
||||||
|
vi.useFakeTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(() => {
|
||||||
|
vi.useRealTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not override interval', () => {
|
||||||
|
sut.handleConnectionError(new Error('Error'));
|
||||||
|
expect(loggerMock.error).toHaveBeenCalled();
|
||||||
|
|
||||||
|
sut.handleConnectionError(new Error('foo'));
|
||||||
|
expect(loggerMock.error).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reconnect when interval elapses', async () => {
|
||||||
|
databaseMock.reconnect.mockResolvedValue(true);
|
||||||
|
|
||||||
|
sut.handleConnectionError(new Error('error'));
|
||||||
|
await vi.advanceTimersByTimeAsync(5000);
|
||||||
|
|
||||||
|
expect(databaseMock.reconnect).toHaveBeenCalledTimes(1);
|
||||||
|
expect(loggerMock.log).toHaveBeenCalledWith('Database reconnected');
|
||||||
|
|
||||||
|
await vi.advanceTimersByTimeAsync(5000);
|
||||||
|
expect(databaseMock.reconnect).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should try again when reconnection fails', async () => {
|
||||||
|
databaseMock.reconnect.mockResolvedValueOnce(false);
|
||||||
|
|
||||||
|
sut.handleConnectionError(new Error('error'));
|
||||||
|
await vi.advanceTimersByTimeAsync(5000);
|
||||||
|
|
||||||
|
expect(databaseMock.reconnect).toHaveBeenCalledTimes(1);
|
||||||
|
expect(loggerMock.warn).toHaveBeenCalledWith(expect.stringContaining('Database connection failed'));
|
||||||
|
|
||||||
|
databaseMock.reconnect.mockResolvedValueOnce(true);
|
||||||
|
await vi.advanceTimersByTimeAsync(5000);
|
||||||
|
expect(databaseMock.reconnect).toHaveBeenCalledTimes(2);
|
||||||
|
expect(loggerMock.log).toHaveBeenCalledWith('Database reconnected');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
@ -8,7 +8,6 @@ import { AssetEntity } from 'src/entities/asset.entity';
|
|||||||
import { Permission } from 'src/enum';
|
import { Permission } from 'src/enum';
|
||||||
import { ImmichReadStream } from 'src/interfaces/storage.interface';
|
import { ImmichReadStream } from 'src/interfaces/storage.interface';
|
||||||
import { BaseService } from 'src/services/base.service';
|
import { BaseService } from 'src/services/base.service';
|
||||||
import { requireAccess } from 'src/utils/access';
|
|
||||||
import { HumanReadableSize } from 'src/utils/bytes';
|
import { HumanReadableSize } from 'src/utils/bytes';
|
||||||
import { usePagination } from 'src/utils/pagination';
|
import { usePagination } from 'src/utils/pagination';
|
||||||
import { getPreferences } from 'src/utils/preferences';
|
import { getPreferences } from 'src/utils/preferences';
|
||||||
@ -62,7 +61,7 @@ export class DownloadService extends BaseService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async downloadArchive(auth: AuthDto, dto: AssetIdsDto): Promise<ImmichReadStream> {
|
async downloadArchive(auth: AuthDto, dto: AssetIdsDto): Promise<ImmichReadStream> {
|
||||||
await requireAccess(this.accessRepository, { auth, permission: Permission.ASSET_DOWNLOAD, ids: dto.assetIds });
|
await this.requireAccess({ auth, permission: Permission.ASSET_DOWNLOAD, ids: dto.assetIds });
|
||||||
|
|
||||||
const zip = this.storageRepository.createZipStream();
|
const zip = this.storageRepository.createZipStream();
|
||||||
const assets = await this.assetRepository.getByIds(dto.assetIds);
|
const assets = await this.assetRepository.getByIds(dto.assetIds);
|
||||||
@ -105,20 +104,20 @@ export class DownloadService extends BaseService {
|
|||||||
|
|
||||||
if (dto.assetIds) {
|
if (dto.assetIds) {
|
||||||
const assetIds = dto.assetIds;
|
const assetIds = dto.assetIds;
|
||||||
await requireAccess(this.accessRepository, { auth, permission: Permission.ASSET_DOWNLOAD, ids: assetIds });
|
await this.requireAccess({ auth, permission: Permission.ASSET_DOWNLOAD, ids: assetIds });
|
||||||
const assets = await this.assetRepository.getByIds(assetIds, { exifInfo: true });
|
const assets = await this.assetRepository.getByIds(assetIds, { exifInfo: true });
|
||||||
return usePagination(PAGINATION_SIZE, () => ({ hasNextPage: false, items: assets }));
|
return usePagination(PAGINATION_SIZE, () => ({ hasNextPage: false, items: assets }));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (dto.albumId) {
|
if (dto.albumId) {
|
||||||
const albumId = dto.albumId;
|
const albumId = dto.albumId;
|
||||||
await requireAccess(this.accessRepository, { auth, permission: Permission.ALBUM_DOWNLOAD, ids: [albumId] });
|
await this.requireAccess({ auth, permission: Permission.ALBUM_DOWNLOAD, ids: [albumId] });
|
||||||
return usePagination(PAGINATION_SIZE, (pagination) => this.assetRepository.getByAlbumId(pagination, albumId));
|
return usePagination(PAGINATION_SIZE, (pagination) => this.assetRepository.getByAlbumId(pagination, albumId));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (dto.userId) {
|
if (dto.userId) {
|
||||||
const userId = dto.userId;
|
const userId = dto.userId;
|
||||||
await requireAccess(this.accessRepository, { auth, permission: Permission.TIMELINE_DOWNLOAD, ids: [userId] });
|
await this.requireAccess({ auth, permission: Permission.TIMELINE_DOWNLOAD, ids: [userId] });
|
||||||
return usePagination(PAGINATION_SIZE, (pagination) =>
|
return usePagination(PAGINATION_SIZE, (pagination) =>
|
||||||
this.assetRepository.getByUserId(pagination, userId, { isVisible: true }),
|
this.assetRepository.getByUserId(pagination, userId, { isVisible: true }),
|
||||||
);
|
);
|
||||||
|
@ -141,8 +141,6 @@ describe(LibraryService.name, () => {
|
|||||||
|
|
||||||
describe('handleQueueAssetRefresh', () => {
|
describe('handleQueueAssetRefresh', () => {
|
||||||
it('should queue refresh of a new asset', async () => {
|
it('should queue refresh of a new asset', async () => {
|
||||||
assetMock.getWith.mockResolvedValue({ items: [], hasNextPage: false });
|
|
||||||
|
|
||||||
libraryMock.get.mockResolvedValue(libraryStub.externalLibrary1);
|
libraryMock.get.mockResolvedValue(libraryStub.externalLibrary1);
|
||||||
storageMock.walk.mockImplementation(mockWalk);
|
storageMock.walk.mockImplementation(mockWalk);
|
||||||
|
|
||||||
@ -179,8 +177,6 @@ describe(LibraryService.name, () => {
|
|||||||
|
|
||||||
storageMock.checkFileExists.mockResolvedValue(true);
|
storageMock.checkFileExists.mockResolvedValue(true);
|
||||||
|
|
||||||
assetMock.getWith.mockResolvedValue({ items: [], hasNextPage: false });
|
|
||||||
|
|
||||||
libraryMock.get.mockResolvedValue(libraryStub.externalLibraryWithImportPaths1);
|
libraryMock.get.mockResolvedValue(libraryStub.externalLibraryWithImportPaths1);
|
||||||
|
|
||||||
await sut.handleQueueSyncFiles({ id: libraryStub.externalLibraryWithImportPaths1.id });
|
await sut.handleQueueSyncFiles({ id: libraryStub.externalLibraryWithImportPaths1.id });
|
||||||
|
@ -341,7 +341,10 @@ export class LibraryService extends BaseService {
|
|||||||
|
|
||||||
this.logger.debug(`Will delete all assets in library ${libraryId}`);
|
this.logger.debug(`Will delete all assets in library ${libraryId}`);
|
||||||
for await (const assets of assetPagination) {
|
for await (const assets of assetPagination) {
|
||||||
|
if (assets.length > 0) {
|
||||||
assetsFound = true;
|
assetsFound = true;
|
||||||
|
}
|
||||||
|
|
||||||
this.logger.debug(`Queueing deletion of ${assets.length} asset(s) in library ${libraryId}`);
|
this.logger.debug(`Queueing deletion of ${assets.length} asset(s) in library ${libraryId}`);
|
||||||
await this.jobRepository.queueAll(
|
await this.jobRepository.queueAll(
|
||||||
assets.map((asset) => ({
|
assets.map((asset) => ({
|
||||||
@ -544,7 +547,10 @@ export class LibraryService extends BaseService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (validImportPaths) {
|
if (validImportPaths.length === 0) {
|
||||||
|
this.logger.warn(`No valid import paths found for library ${library.id}`);
|
||||||
|
}
|
||||||
|
|
||||||
const assetsOnDisk = this.storageRepository.walk({
|
const assetsOnDisk = this.storageRepository.walk({
|
||||||
pathsToCrawl: validImportPaths,
|
pathsToCrawl: validImportPaths,
|
||||||
includeHidden: false,
|
includeHidden: false,
|
||||||
@ -563,12 +569,9 @@ export class LibraryService extends BaseService {
|
|||||||
|
|
||||||
if (count > 0) {
|
if (count > 0) {
|
||||||
this.logger.debug(`Finished queueing scan of ${count} assets on disk for library ${library.id}`);
|
this.logger.debug(`Finished queueing scan of ${count} assets on disk for library ${library.id}`);
|
||||||
} else {
|
} else if (validImportPaths.length > 0) {
|
||||||
this.logger.debug(`No non-excluded assets found in any import path for library ${library.id}`);
|
this.logger.debug(`No non-excluded assets found in any import path for library ${library.id}`);
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
this.logger.warn(`No valid import paths found for library ${library.id}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.libraryRepository.update({ id: job.id, refreshedAt: new Date() });
|
await this.libraryRepository.update({ id: job.id, refreshedAt: new Date() });
|
||||||
|
|
||||||
|
@ -5,7 +5,6 @@ import { MemoryCreateDto, MemoryResponseDto, MemoryUpdateDto, mapMemory } from '
|
|||||||
import { AssetEntity } from 'src/entities/asset.entity';
|
import { AssetEntity } from 'src/entities/asset.entity';
|
||||||
import { Permission } from 'src/enum';
|
import { Permission } from 'src/enum';
|
||||||
import { BaseService } from 'src/services/base.service';
|
import { BaseService } from 'src/services/base.service';
|
||||||
import { checkAccess, requireAccess } from 'src/utils/access';
|
|
||||||
import { addAssets, removeAssets } from 'src/utils/asset.util';
|
import { addAssets, removeAssets } from 'src/utils/asset.util';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@ -16,7 +15,7 @@ export class MemoryService extends BaseService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async get(auth: AuthDto, id: string): Promise<MemoryResponseDto> {
|
async get(auth: AuthDto, id: string): Promise<MemoryResponseDto> {
|
||||||
await requireAccess(this.accessRepository, { auth, permission: Permission.MEMORY_READ, ids: [id] });
|
await this.requireAccess({ auth, permission: Permission.MEMORY_READ, ids: [id] });
|
||||||
const memory = await this.findOrFail(id);
|
const memory = await this.findOrFail(id);
|
||||||
return mapMemory(memory);
|
return mapMemory(memory);
|
||||||
}
|
}
|
||||||
@ -25,7 +24,7 @@ export class MemoryService extends BaseService {
|
|||||||
// TODO validate type/data combination
|
// TODO validate type/data combination
|
||||||
|
|
||||||
const assetIds = dto.assetIds || [];
|
const assetIds = dto.assetIds || [];
|
||||||
const allowedAssetIds = await checkAccess(this.accessRepository, {
|
const allowedAssetIds = await this.checkAccess({
|
||||||
auth,
|
auth,
|
||||||
permission: Permission.ASSET_SHARE,
|
permission: Permission.ASSET_SHARE,
|
||||||
ids: assetIds,
|
ids: assetIds,
|
||||||
@ -44,7 +43,7 @@ export class MemoryService extends BaseService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async update(auth: AuthDto, id: string, dto: MemoryUpdateDto): Promise<MemoryResponseDto> {
|
async update(auth: AuthDto, id: string, dto: MemoryUpdateDto): Promise<MemoryResponseDto> {
|
||||||
await requireAccess(this.accessRepository, { auth, permission: Permission.MEMORY_UPDATE, ids: [id] });
|
await this.requireAccess({ auth, permission: Permission.MEMORY_UPDATE, ids: [id] });
|
||||||
|
|
||||||
const memory = await this.memoryRepository.update({
|
const memory = await this.memoryRepository.update({
|
||||||
id,
|
id,
|
||||||
@ -57,12 +56,12 @@ export class MemoryService extends BaseService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async remove(auth: AuthDto, id: string): Promise<void> {
|
async remove(auth: AuthDto, id: string): Promise<void> {
|
||||||
await requireAccess(this.accessRepository, { auth, permission: Permission.MEMORY_DELETE, ids: [id] });
|
await this.requireAccess({ auth, permission: Permission.MEMORY_DELETE, ids: [id] });
|
||||||
await this.memoryRepository.delete(id);
|
await this.memoryRepository.delete(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
async addAssets(auth: AuthDto, id: string, dto: BulkIdsDto): Promise<BulkIdResponseDto[]> {
|
async addAssets(auth: AuthDto, id: string, dto: BulkIdsDto): Promise<BulkIdResponseDto[]> {
|
||||||
await requireAccess(this.accessRepository, { auth, permission: Permission.MEMORY_READ, ids: [id] });
|
await this.requireAccess({ auth, permission: Permission.MEMORY_READ, ids: [id] });
|
||||||
|
|
||||||
const repos = { access: this.accessRepository, bulk: this.memoryRepository };
|
const repos = { access: this.accessRepository, bulk: this.memoryRepository };
|
||||||
const results = await addAssets(auth, repos, { parentId: id, assetIds: dto.ids });
|
const results = await addAssets(auth, repos, { parentId: id, assetIds: dto.ids });
|
||||||
@ -76,7 +75,7 @@ export class MemoryService extends BaseService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async removeAssets(auth: AuthDto, id: string, dto: BulkIdsDto): Promise<BulkIdResponseDto[]> {
|
async removeAssets(auth: AuthDto, id: string, dto: BulkIdsDto): Promise<BulkIdResponseDto[]> {
|
||||||
await requireAccess(this.accessRepository, { auth, permission: Permission.MEMORY_UPDATE, ids: [id] });
|
await this.requireAccess({ auth, permission: Permission.MEMORY_UPDATE, ids: [id] });
|
||||||
|
|
||||||
const repos = { access: this.accessRepository, bulk: this.memoryRepository };
|
const repos = { access: this.accessRepository, bulk: this.memoryRepository };
|
||||||
const results = await removeAssets(auth, repos, {
|
const results = await removeAssets(auth, repos, {
|
||||||
|
@ -6,7 +6,6 @@ import { PartnerEntity } from 'src/entities/partner.entity';
|
|||||||
import { Permission } from 'src/enum';
|
import { Permission } from 'src/enum';
|
||||||
import { PartnerDirection, PartnerIds } from 'src/interfaces/partner.interface';
|
import { PartnerDirection, PartnerIds } from 'src/interfaces/partner.interface';
|
||||||
import { BaseService } from 'src/services/base.service';
|
import { BaseService } from 'src/services/base.service';
|
||||||
import { requireAccess } from 'src/utils/access';
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class PartnerService extends BaseService {
|
export class PartnerService extends BaseService {
|
||||||
@ -41,7 +40,7 @@ export class PartnerService extends BaseService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async update(auth: AuthDto, sharedById: string, dto: UpdatePartnerDto): Promise<PartnerResponseDto> {
|
async update(auth: AuthDto, sharedById: string, dto: UpdatePartnerDto): Promise<PartnerResponseDto> {
|
||||||
await requireAccess(this.accessRepository, { auth, permission: Permission.PARTNER_UPDATE, ids: [sharedById] });
|
await this.requireAccess({ auth, permission: Permission.PARTNER_UPDATE, ids: [sharedById] });
|
||||||
const partnerId: PartnerIds = { sharedById, sharedWithId: auth.user.id };
|
const partnerId: PartnerIds = { sharedById, sharedWithId: auth.user.id };
|
||||||
|
|
||||||
const entity = await this.partnerRepository.update({ ...partnerId, inTimeline: dto.inTimeline });
|
const entity = await this.partnerRepository.update({ ...partnerId, inTimeline: dto.inTimeline });
|
||||||
|
@ -721,7 +721,6 @@ describe(PersonService.name, () => {
|
|||||||
'/uploads/user-id/thumbs/path.jpg',
|
'/uploads/user-id/thumbs/path.jpg',
|
||||||
expect.objectContaining({ minScore: 0.7, modelName: 'buffalo_l' }),
|
expect.objectContaining({ minScore: 0.7, modelName: 'buffalo_l' }),
|
||||||
);
|
);
|
||||||
expect(personMock.createFaces).not.toHaveBeenCalled();
|
|
||||||
expect(jobMock.queue).not.toHaveBeenCalled();
|
expect(jobMock.queue).not.toHaveBeenCalled();
|
||||||
expect(jobMock.queueAll).not.toHaveBeenCalled();
|
expect(jobMock.queueAll).not.toHaveBeenCalled();
|
||||||
|
|
||||||
@ -733,7 +732,6 @@ describe(PersonService.name, () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should create a face with no person and queue recognition job', async () => {
|
it('should create a face with no person and queue recognition job', async () => {
|
||||||
personMock.createFaces.mockResolvedValue([faceStub.face1.id]);
|
|
||||||
machineLearningMock.detectFaces.mockResolvedValue(detectFaceMock);
|
machineLearningMock.detectFaces.mockResolvedValue(detectFaceMock);
|
||||||
assetMock.getByIds.mockResolvedValue([assetStub.image]);
|
assetMock.getByIds.mockResolvedValue([assetStub.image]);
|
||||||
|
|
||||||
@ -761,7 +759,6 @@ describe(PersonService.name, () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should add new face and delete an existing face not among the new detected faces', async () => {
|
it('should add new face and delete an existing face not among the new detected faces', async () => {
|
||||||
personMock.createFaces.mockResolvedValue([faceStub.face1.id]);
|
|
||||||
machineLearningMock.detectFaces.mockResolvedValue(detectFaceMock);
|
machineLearningMock.detectFaces.mockResolvedValue(detectFaceMock);
|
||||||
assetMock.getByIds.mockResolvedValue([{ ...assetStub.image, faces: [faceStub.primaryFace1] }]);
|
assetMock.getByIds.mockResolvedValue([{ ...assetStub.image, faces: [faceStub.primaryFace1] }]);
|
||||||
|
|
||||||
@ -816,7 +813,6 @@ describe(PersonService.name, () => {
|
|||||||
|
|
||||||
expect(personMock.reassignFaces).not.toHaveBeenCalled();
|
expect(personMock.reassignFaces).not.toHaveBeenCalled();
|
||||||
expect(personMock.create).not.toHaveBeenCalled();
|
expect(personMock.create).not.toHaveBeenCalled();
|
||||||
expect(personMock.createFaces).not.toHaveBeenCalled();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should fail if face does not have asset', async () => {
|
it('should fail if face does not have asset', async () => {
|
||||||
@ -827,7 +823,6 @@ describe(PersonService.name, () => {
|
|||||||
|
|
||||||
expect(personMock.reassignFaces).not.toHaveBeenCalled();
|
expect(personMock.reassignFaces).not.toHaveBeenCalled();
|
||||||
expect(personMock.create).not.toHaveBeenCalled();
|
expect(personMock.create).not.toHaveBeenCalled();
|
||||||
expect(personMock.createFaces).not.toHaveBeenCalled();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should skip if face already has an assigned person', async () => {
|
it('should skip if face already has an assigned person', async () => {
|
||||||
@ -837,7 +832,6 @@ describe(PersonService.name, () => {
|
|||||||
|
|
||||||
expect(personMock.reassignFaces).not.toHaveBeenCalled();
|
expect(personMock.reassignFaces).not.toHaveBeenCalled();
|
||||||
expect(personMock.create).not.toHaveBeenCalled();
|
expect(personMock.create).not.toHaveBeenCalled();
|
||||||
expect(personMock.createFaces).not.toHaveBeenCalled();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should match existing person', async () => {
|
it('should match existing person', async () => {
|
||||||
|
@ -47,7 +47,6 @@ import { BoundingBox } from 'src/interfaces/machine-learning.interface';
|
|||||||
import { CropOptions, ImageDimensions, InputDimensions } from 'src/interfaces/media.interface';
|
import { CropOptions, ImageDimensions, InputDimensions } from 'src/interfaces/media.interface';
|
||||||
import { UpdateFacesData } from 'src/interfaces/person.interface';
|
import { UpdateFacesData } from 'src/interfaces/person.interface';
|
||||||
import { BaseService } from 'src/services/base.service';
|
import { BaseService } from 'src/services/base.service';
|
||||||
import { checkAccess, requireAccess } from 'src/utils/access';
|
|
||||||
import { getAssetFiles } from 'src/utils/asset.util';
|
import { getAssetFiles } from 'src/utils/asset.util';
|
||||||
import { ImmichFileResponse } from 'src/utils/file';
|
import { ImmichFileResponse } from 'src/utils/file';
|
||||||
import { mimeTypes } from 'src/utils/mime-types';
|
import { mimeTypes } from 'src/utils/mime-types';
|
||||||
@ -80,7 +79,7 @@ export class PersonService extends BaseService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async reassignFaces(auth: AuthDto, personId: string, dto: AssetFaceUpdateDto): Promise<PersonResponseDto[]> {
|
async reassignFaces(auth: AuthDto, personId: string, dto: AssetFaceUpdateDto): Promise<PersonResponseDto[]> {
|
||||||
await requireAccess(this.accessRepository, { auth, permission: Permission.PERSON_UPDATE, ids: [personId] });
|
await this.requireAccess({ auth, permission: Permission.PERSON_UPDATE, ids: [personId] });
|
||||||
const person = await this.findOrFail(personId);
|
const person = await this.findOrFail(personId);
|
||||||
const result: PersonResponseDto[] = [];
|
const result: PersonResponseDto[] = [];
|
||||||
const changeFeaturePhoto: string[] = [];
|
const changeFeaturePhoto: string[] = [];
|
||||||
@ -88,7 +87,7 @@ export class PersonService extends BaseService {
|
|||||||
const faces = await this.personRepository.getFacesByIds([{ personId: data.personId, assetId: data.assetId }]);
|
const faces = await this.personRepository.getFacesByIds([{ personId: data.personId, assetId: data.assetId }]);
|
||||||
|
|
||||||
for (const face of faces) {
|
for (const face of faces) {
|
||||||
await requireAccess(this.accessRepository, { auth, permission: Permission.PERSON_CREATE, ids: [face.id] });
|
await this.requireAccess({ auth, permission: Permission.PERSON_CREATE, ids: [face.id] });
|
||||||
if (person.faceAssetId === null) {
|
if (person.faceAssetId === null) {
|
||||||
changeFeaturePhoto.push(person.id);
|
changeFeaturePhoto.push(person.id);
|
||||||
}
|
}
|
||||||
@ -109,8 +108,8 @@ export class PersonService extends BaseService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async reassignFacesById(auth: AuthDto, personId: string, dto: FaceDto): Promise<PersonResponseDto> {
|
async reassignFacesById(auth: AuthDto, personId: string, dto: FaceDto): Promise<PersonResponseDto> {
|
||||||
await requireAccess(this.accessRepository, { auth, permission: Permission.PERSON_UPDATE, ids: [personId] });
|
await this.requireAccess({ auth, permission: Permission.PERSON_UPDATE, ids: [personId] });
|
||||||
await requireAccess(this.accessRepository, { auth, permission: Permission.PERSON_CREATE, ids: [dto.id] });
|
await this.requireAccess({ auth, permission: Permission.PERSON_CREATE, ids: [dto.id] });
|
||||||
const face = await this.personRepository.getFaceById(dto.id);
|
const face = await this.personRepository.getFaceById(dto.id);
|
||||||
const person = await this.findOrFail(personId);
|
const person = await this.findOrFail(personId);
|
||||||
|
|
||||||
@ -126,7 +125,7 @@ export class PersonService extends BaseService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getFacesById(auth: AuthDto, dto: FaceDto): Promise<AssetFaceResponseDto[]> {
|
async getFacesById(auth: AuthDto, dto: FaceDto): Promise<AssetFaceResponseDto[]> {
|
||||||
await requireAccess(this.accessRepository, { auth, permission: Permission.ASSET_READ, ids: [dto.id] });
|
await this.requireAccess({ auth, permission: Permission.ASSET_READ, ids: [dto.id] });
|
||||||
const faces = await this.personRepository.getFaces(dto.id);
|
const faces = await this.personRepository.getFaces(dto.id);
|
||||||
return faces.map((asset) => mapFaces(asset, auth));
|
return faces.map((asset) => mapFaces(asset, auth));
|
||||||
}
|
}
|
||||||
@ -150,17 +149,17 @@ export class PersonService extends BaseService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getById(auth: AuthDto, id: string): Promise<PersonResponseDto> {
|
async getById(auth: AuthDto, id: string): Promise<PersonResponseDto> {
|
||||||
await requireAccess(this.accessRepository, { auth, permission: Permission.PERSON_READ, ids: [id] });
|
await this.requireAccess({ auth, permission: Permission.PERSON_READ, ids: [id] });
|
||||||
return this.findOrFail(id).then(mapPerson);
|
return this.findOrFail(id).then(mapPerson);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getStatistics(auth: AuthDto, id: string): Promise<PersonStatisticsResponseDto> {
|
async getStatistics(auth: AuthDto, id: string): Promise<PersonStatisticsResponseDto> {
|
||||||
await requireAccess(this.accessRepository, { auth, permission: Permission.PERSON_READ, ids: [id] });
|
await this.requireAccess({ auth, permission: Permission.PERSON_READ, ids: [id] });
|
||||||
return this.personRepository.getStatistics(id);
|
return this.personRepository.getStatistics(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getThumbnail(auth: AuthDto, id: string): Promise<ImmichFileResponse> {
|
async getThumbnail(auth: AuthDto, id: string): Promise<ImmichFileResponse> {
|
||||||
await requireAccess(this.accessRepository, { auth, permission: Permission.PERSON_READ, ids: [id] });
|
await this.requireAccess({ auth, permission: Permission.PERSON_READ, ids: [id] });
|
||||||
const person = await this.personRepository.getById(id);
|
const person = await this.personRepository.getById(id);
|
||||||
if (!person || !person.thumbnailPath) {
|
if (!person || !person.thumbnailPath) {
|
||||||
throw new NotFoundException();
|
throw new NotFoundException();
|
||||||
@ -183,13 +182,13 @@ export class PersonService extends BaseService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async update(auth: AuthDto, id: string, dto: PersonUpdateDto): Promise<PersonResponseDto> {
|
async update(auth: AuthDto, id: string, dto: PersonUpdateDto): Promise<PersonResponseDto> {
|
||||||
await requireAccess(this.accessRepository, { auth, permission: Permission.PERSON_UPDATE, ids: [id] });
|
await this.requireAccess({ auth, permission: Permission.PERSON_UPDATE, ids: [id] });
|
||||||
|
|
||||||
const { name, birthDate, isHidden, featureFaceAssetId: assetId } = dto;
|
const { name, birthDate, isHidden, featureFaceAssetId: assetId } = dto;
|
||||||
// TODO: set by faceId directly
|
// TODO: set by faceId directly
|
||||||
let faceId: string | undefined = undefined;
|
let faceId: string | undefined = undefined;
|
||||||
if (assetId) {
|
if (assetId) {
|
||||||
await requireAccess(this.accessRepository, { auth, permission: Permission.ASSET_READ, ids: [assetId] });
|
await this.requireAccess({ auth, permission: Permission.ASSET_READ, ids: [assetId] });
|
||||||
const [face] = await this.personRepository.getFacesByIds([{ personId: id, assetId }]);
|
const [face] = await this.personRepository.getFacesByIds([{ personId: id, assetId }]);
|
||||||
if (!face) {
|
if (!face) {
|
||||||
throw new BadRequestException('Invalid assetId for feature face');
|
throw new BadRequestException('Invalid assetId for feature face');
|
||||||
@ -584,13 +583,13 @@ export class PersonService extends BaseService {
|
|||||||
throw new BadRequestException('Cannot merge a person into themselves');
|
throw new BadRequestException('Cannot merge a person into themselves');
|
||||||
}
|
}
|
||||||
|
|
||||||
await requireAccess(this.accessRepository, { auth, permission: Permission.PERSON_UPDATE, ids: [id] });
|
await this.requireAccess({ auth, permission: Permission.PERSON_UPDATE, ids: [id] });
|
||||||
let primaryPerson = await this.findOrFail(id);
|
let primaryPerson = await this.findOrFail(id);
|
||||||
const primaryName = primaryPerson.name || primaryPerson.id;
|
const primaryName = primaryPerson.name || primaryPerson.id;
|
||||||
|
|
||||||
const results: BulkIdResponseDto[] = [];
|
const results: BulkIdResponseDto[] = [];
|
||||||
|
|
||||||
const allowedIds = await checkAccess(this.accessRepository, {
|
const allowedIds = await this.checkAccess({
|
||||||
auth,
|
auth,
|
||||||
permission: Permission.PERSON_MERGE,
|
permission: Permission.PERSON_MERGE,
|
||||||
ids: mergeIds,
|
ids: mergeIds,
|
||||||
|
@ -5,7 +5,6 @@ import { SessionResponseDto, mapSession } from 'src/dtos/session.dto';
|
|||||||
import { Permission } from 'src/enum';
|
import { Permission } from 'src/enum';
|
||||||
import { JobStatus } from 'src/interfaces/job.interface';
|
import { JobStatus } from 'src/interfaces/job.interface';
|
||||||
import { BaseService } from 'src/services/base.service';
|
import { BaseService } from 'src/services/base.service';
|
||||||
import { requireAccess } from 'src/utils/access';
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class SessionService extends BaseService {
|
export class SessionService extends BaseService {
|
||||||
@ -34,7 +33,7 @@ export class SessionService extends BaseService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async delete(auth: AuthDto, id: string): Promise<void> {
|
async delete(auth: AuthDto, id: string): Promise<void> {
|
||||||
await requireAccess(this.accessRepository, { auth, permission: Permission.AUTH_DEVICE_DELETE, ids: [id] });
|
await this.requireAccess({ auth, permission: Permission.AUTH_DEVICE_DELETE, ids: [id] });
|
||||||
await this.sessionRepository.delete(id);
|
await this.sessionRepository.delete(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -15,7 +15,6 @@ import { AssetEntity } from 'src/entities/asset.entity';
|
|||||||
import { SharedLinkEntity } from 'src/entities/shared-link.entity';
|
import { SharedLinkEntity } from 'src/entities/shared-link.entity';
|
||||||
import { Permission, SharedLinkType } from 'src/enum';
|
import { Permission, SharedLinkType } from 'src/enum';
|
||||||
import { BaseService } from 'src/services/base.service';
|
import { BaseService } from 'src/services/base.service';
|
||||||
import { checkAccess, requireAccess } from 'src/utils/access';
|
|
||||||
import { OpenGraphTags } from 'src/utils/misc';
|
import { OpenGraphTags } from 'src/utils/misc';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@ -49,7 +48,7 @@ export class SharedLinkService extends BaseService {
|
|||||||
if (!dto.albumId) {
|
if (!dto.albumId) {
|
||||||
throw new BadRequestException('Invalid albumId');
|
throw new BadRequestException('Invalid albumId');
|
||||||
}
|
}
|
||||||
await requireAccess(this.accessRepository, { auth, permission: Permission.ALBUM_SHARE, ids: [dto.albumId] });
|
await this.requireAccess({ auth, permission: Permission.ALBUM_SHARE, ids: [dto.albumId] });
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -58,7 +57,7 @@ export class SharedLinkService extends BaseService {
|
|||||||
throw new BadRequestException('Invalid assetIds');
|
throw new BadRequestException('Invalid assetIds');
|
||||||
}
|
}
|
||||||
|
|
||||||
await requireAccess(this.accessRepository, { auth, permission: Permission.ASSET_SHARE, ids: dto.assetIds });
|
await this.requireAccess({ auth, permission: Permission.ASSET_SHARE, ids: dto.assetIds });
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -119,7 +118,7 @@ export class SharedLinkService extends BaseService {
|
|||||||
|
|
||||||
const existingAssetIds = new Set(sharedLink.assets.map((asset) => asset.id));
|
const existingAssetIds = new Set(sharedLink.assets.map((asset) => asset.id));
|
||||||
const notPresentAssetIds = dto.assetIds.filter((assetId) => !existingAssetIds.has(assetId));
|
const notPresentAssetIds = dto.assetIds.filter((assetId) => !existingAssetIds.has(assetId));
|
||||||
const allowedAssetIds = await checkAccess(this.accessRepository, {
|
const allowedAssetIds = await this.checkAccess({
|
||||||
auth,
|
auth,
|
||||||
permission: Permission.ASSET_SHARE,
|
permission: Permission.ASSET_SHARE,
|
||||||
ids: notPresentAssetIds,
|
ids: notPresentAssetIds,
|
||||||
|
@ -4,7 +4,6 @@ import { AuthDto } from 'src/dtos/auth.dto';
|
|||||||
import { StackCreateDto, StackResponseDto, StackSearchDto, StackUpdateDto, mapStack } from 'src/dtos/stack.dto';
|
import { StackCreateDto, StackResponseDto, StackSearchDto, StackUpdateDto, mapStack } from 'src/dtos/stack.dto';
|
||||||
import { Permission } from 'src/enum';
|
import { Permission } from 'src/enum';
|
||||||
import { BaseService } from 'src/services/base.service';
|
import { BaseService } from 'src/services/base.service';
|
||||||
import { requireAccess } from 'src/utils/access';
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class StackService extends BaseService {
|
export class StackService extends BaseService {
|
||||||
@ -18,7 +17,7 @@ export class StackService extends BaseService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async create(auth: AuthDto, dto: StackCreateDto): Promise<StackResponseDto> {
|
async create(auth: AuthDto, dto: StackCreateDto): Promise<StackResponseDto> {
|
||||||
await requireAccess(this.accessRepository, { auth, permission: Permission.ASSET_UPDATE, ids: dto.assetIds });
|
await this.requireAccess({ auth, permission: Permission.ASSET_UPDATE, ids: dto.assetIds });
|
||||||
|
|
||||||
const stack = await this.stackRepository.create({ ownerId: auth.user.id, assetIds: dto.assetIds });
|
const stack = await this.stackRepository.create({ ownerId: auth.user.id, assetIds: dto.assetIds });
|
||||||
|
|
||||||
@ -28,13 +27,13 @@ export class StackService extends BaseService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async get(auth: AuthDto, id: string): Promise<StackResponseDto> {
|
async get(auth: AuthDto, id: string): Promise<StackResponseDto> {
|
||||||
await requireAccess(this.accessRepository, { auth, permission: Permission.STACK_READ, ids: [id] });
|
await this.requireAccess({ auth, permission: Permission.STACK_READ, ids: [id] });
|
||||||
const stack = await this.findOrFail(id);
|
const stack = await this.findOrFail(id);
|
||||||
return mapStack(stack, { auth });
|
return mapStack(stack, { auth });
|
||||||
}
|
}
|
||||||
|
|
||||||
async update(auth: AuthDto, id: string, dto: StackUpdateDto): Promise<StackResponseDto> {
|
async update(auth: AuthDto, id: string, dto: StackUpdateDto): Promise<StackResponseDto> {
|
||||||
await requireAccess(this.accessRepository, { auth, permission: Permission.STACK_UPDATE, ids: [id] });
|
await this.requireAccess({ auth, permission: Permission.STACK_UPDATE, ids: [id] });
|
||||||
const stack = await this.findOrFail(id);
|
const stack = await this.findOrFail(id);
|
||||||
if (dto.primaryAssetId && !stack.assets.some(({ id }) => id === dto.primaryAssetId)) {
|
if (dto.primaryAssetId && !stack.assets.some(({ id }) => id === dto.primaryAssetId)) {
|
||||||
throw new BadRequestException('Primary asset must be in the stack');
|
throw new BadRequestException('Primary asset must be in the stack');
|
||||||
@ -48,13 +47,13 @@ export class StackService extends BaseService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async delete(auth: AuthDto, id: string): Promise<void> {
|
async delete(auth: AuthDto, id: string): Promise<void> {
|
||||||
await requireAccess(this.accessRepository, { auth, permission: Permission.STACK_DELETE, ids: [id] });
|
await this.requireAccess({ auth, permission: Permission.STACK_DELETE, ids: [id] });
|
||||||
await this.stackRepository.delete(id);
|
await this.stackRepository.delete(id);
|
||||||
await this.eventRepository.emit('stack.delete', { stackId: id, userId: auth.user.id });
|
await this.eventRepository.emit('stack.delete', { stackId: id, userId: auth.user.id });
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteAll(auth: AuthDto, dto: BulkIdsDto): Promise<void> {
|
async deleteAll(auth: AuthDto, dto: BulkIdsDto): Promise<void> {
|
||||||
await requireAccess(this.accessRepository, { auth, permission: Permission.STACK_DELETE, ids: dto.ids });
|
await this.requireAccess({ auth, permission: Permission.STACK_DELETE, ids: dto.ids });
|
||||||
await this.stackRepository.deleteAll(dto.ids);
|
await this.stackRepository.deleteAll(dto.ids);
|
||||||
await this.eventRepository.emit('stacks.delete', { stackIds: dto.ids, userId: auth.user.id });
|
await this.eventRepository.emit('stacks.delete', { stackIds: dto.ids, userId: auth.user.id });
|
||||||
}
|
}
|
||||||
|
@ -5,7 +5,6 @@ import { AuthDto } from 'src/dtos/auth.dto';
|
|||||||
import { AssetDeltaSyncDto, AssetDeltaSyncResponseDto, AssetFullSyncDto } from 'src/dtos/sync.dto';
|
import { AssetDeltaSyncDto, AssetDeltaSyncResponseDto, AssetFullSyncDto } from 'src/dtos/sync.dto';
|
||||||
import { DatabaseAction, EntityType, Permission } from 'src/enum';
|
import { DatabaseAction, EntityType, Permission } from 'src/enum';
|
||||||
import { BaseService } from 'src/services/base.service';
|
import { BaseService } from 'src/services/base.service';
|
||||||
import { requireAccess } from 'src/utils/access';
|
|
||||||
import { getMyPartnerIds } from 'src/utils/asset.util';
|
import { getMyPartnerIds } from 'src/utils/asset.util';
|
||||||
import { setIsEqual } from 'src/utils/set';
|
import { setIsEqual } from 'src/utils/set';
|
||||||
|
|
||||||
@ -15,7 +14,7 @@ export class SyncService extends BaseService {
|
|||||||
async getFullSync(auth: AuthDto, dto: AssetFullSyncDto): Promise<AssetResponseDto[]> {
|
async getFullSync(auth: AuthDto, dto: AssetFullSyncDto): Promise<AssetResponseDto[]> {
|
||||||
// mobile implementation is faster if this is a single id
|
// mobile implementation is faster if this is a single id
|
||||||
const userId = dto.userId || auth.user.id;
|
const userId = dto.userId || auth.user.id;
|
||||||
await requireAccess(this.accessRepository, { auth, permission: Permission.TIMELINE_READ, ids: [userId] });
|
await this.requireAccess({ auth, permission: Permission.TIMELINE_READ, ids: [userId] });
|
||||||
const assets = await this.assetRepository.getAllForUserFullSync({
|
const assets = await this.assetRepository.getAllForUserFullSync({
|
||||||
ownerId: userId,
|
ownerId: userId,
|
||||||
updatedUntil: dto.updatedUntil,
|
updatedUntil: dto.updatedUntil,
|
||||||
@ -39,7 +38,7 @@ export class SyncService extends BaseService {
|
|||||||
return FULL_SYNC;
|
return FULL_SYNC;
|
||||||
}
|
}
|
||||||
|
|
||||||
await requireAccess(this.accessRepository, { auth, permission: Permission.TIMELINE_READ, ids: dto.userIds });
|
await this.requireAccess({ auth, permission: Permission.TIMELINE_READ, ids: dto.userIds });
|
||||||
|
|
||||||
const limit = 10_000;
|
const limit = 10_000;
|
||||||
const upserted = await this.assetRepository.getChangedDeltaSync({ limit, updatedAfter: dto.updatedAfter, userIds });
|
const upserted = await this.assetRepository.getChangedDeltaSync({ limit, updatedAfter: dto.updatedAfter, userIds });
|
||||||
|
@ -15,7 +15,6 @@ import { Permission } from 'src/enum';
|
|||||||
import { JobStatus } from 'src/interfaces/job.interface';
|
import { JobStatus } from 'src/interfaces/job.interface';
|
||||||
import { AssetTagItem } from 'src/interfaces/tag.interface';
|
import { AssetTagItem } from 'src/interfaces/tag.interface';
|
||||||
import { BaseService } from 'src/services/base.service';
|
import { BaseService } from 'src/services/base.service';
|
||||||
import { checkAccess, requireAccess } from 'src/utils/access';
|
|
||||||
import { addAssets, removeAssets } from 'src/utils/asset.util';
|
import { addAssets, removeAssets } from 'src/utils/asset.util';
|
||||||
import { upsertTags } from 'src/utils/tag';
|
import { upsertTags } from 'src/utils/tag';
|
||||||
|
|
||||||
@ -27,7 +26,7 @@ export class TagService extends BaseService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async get(auth: AuthDto, id: string): Promise<TagResponseDto> {
|
async get(auth: AuthDto, id: string): Promise<TagResponseDto> {
|
||||||
await requireAccess(this.accessRepository, { auth, permission: Permission.TAG_READ, ids: [id] });
|
await this.requireAccess({ auth, permission: Permission.TAG_READ, ids: [id] });
|
||||||
const tag = await this.findOrFail(id);
|
const tag = await this.findOrFail(id);
|
||||||
return mapTag(tag);
|
return mapTag(tag);
|
||||||
}
|
}
|
||||||
@ -35,7 +34,7 @@ export class TagService extends BaseService {
|
|||||||
async create(auth: AuthDto, dto: TagCreateDto) {
|
async create(auth: AuthDto, dto: TagCreateDto) {
|
||||||
let parent: TagEntity | undefined;
|
let parent: TagEntity | undefined;
|
||||||
if (dto.parentId) {
|
if (dto.parentId) {
|
||||||
await requireAccess(this.accessRepository, { auth, permission: Permission.TAG_READ, ids: [dto.parentId] });
|
await this.requireAccess({ auth, permission: Permission.TAG_READ, ids: [dto.parentId] });
|
||||||
parent = (await this.tagRepository.get(dto.parentId)) || undefined;
|
parent = (await this.tagRepository.get(dto.parentId)) || undefined;
|
||||||
if (!parent) {
|
if (!parent) {
|
||||||
throw new BadRequestException('Tag not found');
|
throw new BadRequestException('Tag not found');
|
||||||
@ -55,7 +54,7 @@ export class TagService extends BaseService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async update(auth: AuthDto, id: string, dto: TagUpdateDto): Promise<TagResponseDto> {
|
async update(auth: AuthDto, id: string, dto: TagUpdateDto): Promise<TagResponseDto> {
|
||||||
await requireAccess(this.accessRepository, { auth, permission: Permission.TAG_UPDATE, ids: [id] });
|
await this.requireAccess({ auth, permission: Permission.TAG_UPDATE, ids: [id] });
|
||||||
|
|
||||||
const { color } = dto;
|
const { color } = dto;
|
||||||
const tag = await this.tagRepository.update({ id, color });
|
const tag = await this.tagRepository.update({ id, color });
|
||||||
@ -68,7 +67,7 @@ export class TagService extends BaseService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async remove(auth: AuthDto, id: string): Promise<void> {
|
async remove(auth: AuthDto, id: string): Promise<void> {
|
||||||
await requireAccess(this.accessRepository, { auth, permission: Permission.TAG_DELETE, ids: [id] });
|
await this.requireAccess({ auth, permission: Permission.TAG_DELETE, ids: [id] });
|
||||||
|
|
||||||
// TODO sync tag changes for affected assets
|
// TODO sync tag changes for affected assets
|
||||||
|
|
||||||
@ -77,8 +76,8 @@ export class TagService extends BaseService {
|
|||||||
|
|
||||||
async bulkTagAssets(auth: AuthDto, dto: TagBulkAssetsDto): Promise<TagBulkAssetsResponseDto> {
|
async bulkTagAssets(auth: AuthDto, dto: TagBulkAssetsDto): Promise<TagBulkAssetsResponseDto> {
|
||||||
const [tagIds, assetIds] = await Promise.all([
|
const [tagIds, assetIds] = await Promise.all([
|
||||||
checkAccess(this.accessRepository, { auth, permission: Permission.TAG_ASSET, ids: dto.tagIds }),
|
this.checkAccess({ auth, permission: Permission.TAG_ASSET, ids: dto.tagIds }),
|
||||||
checkAccess(this.accessRepository, { auth, permission: Permission.ASSET_UPDATE, ids: dto.assetIds }),
|
this.checkAccess({ auth, permission: Permission.ASSET_UPDATE, ids: dto.assetIds }),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const items: AssetTagItem[] = [];
|
const items: AssetTagItem[] = [];
|
||||||
@ -97,7 +96,7 @@ export class TagService extends BaseService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async addAssets(auth: AuthDto, id: string, dto: BulkIdsDto): Promise<BulkIdResponseDto[]> {
|
async addAssets(auth: AuthDto, id: string, dto: BulkIdsDto): Promise<BulkIdResponseDto[]> {
|
||||||
await requireAccess(this.accessRepository, { auth, permission: Permission.TAG_ASSET, ids: [id] });
|
await this.requireAccess({ auth, permission: Permission.TAG_ASSET, ids: [id] });
|
||||||
|
|
||||||
const results = await addAssets(
|
const results = await addAssets(
|
||||||
auth,
|
auth,
|
||||||
@ -115,7 +114,7 @@ export class TagService extends BaseService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async removeAssets(auth: AuthDto, id: string, dto: BulkIdsDto): Promise<BulkIdResponseDto[]> {
|
async removeAssets(auth: AuthDto, id: string, dto: BulkIdsDto): Promise<BulkIdResponseDto[]> {
|
||||||
await requireAccess(this.accessRepository, { auth, permission: Permission.TAG_ASSET, ids: [id] });
|
await this.requireAccess({ auth, permission: Permission.TAG_ASSET, ids: [id] });
|
||||||
|
|
||||||
const results = await removeAssets(
|
const results = await removeAssets(
|
||||||
auth,
|
auth,
|
||||||
|
@ -5,7 +5,6 @@ import { TimeBucketAssetDto, TimeBucketDto, TimeBucketResponseDto } from 'src/dt
|
|||||||
import { Permission } from 'src/enum';
|
import { Permission } from 'src/enum';
|
||||||
import { TimeBucketOptions } from 'src/interfaces/asset.interface';
|
import { TimeBucketOptions } from 'src/interfaces/asset.interface';
|
||||||
import { BaseService } from 'src/services/base.service';
|
import { BaseService } from 'src/services/base.service';
|
||||||
import { requireAccess } from 'src/utils/access';
|
|
||||||
import { getMyPartnerIds } from 'src/utils/asset.util';
|
import { getMyPartnerIds } from 'src/utils/asset.util';
|
||||||
|
|
||||||
export class TimelineService extends BaseService {
|
export class TimelineService extends BaseService {
|
||||||
@ -48,20 +47,20 @@ export class TimelineService extends BaseService {
|
|||||||
|
|
||||||
private async timeBucketChecks(auth: AuthDto, dto: TimeBucketDto) {
|
private async timeBucketChecks(auth: AuthDto, dto: TimeBucketDto) {
|
||||||
if (dto.albumId) {
|
if (dto.albumId) {
|
||||||
await requireAccess(this.accessRepository, { auth, permission: Permission.ALBUM_READ, ids: [dto.albumId] });
|
await this.requireAccess({ auth, permission: Permission.ALBUM_READ, ids: [dto.albumId] });
|
||||||
} else {
|
} else {
|
||||||
dto.userId = dto.userId || auth.user.id;
|
dto.userId = dto.userId || auth.user.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (dto.userId) {
|
if (dto.userId) {
|
||||||
await requireAccess(this.accessRepository, { auth, permission: Permission.TIMELINE_READ, ids: [dto.userId] });
|
await this.requireAccess({ auth, permission: Permission.TIMELINE_READ, ids: [dto.userId] });
|
||||||
if (dto.isArchived !== false) {
|
if (dto.isArchived !== false) {
|
||||||
await requireAccess(this.accessRepository, { auth, permission: Permission.ARCHIVE_READ, ids: [dto.userId] });
|
await this.requireAccess({ auth, permission: Permission.ARCHIVE_READ, ids: [dto.userId] });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (dto.tagId) {
|
if (dto.tagId) {
|
||||||
await requireAccess(this.accessRepository, { auth, permission: Permission.TAG_READ, ids: [dto.tagId] });
|
await this.requireAccess({ auth, permission: Permission.TAG_READ, ids: [dto.tagId] });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (dto.withPartners) {
|
if (dto.withPartners) {
|
||||||
|
@ -5,7 +5,6 @@ import { TrashResponseDto } from 'src/dtos/trash.dto';
|
|||||||
import { Permission } from 'src/enum';
|
import { Permission } from 'src/enum';
|
||||||
import { JOBS_ASSET_PAGINATION_SIZE, JobName, JobStatus } from 'src/interfaces/job.interface';
|
import { JOBS_ASSET_PAGINATION_SIZE, JobName, JobStatus } from 'src/interfaces/job.interface';
|
||||||
import { BaseService } from 'src/services/base.service';
|
import { BaseService } from 'src/services/base.service';
|
||||||
import { requireAccess } from 'src/utils/access';
|
|
||||||
import { usePagination } from 'src/utils/pagination';
|
import { usePagination } from 'src/utils/pagination';
|
||||||
|
|
||||||
export class TrashService extends BaseService {
|
export class TrashService extends BaseService {
|
||||||
@ -15,7 +14,7 @@ export class TrashService extends BaseService {
|
|||||||
return { count: 0 };
|
return { count: 0 };
|
||||||
}
|
}
|
||||||
|
|
||||||
await requireAccess(this.accessRepository, { auth, permission: Permission.ASSET_DELETE, ids });
|
await this.requireAccess({ auth, permission: Permission.ASSET_DELETE, ids });
|
||||||
await this.trashRepository.restoreAll(ids);
|
await this.trashRepository.restoreAll(ids);
|
||||||
await this.eventRepository.emit('assets.restore', { assetIds: ids, userId: auth.user.id });
|
await this.eventRepository.emit('assets.restore', { assetIds: ids, userId: auth.user.id });
|
||||||
|
|
||||||
|
51
server/test/fixtures/album.stub.ts
vendored
51
server/test/fixtures/album.stub.ts
vendored
@ -155,55 +155,4 @@ export const albumStub = {
|
|||||||
isActivityEnabled: true,
|
isActivityEnabled: true,
|
||||||
order: AssetOrder.DESC,
|
order: AssetOrder.DESC,
|
||||||
}),
|
}),
|
||||||
emptyWithInvalidThumbnail: Object.freeze<AlbumEntity>({
|
|
||||||
id: 'album-5',
|
|
||||||
albumName: 'Empty album with invalid thumbnail',
|
|
||||||
description: '',
|
|
||||||
ownerId: authStub.admin.user.id,
|
|
||||||
owner: userStub.admin,
|
|
||||||
assets: [],
|
|
||||||
albumThumbnailAsset: null,
|
|
||||||
albumThumbnailAssetId: null,
|
|
||||||
createdAt: new Date(),
|
|
||||||
updatedAt: new Date(),
|
|
||||||
deletedAt: null,
|
|
||||||
sharedLinks: [],
|
|
||||||
albumUsers: [],
|
|
||||||
isActivityEnabled: true,
|
|
||||||
order: AssetOrder.DESC,
|
|
||||||
}),
|
|
||||||
oneAssetInvalidThumbnail: Object.freeze<AlbumEntity>({
|
|
||||||
id: 'album-6',
|
|
||||||
albumName: 'Album with one asset and invalid thumbnail',
|
|
||||||
description: '',
|
|
||||||
ownerId: authStub.admin.user.id,
|
|
||||||
owner: userStub.admin,
|
|
||||||
assets: [assetStub.image],
|
|
||||||
albumThumbnailAsset: assetStub.livePhotoMotionAsset,
|
|
||||||
albumThumbnailAssetId: assetStub.livePhotoMotionAsset.id,
|
|
||||||
createdAt: new Date(),
|
|
||||||
updatedAt: new Date(),
|
|
||||||
deletedAt: null,
|
|
||||||
sharedLinks: [],
|
|
||||||
albumUsers: [],
|
|
||||||
isActivityEnabled: true,
|
|
||||||
order: AssetOrder.DESC,
|
|
||||||
}),
|
|
||||||
oneAssetValidThumbnail: Object.freeze<AlbumEntity>({
|
|
||||||
id: 'album-6',
|
|
||||||
albumName: 'Album with one asset and invalid thumbnail',
|
|
||||||
description: '',
|
|
||||||
ownerId: authStub.admin.user.id,
|
|
||||||
owner: userStub.admin,
|
|
||||||
assets: [assetStub.image],
|
|
||||||
albumThumbnailAsset: assetStub.image,
|
|
||||||
albumThumbnailAssetId: assetStub.image.id,
|
|
||||||
createdAt: new Date(),
|
|
||||||
updatedAt: new Date(),
|
|
||||||
deletedAt: null,
|
|
||||||
sharedLinks: [],
|
|
||||||
albumUsers: [],
|
|
||||||
isActivityEnabled: true,
|
|
||||||
order: AssetOrder.DESC,
|
|
||||||
}),
|
|
||||||
};
|
};
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user