From 79fccdbee00a831f869bc99cf114fdfd3f03874e Mon Sep 17 00:00:00 2001 From: shenlong <139912620+shenlong-tanwen@users.noreply.github.com> Date: Wed, 15 Apr 2026 23:00:27 +0530 Subject: [PATCH] refactor: yeet old timeline (#27666) * refactor: yank old timeline # Conflicts: # mobile/lib/presentation/pages/editing/drift_edit.page.dart # mobile/lib/providers/websocket.provider.dart # mobile/lib/routing/router.dart * more cleanup * remove native code * chore: bump sqlite-data version * remove old background tasks from BGTaskSchedulerPermittedIdentifiers * rebase --------- Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com> --- .../immich/BackgroundServicePlugin.kt | 389 -- .../app/alextran/immich/BackupWorker.kt | 394 -- .../alextran/immich/ContentObserverWorker.kt | 144 - .../kotlin/app/alextran/immich/ImmichApp.kt | 2 - .../app/alextran/immich/MainActivity.kt | 1 - .../test_utils/general_helper.dart | 14 +- mobile/ios/Podfile.lock | 22 +- mobile/ios/Runner.xcodeproj/project.pbxproj | 34 +- .../xcshareddata/swiftpm/Package.resolved | 12 +- mobile/ios/Runner/AppDelegate.swift | 25 - .../BackgroundServicePlugin.swift | 408 -- .../BackgroundSync/BackgroundSyncWorker.swift | 271 -- mobile/ios/Runner/Info.plist | 2 - mobile/lib/constants/constants.dart | 7 - mobile/lib/constants/enums.dart | 2 - .../lib/domain/interfaces/db.interface.dart | 3 - .../lib/domain/models/device_asset.model.dart | 34 - mobile/lib/domain/services/asset.service.dart | 46 - .../services/background_worker.service.dart | 28 +- mobile/lib/domain/services/log.service.dart | 6 +- mobile/lib/domain/services/store.service.dart | 10 +- mobile/lib/domain/services/user.service.dart | 22 +- mobile/lib/entities/README.md | 1 - mobile/lib/entities/album.entity.dart | 192 - mobile/lib/entities/album.entity.g.dart | 2240 ---------- .../entities/android_device_asset.entity.dart | 10 - .../android_device_asset.entity.g.dart | 463 -- mobile/lib/entities/asset.entity.dart | 575 --- mobile/lib/entities/asset.entity.g.dart | 3711 ----------------- mobile/lib/entities/backup_album.entity.dart | 22 - .../lib/entities/backup_album.entity.g.dart | 679 --- mobile/lib/entities/device_asset.entity.dart | 8 - .../lib/entities/duplicated_asset.entity.dart | 11 - .../entities/duplicated_asset.entity.g.dart | 444 -- mobile/lib/entities/etag.entity.dart | 14 - mobile/lib/entities/etag.entity.g.dart | 796 ---- .../lib/entities/ios_device_asset.entity.dart | 14 - .../entities/ios_device_asset.entity.g.dart | 766 ---- mobile/lib/entities/store.entity.dart | 34 - mobile/lib/extensions/asset_extensions.dart | 17 - .../lib/extensions/collection_extensions.dart | 28 - .../lib/extensions/translate_extensions.dart | 2 +- .../entities/device_asset.entity.dart | 25 - .../entities/device_asset.entity.g.dart | 874 ---- .../infrastructure/entities/exif.entity.dart | 90 - .../entities/exif.entity.g.dart | 3200 -------------- .../infrastructure/entities/store.entity.dart | 13 - .../entities/store.entity.g.dart | 596 --- .../infrastructure/entities/user.entity.dart | 73 - .../entities/user.entity.g.dart | 1854 -------- .../repositories/db.repository.dart | 22 +- .../repositories/device_asset.repository.dart | 31 - .../repositories/exif.repository.dart | 40 - .../repositories/logger_db.repository.dart | 3 +- .../repositories/remote_asset.repository.dart | 2 +- .../repositories/store.repository.dart | 111 +- .../repositories/user.repository.dart | 64 +- mobile/lib/main.dart | 43 +- .../album_add_asset_response.model.dart | 38 - .../albums/album_viewer_page_state.model.dart | 60 - .../asset_selection_page_result.model.dart | 18 - mobile/lib/models/asset_selection_state.dart | 47 - .../models/backup/available_album.model.dart | 35 - .../models/backup/backup_candidate.model.dart | 19 - .../lib/models/backup/backup_state.model.dart | 173 - .../backup/current_upload_asset.model.dart | 95 - .../backup/error_upload_asset.model.dart | 65 - .../backup/manual_upload_state.model.dart | 102 - .../backup/success_upload_asset.model.dart | 31 - mobile/lib/models/memories/memory.model.dart | 29 - .../models/search/search_filter.model.dart | 2 +- .../models/search/search_result.model.dart | 28 - .../search_result_page_state.model.dart | 57 - ...additional_shared_user_selection.page.dart | 115 - .../album/album_asset_selection.page.dart | 74 - .../lib/pages/album/album_control_button.dart | 37 - mobile/lib/pages/album/album_date_range.dart | 53 - mobile/lib/pages/album/album_description.dart | 42 - .../lib/pages/album/album_options.page.dart | 192 - .../pages/album/album_shared_user_icons.dart | 52 - .../album_shared_user_selection.page.dart | 140 - mobile/lib/pages/album/album_title.dart | 38 - mobile/lib/pages/album/album_viewer.dart | 165 - mobile/lib/pages/album/album_viewer.page.dart | 29 - mobile/lib/pages/albums/albums.page.dart | 359 -- .../lib/pages/backup/album_preview.page.dart | 64 - .../backup/backup_album_selection.page.dart | 225 - .../pages/backup/backup_controller.page.dart | 286 -- .../lib/pages/backup/backup_options.page.dart | 24 - .../backup/failed_backup_status.page.dart | 116 - mobile/lib/pages/common/activities.page.dart | 97 - .../pages/common/change_experience.page.dart | 168 - .../lib/pages/common/create_album.page.dart | 238 -- .../common/gallery_stacked_children.dart | 85 - .../lib/pages/common/gallery_viewer.page.dart | 438 -- .../common/native_video_viewer.page.dart | 282 -- mobile/lib/pages/common/settings.page.dart | 19 +- .../lib/pages/common/splash_screen.page.dart | 97 +- .../lib/pages/common/tab_controller.page.dart | 142 - mobile/lib/pages/editing/crop.page.dart | 177 - mobile/lib/pages/editing/edit.page.dart | 131 - mobile/lib/pages/editing/filter.page.dart | 159 - mobile/lib/pages/library/archive.page.dart | 37 - mobile/lib/pages/library/favorite.page.dart | 34 - mobile/lib/pages/library/library.page.dart | 383 -- .../lib/pages/library/local_albums.page.dart | 49 - .../lib/pages/library/locked/locked.page.dart | 82 - .../pages/library/locked/pin_auth.page.dart | 14 +- .../pages/library/partner/partner.page.dart | 139 - .../library/partner/partner_detail.page.dart | 99 - .../people/people_collection.page.dart | 127 - .../places/places_collection.page.dart | 136 - mobile/lib/pages/library/trash.page.dart | 225 - .../permission_onboarding.page.dart | 141 - mobile/lib/pages/photos/memory.page.dart | 324 -- mobile/lib/pages/photos/photos.page.dart | 130 - .../pages/search/all_motion_videos.page.dart | 25 - mobile/lib/pages/search/all_people.page.dart | 31 - mobile/lib/pages/search/all_places.page.dart | 26 - mobile/lib/pages/search/all_videos.page.dart | 22 - mobile/lib/pages/search/map/map.page.dart | 384 -- .../lib/pages/search/person_result.page.dart | 101 - .../lib/pages/search/recently_taken.page.dart | 25 - mobile/lib/pages/search/search.page.dart | 760 ---- .../pages/share_intent/share_intent.page.dart | 3 +- .../pages/search/drift_search.page.dart | 2 +- .../similar_photos_action_button.widget.dart | 2 +- .../widgets/album/album_selector.widget.dart | 2 +- .../presentation/widgets/map/map.widget.dart | 2 +- .../lib/providers/album/album.provider.dart | 151 - .../album/album_sort_by_options.provider.dart | 114 +- .../album_sort_by_options.provider.g.dart | 43 - .../album/album_viewer.provider.dart | 74 - .../album/current_album.provider.dart | 15 - .../suggested_shared_users.provider.dart | 14 - .../providers/app_life_cycle.provider.dart | 78 +- mobile/lib/providers/asset.provider.dart | 182 - .../asset_viewer/asset_people.provider.dart | 49 - .../asset_viewer/asset_people.provider.g.dart | 192 - .../asset_viewer/asset_stack.provider.dart | 42 - .../asset_viewer/asset_stack.provider.g.dart | 27 - .../asset_viewer/current_asset.provider.dart | 15 - .../current_asset.provider.g.dart | 26 - .../asset_viewer/download.provider.dart | 52 +- .../render_list_status_provider.dart | 19 - .../lib/providers/backup/backup.provider.dart | 661 +-- .../backup/backup_verification.provider.dart | 101 - .../backup_verification.provider.g.dart | 27 - .../backup/error_backup_list.provider.dart | 22 - .../ios_background_settings.provider.dart | 54 - .../backup/manual_upload.provider.dart | 391 -- mobile/lib/providers/cast.provider.dart | 21 - mobile/lib/providers/db.provider.dart | 5 - .../exceptions/image_loading_exception.dart | 5 - .../infrastructure/action.provider.dart | 6 +- .../providers/infrastructure/db.provider.dart | 7 - .../infrastructure/db.provider.g.dart | 27 - .../infrastructure/device_asset.provider.dart | 7 - .../infrastructure/exif.provider.dart | 9 - .../infrastructure/exif.provider.g.dart | 27 - .../infrastructure/store.provider.dart | 5 - .../infrastructure/store.provider.g.dart | 17 - .../infrastructure/user.provider.dart | 11 +- .../infrastructure/user.provider.g.dart | 19 +- mobile/lib/providers/memory.provider.dart | 9 - mobile/lib/providers/partner.provider.dart | 89 - .../search/all_motion_photos.provider.dart | 7 - .../search/paginated_search.provider.dart | 46 - .../search/paginated_search.provider.g.dart | 29 - .../lib/providers/search/people.provider.dart | 13 - .../providers/search/people.provider.g.dart | 122 +- .../search/recently_taken_asset.provider.dart | 9 - mobile/lib/providers/timeline.provider.dart | 68 - mobile/lib/providers/trash.provider.dart | 45 - mobile/lib/providers/user.provider.dart | 27 - mobile/lib/providers/websocket.provider.dart | 182 +- mobile/lib/repositories/album.repository.dart | 139 - .../repositories/album_api.repository.dart | 171 - .../repositories/album_media.repository.dart | 99 - mobile/lib/repositories/asset.repository.dart | 220 - .../repositories/asset_api.repository.dart | 26 +- .../repositories/asset_media.repository.dart | 39 +- mobile/lib/repositories/auth.repository.dart | 25 +- .../lib/repositories/backup.repository.dart | 32 - .../lib/repositories/database.repository.dart | 25 - mobile/lib/repositories/etag.repository.dart | 27 - .../repositories/file_media.repository.dart | 24 +- .../lib/repositories/partner.repository.dart | 34 - .../lib/repositories/timeline.repository.dart | 146 - .../lib/routing/app_navigation_observer.dart | 16 - .../lib/routing/backup_permission_guard.dart | 21 - mobile/lib/routing/gallery_guard.dart | 34 - mobile/lib/routing/router.dart | 154 - mobile/lib/routing/router.gr.dart | 1201 ------ mobile/lib/services/activity.service.dart | 23 +- mobile/lib/services/album.service.dart | 425 -- mobile/lib/services/asset.service.dart | 465 --- mobile/lib/services/background.service.dart | 595 --- mobile/lib/services/backup.service.dart | 473 --- mobile/lib/services/backup_album.service.dart | 33 - .../services/backup_verification.service.dart | 192 - mobile/lib/services/deep_link.service.dart | 145 +- mobile/lib/services/device.service.dart | 25 - mobile/lib/services/download.service.dart | 65 +- mobile/lib/services/entity.service.dart | 44 - mobile/lib/services/etag.service.dart | 14 - mobile/lib/services/exif.service.dart | 15 - mobile/lib/services/hash.service.dart | 191 - .../services/local_notification.service.dart | 118 - mobile/lib/services/memory.service.dart | 71 - mobile/lib/services/partner.service.dart | 73 - mobile/lib/services/person.service.dart | 24 +- mobile/lib/services/person.service.g.dart | 2 +- mobile/lib/services/search.service.dart | 33 +- mobile/lib/services/share.service.dart | 74 - mobile/lib/services/stack.service.dart | 64 - mobile/lib/services/sync.service.dart | 945 ----- mobile/lib/services/timeline.service.dart | 98 - mobile/lib/services/trash.service.dart | 75 - mobile/lib/utils/backup_progress.dart | 83 - mobile/lib/utils/bootstrap.dart | 63 +- mobile/lib/utils/color_filter_generator.dart | 99 - mobile/lib/utils/datetime_comparison.dart | 2 - mobile/lib/utils/hooks/blurhash_hook.dart | 12 +- mobile/lib/utils/image_url_builder.dart | 40 - mobile/lib/utils/immich_loading_overlay.dart | 64 - mobile/lib/utils/isolate.dart | 16 +- mobile/lib/utils/migration.dart | 466 +-- mobile/lib/utils/provider_utils.dart | 4 - mobile/lib/utils/selection_handlers.dart | 143 - mobile/lib/utils/string_helper.dart | 7 - mobile/lib/utils/throttle.dart | 51 - mobile/lib/utils/thumbnail_utils.dart | 49 - .../activities/activity_text_field.dart | 85 - .../lib/widgets/activities/activity_tile.dart | 113 - .../activities/dismissible_activity.dart | 2 - .../album/add_to_album_bottom_sheet.dart | 98 - .../album/add_to_album_sliverlist.dart | 65 - .../widgets/album/album_thumbnail_card.dart | 111 - .../album/album_thumbnail_listtile.dart | 93 - .../widgets/album/album_title_text_field.dart | 74 - .../widgets/album/album_viewer_appbar.dart | 307 -- .../album_viewer_editable_description.dart | 82 - .../album/album_viewer_editable_title.dart | 81 - .../album/shared_album_thumbnail_image.dart | 20 - .../widgets/asset_grid/asset_drag_region.dart | 207 - .../asset_grid/asset_grid_data_structure.dart | 307 -- .../asset_grid/control_bottom_app_bar.dart | 388 -- .../lib/widgets/asset_grid/delete_dialog.dart | 12 - .../disable_multi_select_button.dart | 31 - .../asset_grid/draggable_scrollbar.dart | 559 --- .../draggable_scrollbar_custom.dart | 490 --- .../asset_grid/group_divider_title.dart | 84 - .../widgets/asset_grid/immich_asset_grid.dart | 135 - .../asset_grid/immich_asset_grid_view.dart | 828 ---- .../widgets/asset_grid/multiselect_grid.dart | 458 -- .../multiselect_grid_status_indicator.dart | 26 - .../widgets/asset_grid/thumbnail_image.dart | 259 -- .../lib/widgets/asset_grid/upload_dialog.dart | 14 - .../asset_viewer/advanced_bottom_sheet.dart | 77 - .../asset_viewer/bottom_gallery_bar.dart | 362 -- .../asset_viewer/center_play_button.dart | 44 - .../custom_video_player_controls.dart | 114 - .../asset_viewer/description_input.dart | 106 - .../detail_panel/asset_date_time.dart | 44 - .../detail_panel/asset_details.dart | 40 - .../detail_panel/asset_location.dart | 88 - .../detail_panel/camera_info.dart | 26 - .../detail_panel/detail_panel.dart | 37 - .../asset_viewer/detail_panel/file_info.dart | 47 - .../detail_panel/people_info.dart | 91 - .../widgets/asset_viewer/gallery_app_bar.dart | 119 - .../asset_viewer/motion_photo_button.dart | 22 - .../asset_viewer/top_control_app_bar.dart | 182 - .../lib/widgets/backup/album_info_card.dart | 185 - .../widgets/backup/album_info_list_tile.dart | 98 - .../lib/widgets/backup/asset_info_table.dart | 105 - .../backup/current_backup_asset_info_box.dart | 37 - mobile/lib/widgets/backup/error_chip.dart | 28 - .../lib/widgets/backup/error_chip_text.dart | 22 - .../backup/icloud_download_progress_bar.dart | 43 - .../widgets/backup/ios_debug_info_tile.dart | 49 - .../widgets/backup/upload_progress_bar.dart | 45 - mobile/lib/widgets/backup/upload_stats.dart | 51 - .../common/app_bar_dialog/app_bar_dialog.dart | 20 +- .../app_bar_dialog/app_bar_profile_info.dart | 5 - mobile/lib/widgets/common/drag_sheet.dart | 54 - mobile/lib/widgets/common/immich_app_bar.dart | 170 - mobile/lib/widgets/common/immich_image.dart | 100 - .../lib/widgets/common/immich_thumbnail.dart | 88 - mobile/lib/widgets/common/share_dialog.dart | 19 - .../widgets/common/thumbhash_placeholder.dart | 9 - .../widgets/forms/change_password_form.dart | 9 +- .../lib/widgets/forms/login/email_input.dart | 41 - .../lib/widgets/forms/login/loading_icon.dart | 13 - .../lib/widgets/forms/login/login_button.dart | 19 - .../lib/widgets/forms/login/login_form.dart | 40 +- mobile/lib/widgets/map/map_app_bar.dart | 128 - mobile/lib/widgets/map/map_asset_grid.dart | 289 -- mobile/lib/widgets/map/map_bottom_sheet.dart | 98 - .../lib/widgets/map/map_settings_sheet.dart | 61 - .../map/positioned_asset_marker_icon.dart | 43 - .../widgets/memories/memory_bottom_info.dart | 50 - mobile/lib/widgets/memories/memory_card.dart | 117 - mobile/lib/widgets/memories/memory_lane.dart | 91 - .../widgets/search/curated_people_row.dart | 89 - .../widgets/search/curated_places_row.dart | 60 - mobile/lib/widgets/search/explore_grid.dart | 69 - .../widgets/search/person_name_edit_form.dart | 65 - .../search_filter/media_type_picker.dart | 2 +- .../widgets/search/search_map_thumbnail.dart | 27 - .../widgets/search/search_row_section.dart | 34 - .../widgets/settings/advanced_settings.dart | 47 +- .../asset_list_group_settings.dart | 2 +- .../asset_list_layout_settings.dart | 9 - .../backup_settings/background_settings.dart | 204 - .../backup_settings/backup_settings.dart | 82 - .../backup_settings/foreground_settings.dart | 35 - .../settings/beta_timeline_list_tile.dart | 71 - .../settings/local_storage_settings.dart | 51 - mobile/pubspec.lock | 58 - mobile/pubspec.yaml | 12 - mobile/test/api.mocks.dart | 2 - mobile/test/domain/service.mock.dart | 7 - .../domain/services/album.service_test.dart | 115 - .../domain/services/asset.service_test.dart | 185 - .../domain/services/log_service_test.dart | 4 +- .../domain/services/store_service_test.dart | 32 +- .../domain/services/user_service_test.dart | 15 +- mobile/test/dto.mocks.dart | 6 - mobile/test/fixtures/album.stub.dart | 104 - mobile/test/fixtures/asset.stub.dart | 55 - mobile/test/fixtures/exif.stub.dart | 18 - mobile/test/fixtures/user.stub.dart | 20 - .../repositories/exif_repository_test.dart | 49 - .../repositories/store_repository_test.dart | 71 +- .../sync_api_repository_test.dart | 13 +- .../test/infrastructure/repository.mock.dart | 8 - .../activity/activities_page_test.dart | 175 - .../test/modules/activity/activity_mocks.dart | 19 - .../activity/activity_provider_test.dart | 331 -- .../activity_statistics_provider_test.dart | 71 - .../activity/activity_text_field_test.dart | 149 - .../modules/activity/activity_tile_test.dart | 165 - .../activity/dismissible_activity_test.dart | 99 - mobile/test/modules/album/album_mocks.dart | 13 - .../album_sort_by_options_provider_test.dart | 270 -- .../asset_viewer/asset_viewer_mocks.dart | 13 - .../extensions/asset_extensions_test.dart | 113 - .../home/asset_grid_data_structure_test.dart | 113 - .../modules/map/map_theme_override_test.dart | 10 +- .../test/modules/settings/settings_mocks.dart | 4 - mobile/test/modules/shared/shared_mocks.dart | 11 - .../modules/shared/sync_service_test.dart | 285 -- mobile/test/modules/utils/migration_test.dart | 131 - mobile/test/modules/utils/throttler_test.dart | 46 - .../modules/utils/thumbnail_utils_test.dart | 63 - .../test/pages/search/search.page_test.dart | 98 - mobile/test/repository.mocks.dart | 38 +- mobile/test/service.mocks.dart | 21 - mobile/test/services/album.service_test.dart | 177 - mobile/test/services/asset.service_test.dart | 103 - mobile/test/services/auth.service_test.dart | 24 +- mobile/test/services/entity.service_test.dart | 76 - mobile/test/services/hash_service_test.dart | 349 -- mobile/test/test_utils.dart | 69 - mobile/test/test_utils/medium_factory.dart | 23 - 367 files changed, 332 insertions(+), 50870 deletions(-) delete mode 100644 mobile/android/app/src/main/kotlin/app/alextran/immich/BackgroundServicePlugin.kt delete mode 100644 mobile/android/app/src/main/kotlin/app/alextran/immich/BackupWorker.kt delete mode 100644 mobile/android/app/src/main/kotlin/app/alextran/immich/ContentObserverWorker.kt delete mode 100644 mobile/ios/Runner/BackgroundSync/BackgroundServicePlugin.swift delete mode 100644 mobile/ios/Runner/BackgroundSync/BackgroundSyncWorker.swift delete mode 100644 mobile/lib/domain/interfaces/db.interface.dart delete mode 100644 mobile/lib/domain/models/device_asset.model.dart delete mode 100644 mobile/lib/entities/README.md delete mode 100644 mobile/lib/entities/album.entity.dart delete mode 100644 mobile/lib/entities/album.entity.g.dart delete mode 100644 mobile/lib/entities/android_device_asset.entity.dart delete mode 100644 mobile/lib/entities/android_device_asset.entity.g.dart delete mode 100644 mobile/lib/entities/asset.entity.dart delete mode 100644 mobile/lib/entities/asset.entity.g.dart delete mode 100644 mobile/lib/entities/backup_album.entity.dart delete mode 100644 mobile/lib/entities/backup_album.entity.g.dart delete mode 100644 mobile/lib/entities/device_asset.entity.dart delete mode 100644 mobile/lib/entities/duplicated_asset.entity.dart delete mode 100644 mobile/lib/entities/duplicated_asset.entity.g.dart delete mode 100644 mobile/lib/entities/etag.entity.dart delete mode 100644 mobile/lib/entities/etag.entity.g.dart delete mode 100644 mobile/lib/entities/ios_device_asset.entity.dart delete mode 100644 mobile/lib/entities/ios_device_asset.entity.g.dart delete mode 100644 mobile/lib/infrastructure/entities/device_asset.entity.dart delete mode 100644 mobile/lib/infrastructure/entities/device_asset.entity.g.dart delete mode 100644 mobile/lib/infrastructure/entities/exif.entity.g.dart delete mode 100644 mobile/lib/infrastructure/entities/store.entity.g.dart delete mode 100644 mobile/lib/infrastructure/entities/user.entity.g.dart delete mode 100644 mobile/lib/infrastructure/repositories/device_asset.repository.dart delete mode 100644 mobile/lib/infrastructure/repositories/exif.repository.dart delete mode 100644 mobile/lib/models/albums/album_add_asset_response.model.dart delete mode 100644 mobile/lib/models/albums/album_viewer_page_state.model.dart delete mode 100644 mobile/lib/models/albums/asset_selection_page_result.model.dart delete mode 100644 mobile/lib/models/asset_selection_state.dart delete mode 100644 mobile/lib/models/backup/available_album.model.dart delete mode 100644 mobile/lib/models/backup/backup_candidate.model.dart delete mode 100644 mobile/lib/models/backup/backup_state.model.dart delete mode 100644 mobile/lib/models/backup/current_upload_asset.model.dart delete mode 100644 mobile/lib/models/backup/error_upload_asset.model.dart delete mode 100644 mobile/lib/models/backup/manual_upload_state.model.dart delete mode 100644 mobile/lib/models/backup/success_upload_asset.model.dart delete mode 100644 mobile/lib/models/memories/memory.model.dart delete mode 100644 mobile/lib/models/search/search_result.model.dart delete mode 100644 mobile/lib/models/search/search_result_page_state.model.dart delete mode 100644 mobile/lib/pages/album/album_additional_shared_user_selection.page.dart delete mode 100644 mobile/lib/pages/album/album_asset_selection.page.dart delete mode 100644 mobile/lib/pages/album/album_control_button.dart delete mode 100644 mobile/lib/pages/album/album_date_range.dart delete mode 100644 mobile/lib/pages/album/album_description.dart delete mode 100644 mobile/lib/pages/album/album_options.page.dart delete mode 100644 mobile/lib/pages/album/album_shared_user_icons.dart delete mode 100644 mobile/lib/pages/album/album_shared_user_selection.page.dart delete mode 100644 mobile/lib/pages/album/album_title.dart delete mode 100644 mobile/lib/pages/album/album_viewer.dart delete mode 100644 mobile/lib/pages/album/album_viewer.page.dart delete mode 100644 mobile/lib/pages/albums/albums.page.dart delete mode 100644 mobile/lib/pages/backup/album_preview.page.dart delete mode 100644 mobile/lib/pages/backup/backup_album_selection.page.dart delete mode 100644 mobile/lib/pages/backup/backup_controller.page.dart delete mode 100644 mobile/lib/pages/backup/backup_options.page.dart delete mode 100644 mobile/lib/pages/backup/failed_backup_status.page.dart delete mode 100644 mobile/lib/pages/common/activities.page.dart delete mode 100644 mobile/lib/pages/common/change_experience.page.dart delete mode 100644 mobile/lib/pages/common/create_album.page.dart delete mode 100644 mobile/lib/pages/common/gallery_stacked_children.dart delete mode 100644 mobile/lib/pages/common/gallery_viewer.page.dart delete mode 100644 mobile/lib/pages/common/native_video_viewer.page.dart delete mode 100644 mobile/lib/pages/common/tab_controller.page.dart delete mode 100644 mobile/lib/pages/editing/crop.page.dart delete mode 100644 mobile/lib/pages/editing/edit.page.dart delete mode 100644 mobile/lib/pages/editing/filter.page.dart delete mode 100644 mobile/lib/pages/library/archive.page.dart delete mode 100644 mobile/lib/pages/library/favorite.page.dart delete mode 100644 mobile/lib/pages/library/library.page.dart delete mode 100644 mobile/lib/pages/library/local_albums.page.dart delete mode 100644 mobile/lib/pages/library/locked/locked.page.dart delete mode 100644 mobile/lib/pages/library/partner/partner.page.dart delete mode 100644 mobile/lib/pages/library/partner/partner_detail.page.dart delete mode 100644 mobile/lib/pages/library/people/people_collection.page.dart delete mode 100644 mobile/lib/pages/library/places/places_collection.page.dart delete mode 100644 mobile/lib/pages/library/trash.page.dart delete mode 100644 mobile/lib/pages/onboarding/permission_onboarding.page.dart delete mode 100644 mobile/lib/pages/photos/memory.page.dart delete mode 100644 mobile/lib/pages/photos/photos.page.dart delete mode 100644 mobile/lib/pages/search/all_motion_videos.page.dart delete mode 100644 mobile/lib/pages/search/all_people.page.dart delete mode 100644 mobile/lib/pages/search/all_places.page.dart delete mode 100644 mobile/lib/pages/search/all_videos.page.dart delete mode 100644 mobile/lib/pages/search/map/map.page.dart delete mode 100644 mobile/lib/pages/search/person_result.page.dart delete mode 100644 mobile/lib/pages/search/recently_taken.page.dart delete mode 100644 mobile/lib/pages/search/search.page.dart delete mode 100644 mobile/lib/providers/album/album.provider.dart delete mode 100644 mobile/lib/providers/album/album_sort_by_options.provider.g.dart delete mode 100644 mobile/lib/providers/album/album_viewer.provider.dart delete mode 100644 mobile/lib/providers/album/current_album.provider.dart delete mode 100644 mobile/lib/providers/album/suggested_shared_users.provider.dart delete mode 100644 mobile/lib/providers/asset.provider.dart delete mode 100644 mobile/lib/providers/asset_viewer/asset_people.provider.dart delete mode 100644 mobile/lib/providers/asset_viewer/asset_people.provider.g.dart delete mode 100644 mobile/lib/providers/asset_viewer/asset_stack.provider.dart delete mode 100644 mobile/lib/providers/asset_viewer/asset_stack.provider.g.dart delete mode 100644 mobile/lib/providers/asset_viewer/current_asset.provider.dart delete mode 100644 mobile/lib/providers/asset_viewer/current_asset.provider.g.dart delete mode 100644 mobile/lib/providers/asset_viewer/render_list_status_provider.dart delete mode 100644 mobile/lib/providers/backup/backup_verification.provider.dart delete mode 100644 mobile/lib/providers/backup/backup_verification.provider.g.dart delete mode 100644 mobile/lib/providers/backup/error_backup_list.provider.dart delete mode 100644 mobile/lib/providers/backup/ios_background_settings.provider.dart delete mode 100644 mobile/lib/providers/backup/manual_upload.provider.dart delete mode 100644 mobile/lib/providers/db.provider.dart delete mode 100644 mobile/lib/providers/image/exceptions/image_loading_exception.dart delete mode 100644 mobile/lib/providers/infrastructure/db.provider.g.dart delete mode 100644 mobile/lib/providers/infrastructure/device_asset.provider.dart delete mode 100644 mobile/lib/providers/infrastructure/exif.provider.dart delete mode 100644 mobile/lib/providers/infrastructure/exif.provider.g.dart delete mode 100644 mobile/lib/providers/memory.provider.dart delete mode 100644 mobile/lib/providers/partner.provider.dart delete mode 100644 mobile/lib/providers/search/all_motion_photos.provider.dart delete mode 100644 mobile/lib/providers/search/paginated_search.provider.dart delete mode 100644 mobile/lib/providers/search/paginated_search.provider.g.dart delete mode 100644 mobile/lib/providers/search/recently_taken_asset.provider.dart delete mode 100644 mobile/lib/providers/timeline.provider.dart delete mode 100644 mobile/lib/providers/trash.provider.dart delete mode 100644 mobile/lib/repositories/album.repository.dart delete mode 100644 mobile/lib/repositories/album_api.repository.dart delete mode 100644 mobile/lib/repositories/album_media.repository.dart delete mode 100644 mobile/lib/repositories/asset.repository.dart delete mode 100644 mobile/lib/repositories/backup.repository.dart delete mode 100644 mobile/lib/repositories/database.repository.dart delete mode 100644 mobile/lib/repositories/etag.repository.dart delete mode 100644 mobile/lib/repositories/partner.repository.dart delete mode 100644 mobile/lib/repositories/timeline.repository.dart delete mode 100644 mobile/lib/routing/backup_permission_guard.dart delete mode 100644 mobile/lib/routing/gallery_guard.dart delete mode 100644 mobile/lib/services/album.service.dart delete mode 100644 mobile/lib/services/asset.service.dart delete mode 100644 mobile/lib/services/background.service.dart delete mode 100644 mobile/lib/services/backup.service.dart delete mode 100644 mobile/lib/services/backup_album.service.dart delete mode 100644 mobile/lib/services/backup_verification.service.dart delete mode 100644 mobile/lib/services/device.service.dart delete mode 100644 mobile/lib/services/entity.service.dart delete mode 100644 mobile/lib/services/etag.service.dart delete mode 100644 mobile/lib/services/exif.service.dart delete mode 100644 mobile/lib/services/hash.service.dart delete mode 100644 mobile/lib/services/local_notification.service.dart delete mode 100644 mobile/lib/services/memory.service.dart delete mode 100644 mobile/lib/services/partner.service.dart delete mode 100644 mobile/lib/services/share.service.dart delete mode 100644 mobile/lib/services/stack.service.dart delete mode 100644 mobile/lib/services/sync.service.dart delete mode 100644 mobile/lib/services/timeline.service.dart delete mode 100644 mobile/lib/services/trash.service.dart delete mode 100644 mobile/lib/utils/backup_progress.dart delete mode 100644 mobile/lib/utils/color_filter_generator.dart delete mode 100644 mobile/lib/utils/datetime_comparison.dart delete mode 100644 mobile/lib/utils/immich_loading_overlay.dart delete mode 100644 mobile/lib/utils/selection_handlers.dart delete mode 100644 mobile/lib/utils/string_helper.dart delete mode 100644 mobile/lib/utils/throttle.dart delete mode 100644 mobile/lib/utils/thumbnail_utils.dart delete mode 100644 mobile/lib/widgets/activities/activity_text_field.dart delete mode 100644 mobile/lib/widgets/activities/activity_tile.dart delete mode 100644 mobile/lib/widgets/album/add_to_album_bottom_sheet.dart delete mode 100644 mobile/lib/widgets/album/add_to_album_sliverlist.dart delete mode 100644 mobile/lib/widgets/album/album_thumbnail_card.dart delete mode 100644 mobile/lib/widgets/album/album_thumbnail_listtile.dart delete mode 100644 mobile/lib/widgets/album/album_title_text_field.dart delete mode 100644 mobile/lib/widgets/album/album_viewer_appbar.dart delete mode 100644 mobile/lib/widgets/album/album_viewer_editable_description.dart delete mode 100644 mobile/lib/widgets/album/album_viewer_editable_title.dart delete mode 100644 mobile/lib/widgets/album/shared_album_thumbnail_image.dart delete mode 100644 mobile/lib/widgets/asset_grid/asset_drag_region.dart delete mode 100644 mobile/lib/widgets/asset_grid/asset_grid_data_structure.dart delete mode 100644 mobile/lib/widgets/asset_grid/control_bottom_app_bar.dart delete mode 100644 mobile/lib/widgets/asset_grid/disable_multi_select_button.dart delete mode 100644 mobile/lib/widgets/asset_grid/draggable_scrollbar.dart delete mode 100644 mobile/lib/widgets/asset_grid/draggable_scrollbar_custom.dart delete mode 100644 mobile/lib/widgets/asset_grid/group_divider_title.dart delete mode 100644 mobile/lib/widgets/asset_grid/immich_asset_grid.dart delete mode 100644 mobile/lib/widgets/asset_grid/immich_asset_grid_view.dart delete mode 100644 mobile/lib/widgets/asset_grid/multiselect_grid.dart delete mode 100644 mobile/lib/widgets/asset_grid/multiselect_grid_status_indicator.dart delete mode 100644 mobile/lib/widgets/asset_grid/thumbnail_image.dart delete mode 100644 mobile/lib/widgets/asset_grid/upload_dialog.dart delete mode 100644 mobile/lib/widgets/asset_viewer/advanced_bottom_sheet.dart delete mode 100644 mobile/lib/widgets/asset_viewer/bottom_gallery_bar.dart delete mode 100644 mobile/lib/widgets/asset_viewer/center_play_button.dart delete mode 100644 mobile/lib/widgets/asset_viewer/custom_video_player_controls.dart delete mode 100644 mobile/lib/widgets/asset_viewer/description_input.dart delete mode 100644 mobile/lib/widgets/asset_viewer/detail_panel/asset_date_time.dart delete mode 100644 mobile/lib/widgets/asset_viewer/detail_panel/asset_details.dart delete mode 100644 mobile/lib/widgets/asset_viewer/detail_panel/asset_location.dart delete mode 100644 mobile/lib/widgets/asset_viewer/detail_panel/camera_info.dart delete mode 100644 mobile/lib/widgets/asset_viewer/detail_panel/detail_panel.dart delete mode 100644 mobile/lib/widgets/asset_viewer/detail_panel/file_info.dart delete mode 100644 mobile/lib/widgets/asset_viewer/detail_panel/people_info.dart delete mode 100644 mobile/lib/widgets/asset_viewer/gallery_app_bar.dart delete mode 100644 mobile/lib/widgets/asset_viewer/motion_photo_button.dart delete mode 100644 mobile/lib/widgets/asset_viewer/top_control_app_bar.dart delete mode 100644 mobile/lib/widgets/backup/album_info_card.dart delete mode 100644 mobile/lib/widgets/backup/album_info_list_tile.dart delete mode 100644 mobile/lib/widgets/backup/asset_info_table.dart delete mode 100644 mobile/lib/widgets/backup/current_backup_asset_info_box.dart delete mode 100644 mobile/lib/widgets/backup/error_chip.dart delete mode 100644 mobile/lib/widgets/backup/error_chip_text.dart delete mode 100644 mobile/lib/widgets/backup/icloud_download_progress_bar.dart delete mode 100644 mobile/lib/widgets/backup/ios_debug_info_tile.dart delete mode 100644 mobile/lib/widgets/backup/upload_progress_bar.dart delete mode 100644 mobile/lib/widgets/backup/upload_stats.dart delete mode 100644 mobile/lib/widgets/common/drag_sheet.dart delete mode 100644 mobile/lib/widgets/common/immich_app_bar.dart delete mode 100644 mobile/lib/widgets/common/immich_image.dart delete mode 100644 mobile/lib/widgets/common/immich_thumbnail.dart delete mode 100644 mobile/lib/widgets/common/share_dialog.dart delete mode 100644 mobile/lib/widgets/forms/login/email_input.dart delete mode 100644 mobile/lib/widgets/forms/login/loading_icon.dart delete mode 100644 mobile/lib/widgets/forms/login/login_button.dart delete mode 100644 mobile/lib/widgets/map/map_app_bar.dart delete mode 100644 mobile/lib/widgets/map/map_asset_grid.dart delete mode 100644 mobile/lib/widgets/map/map_bottom_sheet.dart delete mode 100644 mobile/lib/widgets/map/map_settings_sheet.dart delete mode 100644 mobile/lib/widgets/map/positioned_asset_marker_icon.dart delete mode 100644 mobile/lib/widgets/memories/memory_bottom_info.dart delete mode 100644 mobile/lib/widgets/memories/memory_card.dart delete mode 100644 mobile/lib/widgets/memories/memory_lane.dart delete mode 100644 mobile/lib/widgets/search/curated_people_row.dart delete mode 100644 mobile/lib/widgets/search/curated_places_row.dart delete mode 100644 mobile/lib/widgets/search/explore_grid.dart delete mode 100644 mobile/lib/widgets/search/person_name_edit_form.dart delete mode 100644 mobile/lib/widgets/search/search_map_thumbnail.dart delete mode 100644 mobile/lib/widgets/search/search_row_section.dart delete mode 100644 mobile/lib/widgets/settings/backup_settings/background_settings.dart delete mode 100644 mobile/lib/widgets/settings/backup_settings/backup_settings.dart delete mode 100644 mobile/lib/widgets/settings/backup_settings/foreground_settings.dart delete mode 100644 mobile/lib/widgets/settings/beta_timeline_list_tile.dart delete mode 100644 mobile/lib/widgets/settings/local_storage_settings.dart delete mode 100644 mobile/test/domain/services/album.service_test.dart delete mode 100644 mobile/test/domain/services/asset.service_test.dart delete mode 100644 mobile/test/dto.mocks.dart delete mode 100644 mobile/test/fixtures/exif.stub.dart delete mode 100644 mobile/test/infrastructure/repositories/exif_repository_test.dart delete mode 100644 mobile/test/modules/activity/activities_page_test.dart delete mode 100644 mobile/test/modules/activity/activity_mocks.dart delete mode 100644 mobile/test/modules/activity/activity_provider_test.dart delete mode 100644 mobile/test/modules/activity/activity_statistics_provider_test.dart delete mode 100644 mobile/test/modules/activity/activity_text_field_test.dart delete mode 100644 mobile/test/modules/activity/activity_tile_test.dart delete mode 100644 mobile/test/modules/activity/dismissible_activity_test.dart delete mode 100644 mobile/test/modules/album/album_mocks.dart delete mode 100644 mobile/test/modules/album/album_sort_by_options_provider_test.dart delete mode 100644 mobile/test/modules/asset_viewer/asset_viewer_mocks.dart delete mode 100644 mobile/test/modules/extensions/asset_extensions_test.dart delete mode 100644 mobile/test/modules/home/asset_grid_data_structure_test.dart delete mode 100644 mobile/test/modules/settings/settings_mocks.dart delete mode 100644 mobile/test/modules/shared/shared_mocks.dart delete mode 100644 mobile/test/modules/shared/sync_service_test.dart delete mode 100644 mobile/test/modules/utils/migration_test.dart delete mode 100644 mobile/test/modules/utils/throttler_test.dart delete mode 100644 mobile/test/modules/utils/thumbnail_utils_test.dart delete mode 100644 mobile/test/pages/search/search.page_test.dart delete mode 100644 mobile/test/services/album.service_test.dart delete mode 100644 mobile/test/services/asset.service_test.dart delete mode 100644 mobile/test/services/entity.service_test.dart delete mode 100644 mobile/test/services/hash_service_test.dart diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/BackgroundServicePlugin.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/BackgroundServicePlugin.kt deleted file mode 100644 index f62f25558d..0000000000 --- a/mobile/android/app/src/main/kotlin/app/alextran/immich/BackgroundServicePlugin.kt +++ /dev/null @@ -1,389 +0,0 @@ -package app.alextran.immich - -import android.app.Activity -import android.content.ContentResolver -import android.content.ContentUris -import android.content.Context -import android.content.Intent -import android.net.Uri -import android.os.Build -import android.os.Bundle -import android.provider.MediaStore -import android.provider.Settings -import android.util.Log -import androidx.annotation.RequiresApi -import io.flutter.embedding.engine.plugins.FlutterPlugin -import io.flutter.embedding.engine.plugins.activity.ActivityAware -import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding -import io.flutter.plugin.common.BinaryMessenger -import io.flutter.plugin.common.MethodCall -import io.flutter.plugin.common.MethodChannel -import io.flutter.plugin.common.MethodChannel.Result -import io.flutter.plugin.common.PluginRegistry -import java.security.MessageDigest -import java.io.FileInputStream -import kotlinx.coroutines.* -import androidx.core.net.toUri - -/** - * Android plugin for Dart `BackgroundService` and file trash operations - */ -class BackgroundServicePlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware, PluginRegistry.ActivityResultListener { - - private var methodChannel: MethodChannel? = null - private var fileTrashChannel: MethodChannel? = null - private var context: Context? = null - private var pendingResult: Result? = null - private val permissionRequestCode = 1001 - private val trashRequestCode = 1002 - private var activityBinding: ActivityPluginBinding? = null - - override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) { - onAttachedToEngine(binding.applicationContext, binding.binaryMessenger) - } - - private fun onAttachedToEngine(ctx: Context, messenger: BinaryMessenger) { - context = ctx - methodChannel = MethodChannel(messenger, "immich/foregroundChannel") - methodChannel?.setMethodCallHandler(this) - - // Add file trash channel - fileTrashChannel = MethodChannel(messenger, "file_trash") - fileTrashChannel?.setMethodCallHandler(this) - } - - override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) { - onDetachedFromEngine() - } - - private fun onDetachedFromEngine() { - methodChannel?.setMethodCallHandler(null) - methodChannel = null - fileTrashChannel?.setMethodCallHandler(null) - fileTrashChannel = null - } - - override fun onMethodCall(call: MethodCall, result: Result) { - val ctx = context!! - when (call.method) { - // Existing BackgroundService methods - "enable" -> { - val args = call.arguments>()!! - ctx.getSharedPreferences(BackupWorker.SHARED_PREF_NAME, Context.MODE_PRIVATE) - .edit() - .putBoolean(ContentObserverWorker.SHARED_PREF_SERVICE_ENABLED, true) - .putLong(BackupWorker.SHARED_PREF_CALLBACK_KEY, args[0] as Long) - .putString(BackupWorker.SHARED_PREF_NOTIFICATION_TITLE, args[1] as String) - .apply() - ContentObserverWorker.enable(ctx, immediate = args[2] as Boolean) - result.success(true) - } - - "configure" -> { - val args = call.arguments>()!! - val requireUnmeteredNetwork = args[0] as Boolean - val requireCharging = args[1] as Boolean - val triggerUpdateDelay = (args[2] as Number).toLong() - val triggerMaxDelay = (args[3] as Number).toLong() - ContentObserverWorker.configureWork( - ctx, - requireUnmeteredNetwork, - requireCharging, - triggerUpdateDelay, - triggerMaxDelay - ) - result.success(true) - } - - "disable" -> { - ContentObserverWorker.disable(ctx) - BackupWorker.stopWork(ctx) - result.success(true) - } - - "isEnabled" -> { - result.success(ContentObserverWorker.isEnabled(ctx)) - } - - "isIgnoringBatteryOptimizations" -> { - result.success(BackupWorker.isIgnoringBatteryOptimizations(ctx)) - } - - "digestFiles" -> { - val args = call.arguments>()!! - GlobalScope.launch(Dispatchers.IO) { - val buf = ByteArray(BUFFER_SIZE) - val digest: MessageDigest = MessageDigest.getInstance("SHA-1") - val hashes = arrayOfNulls(args.size) - for (i in args.indices) { - val path = args[i] - var len = 0 - try { - val file = FileInputStream(path) - file.use { assetFile -> - while (true) { - len = assetFile.read(buf) - if (len != BUFFER_SIZE) break - digest.update(buf) - } - } - digest.update(buf, 0, len) - hashes[i] = digest.digest() - } catch (e: Exception) { - // skip this file - Log.w(TAG, "Failed to hash file ${args[i]}: $e") - } - } - result.success(hashes.asList()) - } - } - - // File Trash methods moved from MainActivity - "moveToTrash" -> { - val mediaUrls = call.argument>("mediaUrls") - if (mediaUrls != null) { - if ((Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) && hasManageMediaPermission()) { - moveToTrash(mediaUrls, result) - } else { - result.error("PERMISSION_DENIED", "Media permission required", null) - } - } else { - result.error("INVALID_NAME", "The mediaUrls is not specified.", null) - } - } - - "restoreFromTrash" -> { - val fileName = call.argument("fileName") - val type = call.argument("type") - val mediaId = call.argument("mediaId") - if (fileName != null && type != null) { - if ((Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) && hasManageMediaPermission()) { - restoreFromTrash(fileName, type, result) - } else { - result.error("PERMISSION_DENIED", "Media permission required", null) - } - } else - if (mediaId != null && type != null) { - if ((Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) && hasManageMediaPermission()) { - restoreFromTrashById(mediaId, type, result) - } else { - result.error("PERMISSION_DENIED", "Media permission required", null) - } - } else { - result.error("INVALID_PARAMS", "Required params are not specified.", null) - } - } - - "requestManageMediaPermission" -> { - if (!hasManageMediaPermission()) { - requestManageMediaPermission(result) - } else { - Log.e("Manage storage permission", "Permission already granted") - result.success(true) - } - } - - "hasManageMediaPermission" -> { - if (hasManageMediaPermission()) { - Log.i("Manage storage permission", "Permission already granted") - result.success(true) - } else { - result.success(false) - } - } - - "manageMediaPermission" -> requestManageMediaPermission(result) - - else -> result.notImplemented() - } - } - - private fun hasManageMediaPermission(): Boolean { - return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - MediaStore.canManageMedia(context!!); - } else { - false - } - } - - private fun requestManageMediaPermission(result: Result) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - pendingResult = result // Store the result callback - val activity = activityBinding?.activity ?: return - - val intent = Intent(Settings.ACTION_REQUEST_MANAGE_MEDIA) - intent.data = "package:${activity.packageName}".toUri() - activity.startActivityForResult(intent, permissionRequestCode) - } else { - result.success(false) - } - } - - @RequiresApi(Build.VERSION_CODES.R) - private fun moveToTrash(mediaUrls: List, result: Result) { - val urisToTrash = mediaUrls.map { it.toUri() } - if (urisToTrash.isEmpty()) { - result.error("INVALID_ARGS", "No valid URIs provided", null) - return - } - - toggleTrash(urisToTrash, true, result); - } - - @RequiresApi(Build.VERSION_CODES.R) - private fun restoreFromTrash(name: String, type: Int, result: Result) { - val uri = getTrashedFileUri(name, type) - if (uri == null) { - Log.e("TrashError", "Asset Uri cannot be found obtained") - result.error("TrashError", "Asset Uri cannot be found obtained", null) - return - } - Log.e("FILE_URI", uri.toString()) - uri.let { toggleTrash(listOf(it), false, result) } - } - - @RequiresApi(Build.VERSION_CODES.R) - private fun restoreFromTrashById(mediaId: String, type: Int, result: Result) { - val id = mediaId.toLongOrNull() - if (id == null) { - result.error("INVALID_ID", "The file id is not a valid number: $mediaId", null) - return - } - if (!isInTrash(id)) { - result.error("TrashNotFound", "Item with id=$id not found in trash", null) - return - } - - val uri = ContentUris.withAppendedId(contentUriForType(type), id) - - try { - Log.i(TAG, "restoreFromTrashById: uri=$uri (type=$type,id=$id)") - restoreUris(listOf(uri), result) - } catch (e: Exception) { - Log.w(TAG, "restoreFromTrashById failed", e) - } - } - - @RequiresApi(Build.VERSION_CODES.R) - private fun toggleTrash(contentUris: List, isTrashed: Boolean, result: Result) { - val activity = activityBinding?.activity - val contentResolver = context?.contentResolver - if (activity == null || contentResolver == null) { - result.error("TrashError", "Activity or ContentResolver not available", null) - return - } - - try { - val pendingIntent = MediaStore.createTrashRequest(contentResolver, contentUris, isTrashed) - pendingResult = result // Store for onActivityResult - activity.startIntentSenderForResult( - pendingIntent.intentSender, - trashRequestCode, - null, 0, 0, 0 - ) - } catch (e: Exception) { - Log.e("TrashError", "Error creating or starting trash request", e) - result.error("TrashError", "Error creating or starting trash request", null) - } - } - - @RequiresApi(Build.VERSION_CODES.R) - private fun getTrashedFileUri(fileName: String, type: Int): Uri? { - val contentResolver = context?.contentResolver ?: return null - val queryUri = MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL) - val projection = arrayOf(MediaStore.Files.FileColumns._ID) - - val queryArgs = Bundle().apply { - putString( - ContentResolver.QUERY_ARG_SQL_SELECTION, - "${MediaStore.Files.FileColumns.DISPLAY_NAME} = ?" - ) - putStringArray(ContentResolver.QUERY_ARG_SQL_SELECTION_ARGS, arrayOf(fileName)) - putInt(MediaStore.QUERY_ARG_MATCH_TRASHED, MediaStore.MATCH_ONLY) - } - - contentResolver.query(queryUri, projection, queryArgs, null)?.use { cursor -> - if (cursor.moveToFirst()) { - val id = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns._ID)) - return ContentUris.withAppendedId(contentUriForType(type), id) - } - } - return null - } - - // ActivityAware implementation - override fun onAttachedToActivity(binding: ActivityPluginBinding) { - activityBinding = binding - binding.addActivityResultListener(this) - } - - override fun onDetachedFromActivityForConfigChanges() { - activityBinding?.removeActivityResultListener(this) - activityBinding = null - } - - override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) { - activityBinding = binding - binding.addActivityResultListener(this) - } - - override fun onDetachedFromActivity() { - activityBinding?.removeActivityResultListener(this) - activityBinding = null - } - - // ActivityResultListener implementation - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?): Boolean { - if (requestCode == permissionRequestCode) { - val granted = hasManageMediaPermission() - pendingResult?.success(granted) - pendingResult = null - return true - } - - if (requestCode == trashRequestCode) { - val approved = resultCode == Activity.RESULT_OK - pendingResult?.success(approved) - pendingResult = null - return true - } - return false - } - - @RequiresApi(Build.VERSION_CODES.R) - private fun isInTrash(id: Long): Boolean { - val contentResolver = context?.contentResolver ?: return false - val filesUri = MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL) - val args = Bundle().apply { - putString(ContentResolver.QUERY_ARG_SQL_SELECTION, "${MediaStore.Files.FileColumns._ID}=?") - putStringArray(ContentResolver.QUERY_ARG_SQL_SELECTION_ARGS, arrayOf(id.toString())) - putInt(MediaStore.QUERY_ARG_MATCH_TRASHED, MediaStore.MATCH_ONLY) - putInt(ContentResolver.QUERY_ARG_LIMIT, 1) - } - return contentResolver.query(filesUri, arrayOf(MediaStore.Files.FileColumns._ID), args, null) - ?.use { it.moveToFirst() } == true - } - - @RequiresApi(Build.VERSION_CODES.R) - private fun restoreUris(uris: List, result: Result) { - if (uris.isEmpty()) { - result.error("TrashError", "No URIs to restore", null) - return - } - Log.i(TAG, "restoreUris: count=${uris.size}, first=${uris.first()}") - toggleTrash(uris, false, result) - } - - @RequiresApi(Build.VERSION_CODES.Q) - private fun contentUriForType(type: Int): Uri = - when (type) { - // same order as AssetType from dart - 1 -> MediaStore.Images.Media.EXTERNAL_CONTENT_URI - 2 -> MediaStore.Video.Media.EXTERNAL_CONTENT_URI - 3 -> MediaStore.Audio.Media.EXTERNAL_CONTENT_URI - else -> MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL) - } -} - -private const val TAG = "BackgroundServicePlugin" -private const val BUFFER_SIZE = 2 * 1024 * 1024 diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/BackupWorker.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/BackupWorker.kt deleted file mode 100644 index 9c90528dc9..0000000000 --- a/mobile/android/app/src/main/kotlin/app/alextran/immich/BackupWorker.kt +++ /dev/null @@ -1,394 +0,0 @@ -package app.alextran.immich - -import android.app.Notification -import android.app.NotificationChannel -import android.app.NotificationManager -import android.content.Context -import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_SHORT_SERVICE -import android.os.Build -import android.os.Handler -import android.os.Looper -import android.os.PowerManager -import android.os.SystemClock -import android.util.Log -import androidx.annotation.RequiresApi -import androidx.core.app.NotificationCompat -import androidx.concurrent.futures.ResolvableFuture -import androidx.work.BackoffPolicy -import androidx.work.Constraints -import androidx.work.ForegroundInfo -import androidx.work.ListenableWorker -import androidx.work.NetworkType -import androidx.work.WorkerParameters -import androidx.work.ExistingWorkPolicy -import androidx.work.OneTimeWorkRequest -import androidx.work.WorkManager -import androidx.work.WorkInfo -import com.google.common.util.concurrent.ListenableFuture -import io.flutter.embedding.engine.FlutterEngine -import io.flutter.embedding.engine.dart.DartExecutor -import io.flutter.embedding.engine.loader.FlutterLoader -import io.flutter.plugin.common.MethodCall -import io.flutter.plugin.common.MethodChannel -import io.flutter.view.FlutterCallbackInformation -import java.util.concurrent.TimeUnit - -/** - * Worker executed by Android WorkManager to perform backup in background - * - * Starts the Dart runtime/engine and calls `_nativeEntry` function in - * `background.service.dart` to run the actual backup logic. - * Called by Android WorkManager when all constraints for the work are met, - * i.e. battery is not low and optionally Wifi and charging are active. - */ -class BackupWorker(ctx: Context, params: WorkerParameters) : ListenableWorker(ctx, params), - MethodChannel.MethodCallHandler { - - private val resolvableFuture = ResolvableFuture.create() - private var engine: FlutterEngine? = null - private lateinit var backgroundChannel: MethodChannel - private val notificationManager = - ctx.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager - private val isIgnoringBatteryOptimizations = isIgnoringBatteryOptimizations(applicationContext) - private var timeBackupStarted: Long = 0L - private var notificationBuilder: NotificationCompat.Builder? = null - private var notificationDetailBuilder: NotificationCompat.Builder? = null - private var fgFuture: ListenableFuture? = null - - override fun startWork(): ListenableFuture { - - Log.d(TAG, "startWork") - - val ctx = applicationContext - - if (!flutterLoader.initialized()) { - flutterLoader.startInitialization(ctx) - } - - // Create a Notification channel - createChannel() - - Log.d(TAG, "isIgnoringBatteryOptimizations $isIgnoringBatteryOptimizations") - if (isIgnoringBatteryOptimizations) { - // normal background services can only up to 10 minutes - // foreground services are allowed to run indefinitely - // requires battery optimizations to be disabled (either manually by the user - // or by the system learning that immich is important to the user) - val title = ctx.getSharedPreferences(SHARED_PREF_NAME, Context.MODE_PRIVATE) - .getString(SHARED_PREF_NOTIFICATION_TITLE, NOTIFICATION_DEFAULT_TITLE)!! - showInfo(getInfoBuilder(title, indeterminate = true).build()) - } - - engine = FlutterEngine(ctx) - - flutterLoader.ensureInitializationCompleteAsync(ctx, null, Handler(Looper.getMainLooper())) { - runDart() - - } - - return resolvableFuture - } - - /** - * Starts the Dart runtime/engine and calls `_nativeEntry` function in - * `background.service.dart` to run the actual backup logic. - */ - private fun runDart() { - val callbackDispatcherHandle = applicationContext.getSharedPreferences( - SHARED_PREF_NAME, Context.MODE_PRIVATE - ).getLong(SHARED_PREF_CALLBACK_KEY, 0L) - val callbackInformation = - FlutterCallbackInformation.lookupCallbackInformation(callbackDispatcherHandle) - val appBundlePath = flutterLoader.findAppBundlePath() - - engine?.let { engine -> - backgroundChannel = MethodChannel(engine.dartExecutor, "immich/backgroundChannel") - backgroundChannel.setMethodCallHandler(this@BackupWorker) - engine.dartExecutor.executeDartCallback( - DartExecutor.DartCallback( - applicationContext.assets, - appBundlePath, - callbackInformation - ) - ) - } - } - - override fun onStopped() { - Log.d(TAG, "onStopped") - // called when the system has to stop this worker because constraints are - // no longer met or the system needs resources for more important tasks - Handler(Looper.getMainLooper()).postAtFrontOfQueue { - if (::backgroundChannel.isInitialized) { - backgroundChannel.invokeMethod("systemStop", null) - } - } - waitOnSetForegroundAsync() - // cannot await/get(block) on resolvableFuture as its already cancelled (would throw CancellationException) - // instead, wait for 5 seconds until forcefully stopping backup work - Handler(Looper.getMainLooper()).postDelayed({ - stopEngine(null) - }, 5000) - } - - private fun waitOnSetForegroundAsync() { - val fgFuture = this.fgFuture - if (fgFuture != null && !fgFuture.isCancelled && !fgFuture.isDone) { - try { - fgFuture.get(500, TimeUnit.MILLISECONDS) - } catch (e: Exception) { - // ignored, there is nothing to be done - } - } - } - - private fun stopEngine(result: Result?) { - clearBackgroundNotification() - engine?.destroy() - engine = null - if (result != null) { - Log.d(TAG, "stopEngine result=${result}") - resolvableFuture.set(result) - } - waitOnSetForegroundAsync() - } - - @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE) - override fun onMethodCall(call: MethodCall, r: MethodChannel.Result) { - when (call.method) { - "initialized" -> { - timeBackupStarted = SystemClock.uptimeMillis() - backgroundChannel.invokeMethod( - "onAssetsChanged", - null, - object : MethodChannel.Result { - override fun notImplemented() { - stopEngine(Result.failure()) - } - - override fun error(errorCode: String, errorMessage: String?, errorDetails: Any?) { - stopEngine(Result.failure()) - } - - override fun success(receivedResult: Any?) { - val success = receivedResult as Boolean - stopEngine(if (success) Result.success() else Result.retry()) - } - } - ) - } - - "updateNotification" -> { - val args = call.arguments>()!! - val title = args[0] as String? - val content = args[1] as String? - val progress = args[2] as Int - val max = args[3] as Int - val indeterminate = args[4] as Boolean - val isDetail = args[5] as Boolean - val onlyIfFG = args[6] as Boolean - if (!onlyIfFG || isIgnoringBatteryOptimizations) { - showInfo( - getInfoBuilder(title, content, isDetail, progress, max, indeterminate).build(), - isDetail - ) - } - } - - "showError" -> { - val args = call.arguments>()!! - val title = args[0] as String - val content = args[1] as String? - val individualTag = args[2] as String? - showError(title, content, individualTag) - } - - "clearErrorNotifications" -> clearErrorNotifications() - "hasContentChanged" -> { - val lastChange = applicationContext - .getSharedPreferences(SHARED_PREF_NAME, Context.MODE_PRIVATE) - .getLong(SHARED_PREF_LAST_CHANGE, timeBackupStarted) - val hasContentChanged = lastChange > timeBackupStarted; - timeBackupStarted = SystemClock.uptimeMillis() - r.success(hasContentChanged) - } - - else -> r.notImplemented() - } - } - - private fun showError(title: String, content: String?, individualTag: String?) { - val notification = NotificationCompat.Builder(applicationContext, NOTIFICATION_CHANNEL_ERROR_ID) - .setContentTitle(title) - .setTicker(title) - .setContentText(content) - .setSmallIcon(R.drawable.notification_icon) - .build() - notificationManager.notify(individualTag, NOTIFICATION_ERROR_ID, notification) - } - - private fun clearErrorNotifications() { - notificationManager.cancel(NOTIFICATION_ERROR_ID) - } - - private fun clearBackgroundNotification() { - notificationManager.cancel(NOTIFICATION_ID) - notificationManager.cancel(NOTIFICATION_DETAIL_ID) - } - - private fun showInfo(notification: Notification, isDetail: Boolean = false) { - val id = if (isDetail) NOTIFICATION_DETAIL_ID else NOTIFICATION_ID - - if (isIgnoringBatteryOptimizations && !isDetail) { - fgFuture = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { - setForegroundAsync(ForegroundInfo(id, notification, FOREGROUND_SERVICE_TYPE_SHORT_SERVICE)) - } else { - setForegroundAsync(ForegroundInfo(id, notification)) - } - } else { - notificationManager.notify(id, notification) - } - } - - private fun getInfoBuilder( - title: String? = null, - content: String? = null, - isDetail: Boolean = false, - progress: Int = 0, - max: Int = 0, - indeterminate: Boolean = false, - ): NotificationCompat.Builder { - var builder = if (isDetail) notificationDetailBuilder else notificationBuilder - if (builder == null) { - builder = NotificationCompat.Builder(applicationContext, NOTIFICATION_CHANNEL_ID) - .setSmallIcon(R.drawable.notification_icon) - .setOnlyAlertOnce(true) - .setOngoing(true) - if (isDetail) { - notificationDetailBuilder = builder - } else { - notificationBuilder = builder - } - } - if (title != null) { - builder.setTicker(title).setContentTitle(title) - } - if (content != null) { - builder.setContentText(content) - } - return builder.setProgress(max, progress, indeterminate) - } - - private fun createChannel() { - val foreground = NotificationChannel( - NOTIFICATION_CHANNEL_ID, - NOTIFICATION_CHANNEL_ID, - NotificationManager.IMPORTANCE_LOW - ) - notificationManager.createNotificationChannel(foreground) - val error = NotificationChannel( - NOTIFICATION_CHANNEL_ERROR_ID, - NOTIFICATION_CHANNEL_ERROR_ID, - NotificationManager.IMPORTANCE_HIGH - ) - notificationManager.createNotificationChannel(error) - } - - companion object { - const val SHARED_PREF_NAME = "immichBackgroundService" - const val SHARED_PREF_CALLBACK_KEY = "callbackDispatcherHandle" - const val SHARED_PREF_NOTIFICATION_TITLE = "notificationTitle" - const val SHARED_PREF_LAST_CHANGE = "lastChange" - - private const val TASK_NAME_BACKUP = "immich/BackupWorker" - private const val NOTIFICATION_CHANNEL_ID = "immich/backgroundService" - private const val NOTIFICATION_CHANNEL_ERROR_ID = "immich/backgroundServiceError" - private const val NOTIFICATION_DEFAULT_TITLE = "Immich" - private const val NOTIFICATION_ID = 1 - private const val NOTIFICATION_ERROR_ID = 2 - private const val NOTIFICATION_DETAIL_ID = 3 - private const val ONE_MINUTE = 60000L - - /** - * Enqueues the BackupWorker to run once the constraints are met - */ - fun enqueueBackupWorker( - context: Context, - requireWifi: Boolean = false, - requireCharging: Boolean = false, - delayMilliseconds: Long = 0L - ) { - val workRequest = buildWorkRequest(requireWifi, requireCharging, delayMilliseconds) - WorkManager.getInstance(context) - .enqueueUniqueWork(TASK_NAME_BACKUP, ExistingWorkPolicy.KEEP, workRequest) - Log.d(TAG, "enqueueBackupWorker: BackupWorker enqueued") - } - - /** - * Updates the constraints of an already enqueued BackupWorker - */ - fun updateBackupWorker( - context: Context, - requireWifi: Boolean = false, - requireCharging: Boolean = false - ) { - try { - val wm = WorkManager.getInstance(context) - val workInfoFuture = wm.getWorkInfosForUniqueWork(TASK_NAME_BACKUP) - val workInfoList = workInfoFuture.get(1000, TimeUnit.MILLISECONDS) - if (workInfoList != null) { - for (workInfo in workInfoList) { - if (workInfo.state == WorkInfo.State.ENQUEUED) { - val workRequest = buildWorkRequest(requireWifi, requireCharging) - wm.enqueueUniqueWork(TASK_NAME_BACKUP, ExistingWorkPolicy.REPLACE, workRequest) - Log.d(TAG, "updateBackupWorker updated BackupWorker constraints") - return - } - } - } - Log.d(TAG, "updateBackupWorker: BackupWorker not enqueued") - } catch (e: Exception) { - Log.d(TAG, "updateBackupWorker failed: $e") - } - } - - /** - * Stops the currently running worker (if any) and removes it from the work queue - */ - fun stopWork(context: Context) { - WorkManager.getInstance(context).cancelUniqueWork(TASK_NAME_BACKUP) - Log.d(TAG, "stopWork: BackupWorker cancelled") - } - - /** - * Returns `true` if the app is ignoring battery optimizations - */ - fun isIgnoringBatteryOptimizations(ctx: Context): Boolean { - val powerManager = ctx.getSystemService(Context.POWER_SERVICE) as PowerManager - return powerManager.isIgnoringBatteryOptimizations(ctx.packageName) - } - - private fun buildWorkRequest( - requireWifi: Boolean = false, - requireCharging: Boolean = false, - delayMilliseconds: Long = 0L - ): OneTimeWorkRequest { - val constraints = Constraints.Builder() - .setRequiredNetworkType(if (requireWifi) NetworkType.UNMETERED else NetworkType.CONNECTED) - .setRequiresBatteryNotLow(true) - .setRequiresCharging(requireCharging) - .build(); - - val work = OneTimeWorkRequest.Builder(BackupWorker::class.java) - .setConstraints(constraints) - .setBackoffCriteria(BackoffPolicy.EXPONENTIAL, ONE_MINUTE, TimeUnit.MILLISECONDS) - .setInitialDelay(delayMilliseconds, TimeUnit.MILLISECONDS) - .build() - return work - } - - private val flutterLoader = FlutterLoader() - } -} - -private const val TAG = "BackupWorker" diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/ContentObserverWorker.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/ContentObserverWorker.kt deleted file mode 100644 index 9cb2ec7779..0000000000 --- a/mobile/android/app/src/main/kotlin/app/alextran/immich/ContentObserverWorker.kt +++ /dev/null @@ -1,144 +0,0 @@ -package app.alextran.immich - -import android.content.Context -import android.os.SystemClock -import android.provider.MediaStore -import android.util.Log -import androidx.work.Constraints -import androidx.work.Worker -import androidx.work.WorkerParameters -import androidx.work.ExistingWorkPolicy -import androidx.work.OneTimeWorkRequest -import androidx.work.WorkManager -import androidx.work.Operation -import java.util.concurrent.TimeUnit - -/** - * Worker executed by Android WorkManager observing content changes (new photos/videos) - * - * Immediately enqueues the BackupWorker when running. - * As this work is not triggered periodically, but on content change, the - * worker enqueues itself again after each run. - */ -class ContentObserverWorker(ctx: Context, params: WorkerParameters) : Worker(ctx, params) { - - override fun doWork(): Result { - if (!isEnabled(applicationContext)) { - return Result.failure() - } - if (triggeredContentUris.size > 0) { - startBackupWorker(applicationContext, delayMilliseconds = 0) - } - enqueueObserverWorker(applicationContext, ExistingWorkPolicy.REPLACE) - return Result.success() - } - - companion object { - const val SHARED_PREF_SERVICE_ENABLED = "serviceEnabled" - private const val SHARED_PREF_REQUIRE_WIFI = "requireWifi" - private const val SHARED_PREF_REQUIRE_CHARGING = "requireCharging" - private const val SHARED_PREF_TRIGGER_UPDATE_DELAY = "triggerUpdateDelay" - private const val SHARED_PREF_TRIGGER_MAX_DELAY = "triggerMaxDelay" - - private const val TASK_NAME_OBSERVER = "immich/ContentObserver" - - /** - * Enqueues the `ContentObserverWorker`. - * - * @param context Android Context - */ - fun enable(context: Context, immediate: Boolean = false) { - enqueueObserverWorker(context, ExistingWorkPolicy.KEEP) - Log.d(TAG, "enabled ContentObserverWorker") - if (immediate) { - startBackupWorker(context, delayMilliseconds = 5000) - } - } - - /** - * Configures the `BackupWorker` to run when all constraints are met. - * - * @param context Android Context - * @param requireWifi if true, task only runs if connected to wifi - * @param requireCharging if true, task only runs if device is charging - */ - fun configureWork(context: Context, - requireWifi: Boolean = false, - requireCharging: Boolean = false, - triggerUpdateDelay: Long = 5000, - triggerMaxDelay: Long = 50000) { - context.getSharedPreferences(BackupWorker.SHARED_PREF_NAME, Context.MODE_PRIVATE) - .edit() - .putBoolean(SHARED_PREF_SERVICE_ENABLED, true) - .putBoolean(SHARED_PREF_REQUIRE_WIFI, requireWifi) - .putBoolean(SHARED_PREF_REQUIRE_CHARGING, requireCharging) - .putLong(SHARED_PREF_TRIGGER_UPDATE_DELAY, triggerUpdateDelay) - .putLong(SHARED_PREF_TRIGGER_MAX_DELAY, triggerMaxDelay) - .apply() - BackupWorker.updateBackupWorker(context, requireWifi, requireCharging) - } - - /** - * Stops the currently running worker (if any) and removes it from the work queue - */ - fun disable(context: Context) { - context.getSharedPreferences(BackupWorker.SHARED_PREF_NAME, Context.MODE_PRIVATE) - .edit().putBoolean(SHARED_PREF_SERVICE_ENABLED, false).apply() - WorkManager.getInstance(context).cancelUniqueWork(TASK_NAME_OBSERVER) - Log.d(TAG, "disabled ContentObserverWorker") - } - - /** - * Return true if the user has enabled the background backup service - */ - fun isEnabled(ctx: Context): Boolean { - return ctx.getSharedPreferences(BackupWorker.SHARED_PREF_NAME, Context.MODE_PRIVATE) - .getBoolean(SHARED_PREF_SERVICE_ENABLED, false) - } - - /** - * Enqueue and replace the worker without the content trigger but with a short delay - */ - fun workManagerAppClearedWorkaround(context: Context) { - val work = OneTimeWorkRequest.Builder(ContentObserverWorker::class.java) - .setInitialDelay(500, TimeUnit.MILLISECONDS) - .build() - WorkManager - .getInstance(context) - .enqueueUniqueWork(TASK_NAME_OBSERVER, ExistingWorkPolicy.REPLACE, work) - .result - .get() - Log.d(TAG, "workManagerAppClearedWorkaround") - } - - private fun enqueueObserverWorker(context: Context, policy: ExistingWorkPolicy) { - val sp = context.getSharedPreferences(BackupWorker.SHARED_PREF_NAME, Context.MODE_PRIVATE) - val constraints = Constraints.Builder() - .addContentUriTrigger(MediaStore.Images.Media.INTERNAL_CONTENT_URI, true) - .addContentUriTrigger(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, true) - .addContentUriTrigger(MediaStore.Video.Media.INTERNAL_CONTENT_URI, true) - .addContentUriTrigger(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, true) - .setTriggerContentUpdateDelay(sp.getLong(SHARED_PREF_TRIGGER_UPDATE_DELAY, 5000), TimeUnit.MILLISECONDS) - .setTriggerContentMaxDelay(sp.getLong(SHARED_PREF_TRIGGER_MAX_DELAY, 50000), TimeUnit.MILLISECONDS) - .build() - - val work = OneTimeWorkRequest.Builder(ContentObserverWorker::class.java) - .setConstraints(constraints) - .build() - WorkManager.getInstance(context).enqueueUniqueWork(TASK_NAME_OBSERVER, policy, work) - } - - fun startBackupWorker(context: Context, delayMilliseconds: Long) { - val sp = context.getSharedPreferences(BackupWorker.SHARED_PREF_NAME, Context.MODE_PRIVATE) - if (!sp.getBoolean(SHARED_PREF_SERVICE_ENABLED, false)) - return - val requireWifi = sp.getBoolean(SHARED_PREF_REQUIRE_WIFI, true) - val requireCharging = sp.getBoolean(SHARED_PREF_REQUIRE_CHARGING, false) - BackupWorker.enqueueBackupWorker(context, requireWifi, requireCharging, delayMilliseconds) - sp.edit().putLong(BackupWorker.SHARED_PREF_LAST_CHANGE, SystemClock.uptimeMillis()).apply() - } - - } -} - -private const val TAG = "ContentObserverWorker" diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/ImmichApp.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/ImmichApp.kt index 4474c63e09..37a325e896 100644 --- a/mobile/android/app/src/main/kotlin/app/alextran/immich/ImmichApp.kt +++ b/mobile/android/app/src/main/kotlin/app/alextran/immich/ImmichApp.kt @@ -18,8 +18,6 @@ class ImmichApp : Application() { // Thus, the BackupWorker is not started. If the system kills the process after each initialization // (because of low memory etc.), the backup is never performed. // As a workaround, we also run a backup check when initializing the application - - ContentObserverWorker.startBackupWorker(context = this, delayMilliseconds = 0) Handler(Looper.getMainLooper()).postDelayed({ // We can only check the engine count and not the status of the lock here, // as the previous start might have been killed without unlocking. diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/MainActivity.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/MainActivity.kt index 06649de8f0..2c80b8d2bd 100644 --- a/mobile/android/app/src/main/kotlin/app/alextran/immich/MainActivity.kt +++ b/mobile/android/app/src/main/kotlin/app/alextran/immich/MainActivity.kt @@ -51,7 +51,6 @@ class MainActivity : FlutterFragmentActivity() { BackgroundWorkerFgHostApi.setUp(messenger, BackgroundWorkerApiImpl(ctx)) ConnectivityApi.setUp(messenger, ConnectivityApiImpl(ctx)) - flutterEngine.plugins.add(BackgroundServicePlugin()) flutterEngine.plugins.add(backgroundEngineLockImpl) flutterEngine.plugins.add(nativeSyncApiImpl) } diff --git a/mobile/integration_test/test_utils/general_helper.dart b/mobile/integration_test/test_utils/general_helper.dart index d6065170ef..66955364f3 100644 --- a/mobile/integration_test/test_utils/general_helper.dart +++ b/mobile/integration_test/test_utils/general_helper.dart @@ -5,7 +5,6 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/main.dart' as app; -import 'package:immich_mobile/providers/db.provider.dart'; import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; import 'package:immich_mobile/utils/bootstrap.dart'; import 'package:integration_test/integration_test.dart'; @@ -39,20 +38,11 @@ class ImmichTestHelper { static Future loadApp(WidgetTester tester) async { await EasyLocalization.ensureInitialized(); // Clear all data from Isar (reuse existing instance if available) - final (isar, drift, logDb) = await Bootstrap.initDB(); - await Bootstrap.initDomain(isar, drift, logDb); + final (drift, _) = await Bootstrap.initDomain(); await Store.clear(); - await isar.writeTxn(() => isar.clear()); // Load main Widget await tester.pumpWidget( - ProviderScope( - overrides: [ - dbProvider.overrideWithValue(isar), - isarProvider.overrideWithValue(isar), - driftProvider.overrideWith(driftOverride(drift)), - ], - child: const app.MainWidget(), - ), + ProviderScope(overrides: [driftProvider.overrideWith(driftOverride(drift))], child: const app.MainWidget()), ); // Post run tasks await EasyLocalization.ensureInitialized(); diff --git a/mobile/ios/Podfile.lock b/mobile/ios/Podfile.lock index e1ec4aff07..c0d7e2c35a 100644 --- a/mobile/ios/Podfile.lock +++ b/mobile/ios/Podfile.lock @@ -33,8 +33,6 @@ PODS: - Flutter - integration_test (0.0.1): - Flutter - - isar_community_flutter_libs (1.0.0): - - Flutter - local_auth_darwin (0.0.1): - Flutter - FlutterMacOS @@ -75,16 +73,16 @@ PODS: - sqflite_darwin (0.0.4): - Flutter - FlutterMacOS - - sqlite3 (3.49.1): - - sqlite3/common (= 3.49.1) - - sqlite3/common (3.49.1) - - sqlite3/dbstatvtab (3.49.1): + - sqlite3 (3.49.2): + - sqlite3/common (= 3.49.2) + - sqlite3/common (3.49.2) + - sqlite3/dbstatvtab (3.49.2): - sqlite3/common - - sqlite3/fts5 (3.49.1): + - sqlite3/fts5 (3.49.2): - sqlite3/common - - sqlite3/perf-threadsafe (3.49.1): + - sqlite3/perf-threadsafe (3.49.2): - sqlite3/common - - sqlite3/rtree (3.49.1): + - sqlite3/rtree (3.49.2): - sqlite3/common - sqlite3_flutter_libs (0.0.1): - Flutter @@ -116,7 +114,6 @@ DEPENDENCIES: - home_widget (from `.symlinks/plugins/home_widget/ios`) - image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`) - integration_test (from `.symlinks/plugins/integration_test/ios`) - - isar_community_flutter_libs (from `.symlinks/plugins/isar_community_flutter_libs/ios`) - local_auth_darwin (from `.symlinks/plugins/local_auth_darwin/darwin`) - maplibre_gl (from `.symlinks/plugins/maplibre_gl/ios`) - native_video_player (from `.symlinks/plugins/native_video_player/ios`) @@ -174,8 +171,6 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/image_picker_ios/ios" integration_test: :path: ".symlinks/plugins/integration_test/ios" - isar_community_flutter_libs: - :path: ".symlinks/plugins/isar_community_flutter_libs/ios" local_auth_darwin: :path: ".symlinks/plugins/local_auth_darwin/darwin" maplibre_gl: @@ -228,7 +223,6 @@ SPEC CHECKSUMS: home_widget: f169fc41fd807b4d46ab6615dc44d62adbf9f64f image_picker_ios: e0ece4aa2a75771a7de3fa735d26d90817041326 integration_test: 4a889634ef21a45d28d50d622cf412dc6d9f586e - isar_community_flutter_libs: bede843185a61a05ff364a05c9b23209523f7e0d local_auth_darwin: 553ce4f9b16d3fdfeafce9cf042e7c9f77c1c391 MapLibre: 69e572367f4ef6287e18246cfafc39c80cdcabcd maplibre_gl: 3c924e44725147b03dda33430ad216005b40555f @@ -245,7 +239,7 @@ SPEC CHECKSUMS: share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7 sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0 - sqlite3: fc1400008a9b3525f5914ed715a5d1af0b8f4983 + sqlite3: 3c950dc86011117c307eb0b28c4a7bb449dce9f1 sqlite3_flutter_libs: f8fc13346870e73fe35ebf6dbb997fbcd156b241 url_launcher_ios: 694010445543906933d732453a59da0a173ae33d wakelock_plus: e29112ab3ef0b318e58cfa5c32326458be66b556 diff --git a/mobile/ios/Runner.xcodeproj/project.pbxproj b/mobile/ios/Runner.xcodeproj/project.pbxproj index 178454f381..f88d624b89 100644 --- a/mobile/ios/Runner.xcodeproj/project.pbxproj +++ b/mobile/ios/Runner.xcodeproj/project.pbxproj @@ -10,8 +10,6 @@ 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 3B6A31FED0FC846D6BD69BBC /* Pods_ShareExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 357FC57E54FD0F51795CF28A /* Pods_ShareExtension.framework */; }; - 65F32F31299BD2F800CE9261 /* BackgroundServicePlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65F32F30299BD2F800CE9261 /* BackgroundServicePlugin.swift */; }; - 65F32F33299D349D00CE9261 /* BackgroundSyncWorker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65F32F32299D349D00CE9261 /* BackgroundSyncWorker.swift */; }; 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; @@ -90,8 +88,6 @@ 357FC57E54FD0F51795CF28A /* Pods_ShareExtension.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_ShareExtension.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; 571EAA93D77181C7C98C2EA6 /* Pods-ShareExtension.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ShareExtension.release.xcconfig"; path = "Target Support Files/Pods-ShareExtension/Pods-ShareExtension.release.xcconfig"; sourceTree = ""; }; - 65F32F30299BD2F800CE9261 /* BackgroundServicePlugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundServicePlugin.swift; sourceTree = ""; }; - 65F32F32299D349D00CE9261 /* BackgroundSyncWorker.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BackgroundSyncWorker.swift; sourceTree = ""; }; 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; @@ -151,11 +147,15 @@ /* Begin PBXFileSystemSynchronizedRootGroup section */ B231F52D2E93A44A00BC45D1 /* Core */ = { isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + ); path = Core; sourceTree = ""; }; B2CF7F8C2DDE4EBB00744BF6 /* Sync */ = { isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + ); path = Sync; sourceTree = ""; }; @@ -177,6 +177,8 @@ }; FEE084F22EC172080045228E /* Schemas */ = { isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + ); path = Schemas; sourceTree = ""; }; @@ -238,15 +240,6 @@ name = Frameworks; sourceTree = ""; }; - 65DD438629917FAD0047FFA8 /* BackgroundSync */ = { - isa = PBXGroup; - children = ( - 65F32F32299D349D00CE9261 /* BackgroundSyncWorker.swift */, - 65F32F30299BD2F800CE9261 /* BackgroundServicePlugin.swift */, - ); - path = BackgroundSync; - sourceTree = ""; - }; 9740EEB11CF90186004384FC /* Flutter */ = { isa = PBXGroup; children = ( @@ -291,7 +284,6 @@ B21E34A62E5AF9760031FDB9 /* Background */, B2CF7F8C2DDE4EBB00744BF6 /* Sync */, FA9973382CF6DF4B000EF859 /* Runner.entitlements */, - 65DD438629917FAD0047FFA8 /* BackgroundSync */, FAC7416727DB9F5500C668D8 /* RunnerProfile.entitlements */, 97C146FA1CF9000F007C117D /* Main.storyboard */, 97C146FD1CF9000F007C117D /* Assets.xcassets */, @@ -571,14 +563,10 @@ inputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist", ); - inputPaths = ( - ); name = "[CP] Copy Pods Resources"; outputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist", ); - outputPaths = ( - ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; @@ -607,14 +595,10 @@ inputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", ); - inputPaths = ( - ); name = "[CP] Embed Pods Frameworks"; outputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", ); - outputPaths = ( - ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; @@ -627,7 +611,6 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 65F32F31299BD2F800CE9261 /* BackgroundServicePlugin.swift in Sources */, 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, A01DD69B2F7F43B40049AB63 /* ImageRequest.swift in Sources */, B21E34AC2E5B09190031FDB9 /* BackgroundWorker.swift in Sources */, @@ -642,7 +625,6 @@ B21E34AA2E5AFD2B0031FDB9 /* BackgroundWorkerApiImpl.swift in Sources */, 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, B2BE315F2E5E5229006EEF88 /* BackgroundWorker.g.swift in Sources */, - 65F32F33299D349D00CE9261 /* BackgroundSyncWorker.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1261,7 +1243,7 @@ repositoryURL = "https://github.com/pointfreeco/sqlite-data"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 1.3.0; + minimumVersion = 1.6.1; }; }; FEE084F92EC1725A0045228E /* XCRemoteSwiftPackageReference "swift-http-structured-headers" */ = { @@ -1269,7 +1251,7 @@ repositoryURL = "https://github.com/apple/swift-http-structured-headers.git"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 1.5.0; + minimumVersion = 1.6.0; }; }; /* End XCRemoteSwiftPackageReference section */ diff --git a/mobile/ios/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved b/mobile/ios/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved index 4962230c22..800ff8ac52 100644 --- a/mobile/ios/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/mobile/ios/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -24,8 +24,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/sqlite-data", "state" : { - "revision" : "05704b563ecb7f0bd7e49b6f360a6383a3e53e7d", - "version" : "1.5.1" + "revision" : "da3a94ed49c7a30d82853de551c07a93196e8cab", + "version" : "1.6.1" } }, { @@ -78,8 +78,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-http-structured-headers.git", "state" : { - "revision" : "a9f3c352f4d46afd155e00b3c6e85decae6bcbeb", - "version" : "1.5.0" + "revision" : "76d7627bd88b47bf5a0f8497dd244885960dde0b", + "version" : "1.6.0" } }, { @@ -123,8 +123,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-structured-queries", "state" : { - "revision" : "d8163b3a98f3c8434c4361e85126db449d84bc66", - "version" : "0.30.0" + "revision" : "8da8818fccd9959bd683934ddc62cf45bb65b3c8", + "version" : "0.31.1" } }, { diff --git a/mobile/ios/Runner/AppDelegate.swift b/mobile/ios/Runner/AppDelegate.swift index 81af41ab08..2d41fd541e 100644 --- a/mobile/ios/Runner/AppDelegate.swift +++ b/mobile/ios/Runner/AppDelegate.swift @@ -24,33 +24,8 @@ import UIKit GeneratedPluginRegistrant.register(with: self) let controller: FlutterViewController = window?.rootViewController as! FlutterViewController AppDelegate.registerPlugins(with: controller.engine, controller: controller) - BackgroundServicePlugin.register(with: self.registrar(forPlugin: "BackgroundServicePlugin")!) - - BackgroundServicePlugin.registerBackgroundProcessing() BackgroundWorkerApiImpl.registerBackgroundWorkers() - BackgroundServicePlugin.setPluginRegistrantCallback { registry in - if !registry.hasPlugin("org.cocoapods.path-provider-foundation") { - PathProviderPlugin.register(with: registry.registrar(forPlugin: "org.cocoapods.path-provider-foundation")!) - } - - if !registry.hasPlugin("org.cocoapods.photo-manager") { - PhotoManagerPlugin.register(with: registry.registrar(forPlugin: "org.cocoapods.photo-manager")!) - } - - if !registry.hasPlugin("org.cocoapods.shared-preferences-foundation") { - SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "org.cocoapods.shared-preferences-foundation")!) - } - - if !registry.hasPlugin("org.cocoapods.permission-handler-apple") { - PermissionHandlerPlugin.register(with: registry.registrar(forPlugin: "org.cocoapods.permission-handler-apple")!) - } - - if !registry.hasPlugin("org.cocoapods.network-info-plus") { - FPPNetworkInfoPlusPlugin.register(with: registry.registrar(forPlugin: "org.cocoapods.network-info-plus")!) - } - } - return super.application(application, didFinishLaunchingWithOptions: launchOptions) } diff --git a/mobile/ios/Runner/BackgroundSync/BackgroundServicePlugin.swift b/mobile/ios/Runner/BackgroundSync/BackgroundServicePlugin.swift deleted file mode 100644 index cac9faab01..0000000000 --- a/mobile/ios/Runner/BackgroundSync/BackgroundServicePlugin.swift +++ /dev/null @@ -1,408 +0,0 @@ -// -// BackgroundServicePlugin.swift -// Runner -// -// Created by Marty Fuhry on 2/14/23. -// - -import Flutter -import BackgroundTasks -import path_provider_foundation -import CryptoKit -import Network - -class BackgroundServicePlugin: NSObject, FlutterPlugin { - - public static var flutterPluginRegistrantCallback: FlutterPluginRegistrantCallback? - - public static func setPluginRegistrantCallback(_ callback: FlutterPluginRegistrantCallback) { - flutterPluginRegistrantCallback = callback - } - - // Pause the application in XCode, then enter - // e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWithIdentifier:@"app.alextran.immich.backgroundFetch"] - // or - // e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWithIdentifier:@"app.alextran.immich.backgroundProcessing"] - // Then resume the application see the background code run - // Tested on a physical device, not a simulator - // This will submit either the Fetch or Processing command to the BGTaskScheduler for immediate processing. - // In my tests, I can only get app.alextran.immich.backgroundProcessing simulated by running the above command - - // This is the task ID in Info.plist to register as our background task ID - public static let backgroundFetchTaskID = "app.alextran.immich.backgroundFetch" - public static let backgroundProcessingTaskID = "app.alextran.immich.backgroundProcessing" - - // Establish communication with the main isolate and set up the channel call - // to this BackgroundServicePlugion() - public static func register(with registrar: FlutterPluginRegistrar) { - let channel = FlutterMethodChannel( - name: "immich/foregroundChannel", - binaryMessenger: registrar.messenger() - ) - - let instance = BackgroundServicePlugin() - registrar.addMethodCallDelegate(instance, channel: channel) - registrar.addApplicationDelegate(instance) - } - - // Registers the Flutter engine with the plugins, used by the other Background Flutter engine - public static func register(engine: FlutterEngine) { - GeneratedPluginRegistrant.register(with: engine) - } - - // Registers the task IDs from the system so that we can process them here in this class - public static func registerBackgroundProcessing() { - - let processingRegisterd = BGTaskScheduler.shared.register( - forTaskWithIdentifier: backgroundProcessingTaskID, - using: nil) { task in - if task is BGProcessingTask { - handleBackgroundProcessing(task: task as! BGProcessingTask) - } - } - - let fetchRegisterd = BGTaskScheduler.shared.register( - forTaskWithIdentifier: backgroundFetchTaskID, - using: nil) { task in - if task is BGAppRefreshTask { - handleBackgroundFetch(task: task as! BGAppRefreshTask) - } - } - } - - // Handles the channel methods from Flutter - public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { - switch call.method { - case "enable": - handleBackgroundEnable(call: call, result: result) - break - case "configure": - handleConfigure(call: call, result: result) - break - case "disable": - handleDisable(call: call, result: result) - break - case "isEnabled": - handleIsEnabled(call: call, result: result) - break - case "isIgnoringBatteryOptimizations": - result(FlutterMethodNotImplemented) - break - case "lastBackgroundFetchTime": - let defaults = UserDefaults.standard - let lastRunTime = defaults.value(forKey: "last_background_fetch_run_time") - result(lastRunTime) - break - case "lastBackgroundProcessingTime": - let defaults = UserDefaults.standard - let lastRunTime = defaults.value(forKey: "last_background_processing_run_time") - result(lastRunTime) - break - case "numberOfBackgroundProcesses": - handleNumberOfProcesses(call: call, result: result) - break - case "backgroundAppRefreshEnabled": - handleBackgroundRefreshStatus(call: call, result: result) - break - case "digestFiles": - handleDigestFiles(call: call, result: result) - break - default: - result(FlutterMethodNotImplemented) - break - } - } - - // Calculates the SHA-1 hash of each file from the list of paths provided - func handleDigestFiles(call: FlutterMethodCall, result: @escaping FlutterResult) { - - let bufsize = 2 * 1024 * 1024 - // Private error to throw if file cannot be read - enum DigestError: String, LocalizedError { - case NoFileHandle = "Cannot Open File Handle" - - public var errorDescription: String? { self.rawValue } - } - - // Parse the arguments or else fail - guard let args = call.arguments as? Array else { - print("Cannot parse args as array: \(String(describing: call.arguments))") - result(FlutterError(code: "Malformed", - message: "Received args is not an Array", - details: nil)) - return - } - - // Compute hash in background thread - DispatchQueue.global(qos: .background).async { - var hashes: [FlutterStandardTypedData?] = Array(repeating: nil, count: args.count) - for i in (0 ..< args.count) { - do { - guard let file = FileHandle(forReadingAtPath: args[i]) else { throw DigestError.NoFileHandle } - var hasher = Insecure.SHA1.init(); - while autoreleasepool(invoking: { - let chunk = file.readData(ofLength: bufsize) - guard !chunk.isEmpty else { return false } // EOF - hasher.update(data: chunk) - return true // continue - }) { } - let digest = hasher.finalize() - hashes[i] = FlutterStandardTypedData(bytes: Data(Array(digest.makeIterator()))) - } catch { - print("Cannot calculate the digest of the file \(args[i]) due to \(error.localizedDescription)") - } - } - - // Return result in main thread - DispatchQueue.main.async { - result(Array(hashes)) - } - } - } - - // Called by the flutter code when enabled so that we can turn on the background services - // and save the callback information to communicate on this method channel - public func handleBackgroundEnable(call: FlutterMethodCall, result: FlutterResult) { - - // Needs to parse the arguments from the method call - guard let args = call.arguments as? Array else { - print("Cannot parse args as array: \(call.arguments)") - result(FlutterMethodNotImplemented) - return - } - - // Requires 3 arguments in the array - guard args.count == 3 else { - print("Requires 3 arguments and received \(args.count)") - result(FlutterMethodNotImplemented) - return - } - - // Parses the arguments - let callbackHandle = args[0] as? Int64 - let notificationTitle = args[1] as? String - let instant = args[2] as? Bool - - // Write enabled to settings - let defaults = UserDefaults.standard - - // We are now enabled, so store this - defaults.set(true, forKey: "background_service_enabled") - - // The callback handle is an int64 address to communicate with the main isolate's - // entry function - defaults.set(callbackHandle, forKey: "callback_handle") - - // This is not used yet and will need to be implemented - defaults.set(notificationTitle, forKey: "notification_title") - - // Schedule the background services - BackgroundServicePlugin.scheduleBackgroundSync() - BackgroundServicePlugin.scheduleBackgroundFetch() - - result(true) - } - - // Called by the flutter code at launch to see if the background service is enabled or not - func handleIsEnabled(call: FlutterMethodCall, result: FlutterResult) { - let defaults = UserDefaults.standard - let enabled = defaults.value(forKey: "background_service_enabled") as? Bool - - // False by default - result(enabled ?? false) - } - - // Called by the Flutter code whenever a change in configuration is set - func handleConfigure(call: FlutterMethodCall, result: FlutterResult) { - - // Needs to be able to parse the arguments or else fail - guard let args = call.arguments as? Array else { - print("Cannot parse args as array: \(call.arguments)") - result(FlutterError()) - return - } - - // Needs to have 4 arguments in the call or else fail - guard args.count == 4 else { - print("Not enough arguments, 4 required: \(args.count) given") - result(FlutterError()) - return - } - - // Parse the arguments from the method call - let requireUnmeteredNetwork = args[0] as? Bool - let requireCharging = args[1] as? Bool - let triggerUpdateDelay = args[2] as? Int - let triggerMaxDelay = args[3] as? Int - - // Store the values from the call in the defaults - let defaults = UserDefaults.standard - defaults.set(requireUnmeteredNetwork, forKey: "require_unmetered_network") - defaults.set(requireCharging, forKey: "require_charging") - defaults.set(triggerUpdateDelay, forKey: "trigger_update_delay") - defaults.set(triggerMaxDelay, forKey: "trigger_max_delay") - - // Cancel the background services and reschedule them - BGTaskScheduler.shared.cancelAllTaskRequests() - BackgroundServicePlugin.scheduleBackgroundSync() - BackgroundServicePlugin.scheduleBackgroundFetch() - result(true) - } - - // Returns the number of currently scheduled background processes to Flutter, strictly - // for debugging - func handleNumberOfProcesses(call: FlutterMethodCall, result: @escaping FlutterResult) { - BGTaskScheduler.shared.getPendingTaskRequests { requests in - result(requests.count) - } - } - - // Disables the service, cancels all the task requests - func handleDisable(call: FlutterMethodCall, result: FlutterResult) { - let defaults = UserDefaults.standard - defaults.set(false, forKey: "background_service_enabled") - - BGTaskScheduler.shared.cancelAllTaskRequests() - result(true) - } - - // Checks the status of the Background App Refresh from the system - // Returns true if the service is enabled for Immich, and false otherwise - func handleBackgroundRefreshStatus(call: FlutterMethodCall, result: FlutterResult) { - switch UIApplication.shared.backgroundRefreshStatus { - case .available: - result(true) - break - case .denied: - result(false) - break - case .restricted: - result(false) - break - default: - result(false) - break - } - } - - - // Schedules a short-running background sync to sync only a few photos - static func scheduleBackgroundFetch() { - // We will schedule this task to run no matter the charging or wifi requirents from the end user - // 1. They can set Background App Refresh to Off / Wi-Fi / Wi-Fi & Cellular Data from Settings - // 2. We will check the battery connectivity when we begin running the background activity - let backgroundFetch = BGAppRefreshTaskRequest(identifier: BackgroundServicePlugin.backgroundFetchTaskID) - - // Use 5 minutes from now as earliest begin date - backgroundFetch.earliestBeginDate = Date(timeIntervalSinceNow: 5 * 60) - - do { - try BGTaskScheduler.shared.submit(backgroundFetch) - } catch { - print("Could not schedule the background task \(error.localizedDescription)") - } - } - - // Schedules a long-running background sync for syncing all of the photos - static func scheduleBackgroundSync() { - let backgroundProcessing = BGProcessingTaskRequest(identifier: BackgroundServicePlugin.backgroundProcessingTaskID) - - // We need the values for requiring charging - let defaults = UserDefaults.standard - let requireCharging = defaults.value(forKey: "require_charging") as? Bool - - // Always require network connectivity, and set the require charging from the above - backgroundProcessing.requiresNetworkConnectivity = true - backgroundProcessing.requiresExternalPower = requireCharging ?? true - - // Use 15 minutes from now as earliest begin date - backgroundProcessing.earliestBeginDate = Date(timeIntervalSinceNow: 15 * 60) - - do { - // Submit the task to the scheduler - try BGTaskScheduler.shared.submit(backgroundProcessing) - } catch { - print("Could not schedule the background task \(error.localizedDescription)") - } - } - - // This function runs when the system kicks off the BGAppRefreshTask from the Background Task Scheduler - static func handleBackgroundFetch(task: BGAppRefreshTask) { - // Schedule the next sync task so we can run this again later - scheduleBackgroundFetch() - - // Log the time of last background processing to now - let defaults = UserDefaults.standard - defaults.set(Date().timeIntervalSince1970, forKey: "last_background_fetch_run_time") - - // If we have required charging, we should check the charging status - let requireCharging = defaults.value(forKey: "require_charging") as? Bool ?? false - if (requireCharging) { - UIDevice.current.isBatteryMonitoringEnabled = true - if (UIDevice.current.batteryState == .unplugged) { - // The device is unplugged and we have required charging - // Therefore, we will simply complete the task without - // running it. - task.setTaskCompleted(success: true) - return - } - } - - // If we have required Wi-Fi, we can check the isExpensive property - let requireWifi = defaults.value(forKey: "require_wifi") as? Bool ?? false - if (requireWifi) { - let wifiMonitor = NWPathMonitor(requiredInterfaceType: .wifi) - let isExpensive = wifiMonitor.currentPath.isExpensive - if (isExpensive) { - // The network is expensive and we have required Wi-Fi - // Therefore, we will simply complete the task without - // running it - task.setTaskCompleted(success: true) - return - } - } - - // Schedule the next sync task so we can run this again later - scheduleBackgroundFetch() - - // The background sync task should only run for 20 seconds at most - BackgroundServicePlugin.runBackgroundSync(task, maxSeconds: 20) - } - - // This function runs when the system kicks off the BGProcessingTask from the Background Task Scheduler - static func handleBackgroundProcessing(task: BGProcessingTask) { - // Schedule the next sync task so we run this again later - scheduleBackgroundSync() - - // Log the time of last background processing to now - let defaults = UserDefaults.standard - defaults.set(Date().timeIntervalSince1970, forKey: "last_background_processing_run_time") - - // We won't specify a max time for the background sync service, so this can run for longer - BackgroundServicePlugin.runBackgroundSync(task, maxSeconds: nil) - } - - // This is a synchronous function which uses a semaphore to run the background sync worker's run - // function, which will create a background Isolate and communicate with the Flutter code to back - // up the assets. When it completes, we signal the semaphore and complete the execution allowing the - // control to pass back to the caller synchronously - static func runBackgroundSync(_ task: BGTask, maxSeconds: Int?) { - - let semaphore = DispatchSemaphore(value: 0) - DispatchQueue.main.async { - let backgroundWorker = BackgroundSyncWorker { _ in - semaphore.signal() - } - task.expirationHandler = { - backgroundWorker.cancel() - task.setTaskCompleted(success: true) - } - - backgroundWorker.run(maxSeconds: maxSeconds) - task.setTaskCompleted(success: true) - } - semaphore.wait() - } - - -} diff --git a/mobile/ios/Runner/BackgroundSync/BackgroundSyncWorker.swift b/mobile/ios/Runner/BackgroundSync/BackgroundSyncWorker.swift deleted file mode 100644 index 88d9368308..0000000000 --- a/mobile/ios/Runner/BackgroundSync/BackgroundSyncWorker.swift +++ /dev/null @@ -1,271 +0,0 @@ -// -// BackgroundSyncProcessing.swift -// Runner -// -// Created by Marty Fuhry on 2/6/23. -// -// Credit to https://github.com/fluttercommunity/flutter_workmanager/blob/main/ios/Classes/BackgroundWorker.swift - -import Foundation -import Flutter -import BackgroundTasks - -// The background worker which creates a new Flutter VM, communicates with it -// to run the backup job, and then finishes execution and calls back to its callback -// handler -class BackgroundSyncWorker { - - // The Flutter engine we create for background execution. - // This is not the main Flutter engine which shows the UI, - // this is a brand new isolate created and managed in this code - // here. It does not share memory with the main - // Flutter engine which shows the UI. - // It needs to be started up, registered, and torn down here - let engine: FlutterEngine? = FlutterEngine( - name: "BackgroundImmich" - ) - - let notificationId = "com.alextran.immich/backgroundNotifications" - // The background message passing channel - var channel: FlutterMethodChannel? - - var completionHandler: (UIBackgroundFetchResult) -> Void - let taskSessionStart = Date() - - // We need the completion handler to tell the system when we are done running - init(_ completionHandler: @escaping (UIBackgroundFetchResult) -> Void) { - - // This is the background message passing channel to be used with the background engine - // created here in this platform code - self.channel = FlutterMethodChannel( - name: "immich/backgroundChannel", - binaryMessenger: engine!.binaryMessenger - ) - self.completionHandler = completionHandler - } - - // Handles all of the messages from the Flutter VM called into this platform code - public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { - switch call.method { - case "initialized": - // Initialize tells us that we can now call into the Flutter VM to tell it to begin the update - self.channel?.invokeMethod( - "backgroundProcessing", - arguments: nil, - result: { flutterResult in - - // This is the result we send back to the BGTaskScheduler to let it know whether we'll need more time later or - // if this execution failed - let result: UIBackgroundFetchResult = (flutterResult as? Bool ?? false) ? .newData : .failed - - // Show the task duration - let taskSessionCompleter = Date() - let taskDuration = taskSessionCompleter.timeIntervalSince(self.taskSessionStart) - print("[\(String(describing: self))] \(#function) -> performBackgroundRequest.\(result) (finished in \(taskDuration) seconds)") - - // Complete the execution - self.complete(result) - }) - break - case "updateNotification": - let handled = self.handleNotification(call) - result(handled) - break - case "showError": - let handled = self.handleError(call) - result(handled) - break - case "clearErrorNotifications": - self.handleClearErrorNotifications() - result(true) - break - case "hasContentChanged": - // This is only called for Android, but we provide an implementation here - // telling Flutter that we don't have any information about whether the gallery - // contents have changed or not, so we can just say "no, they've not changed" - result(false) - break - default: - result(FlutterError()) - self.complete(UIBackgroundFetchResult.failed) - } - } - - // Runs the background sync by starting up a new isolate and handling the calls - // until it completes - public func run(maxSeconds: Int?) { - // We need the callback handle to start up the Flutter VM from the entry point - let defaults = UserDefaults.standard - guard let callbackHandle = defaults.value(forKey: "callback_handle") as? Int64 else { - // Can't find the callback handle, this is fatal - complete(UIBackgroundFetchResult.failed) - return - - } - - // Use the provided callbackHandle to get the callback function - guard let callback = FlutterCallbackCache.lookupCallbackInformation(callbackHandle) else { - // We need this callback or else this is fatal - complete(UIBackgroundFetchResult.failed) - return - } - - // Sanity check for the engine existing - if engine == nil { - complete(UIBackgroundFetchResult.failed) - return - } - - // Run the engine - let isRunning = engine!.run( - withEntrypoint: callback.callbackName, - libraryURI: callback.callbackLibraryPath - ) - - // If this engine isn't running, this is fatal - if !isRunning { - complete(UIBackgroundFetchResult.failed) - return - } - - // If we have a timer, we need to start the timer to cancel ourselves - // so that we don't run longer than the provided maxSeconds - // After maxSeconds has elapsed, we will invoke "systemStop" - if maxSeconds != nil { - // Schedule a non-repeating timer to run after maxSeconds - let timer = Timer.scheduledTimer(withTimeInterval: TimeInterval(maxSeconds!), - repeats: false) { timer in - // The callback invalidates the timer and stops execution - timer.invalidate() - - // If the channel is already deallocated, we don't need to do anything - if self.channel == nil { - return - } - - // Tell the Flutter VM to stop backing up now - self.channel?.invokeMethod( - "systemStop", - arguments: nil, - result: nil) - - // Complete the execution - self.complete(UIBackgroundFetchResult.newData) - } - } - - // Set the handle function to the channel message handler - self.channel?.setMethodCallHandler(handle) - - // Register this to get access to the plugins on the platform channel - BackgroundServicePlugin.flutterPluginRegistrantCallback?(engine!) - } - - // Cancels execution of this task, used by the system's task expiration handler - // which is called shortly before execution is about to expire - public func cancel() { - // If the channel is already deallocated, we don't need to do anything - if self.channel == nil { - return - } - - // Tell the Flutter VM to stop backing up now - self.channel?.invokeMethod( - "systemStop", - arguments: nil, - result: nil) - - // Complete the execution - self.complete(UIBackgroundFetchResult.newData) - } - - // Completes the execution, destroys the engine, and sends a completion to our callback completionHandler - private func complete(_ fetchResult: UIBackgroundFetchResult) { - engine?.destroyContext() - channel = nil - completionHandler(fetchResult) - } - - private func handleNotification(_ call: FlutterMethodCall) -> Bool { - - // Parse the arguments as an array list - guard let args = call.arguments as? Array else { - print("Failed to parse \(call.arguments) as array") - return false; - } - - // Requires 7 arguments passed or else fail - guard args.count == 7 else { - print("Needs 7 arguments, but was only passed \(args.count)") - return false - } - - // Parse the arguments to send the notification update - let title = args[0] as? String - let content = args[1] as? String - let progress = args[2] as? Int - let maximum = args[3] as? Int - let indeterminate = args[4] as? Bool - let isDetail = args[5] as? Bool - let onlyIfForeground = args[6] as? Bool - - // Build the notification - let notificationContent = UNMutableNotificationContent() - notificationContent.body = content ?? "Uploading..." - notificationContent.title = title ?? "Immich" - - // Add it to the Notification center - let notification = UNNotificationRequest( - identifier: notificationId, - content: notificationContent, - trigger: nil - ) - let center = UNUserNotificationCenter.current() - center.add(notification) { (error: Error?) in - if let theError = error { - print("Error showing notifications: \(theError)") - } - } - - return true - } - - private func handleError(_ call: FlutterMethodCall) -> Bool { - // Parse the arguments as an array list - guard let args = call.arguments as? Array else { - return false; - } - - // Requires 7 arguments passed or else fail - guard args.count == 3 else { - return false - } - - let title = args[0] as? String - let content = args[1] as? String - let individualTag = args[2] as? String - - // Build the notification - let notificationContent = UNMutableNotificationContent() - notificationContent.body = content ?? "Error running the backup job." - notificationContent.title = title ?? "Immich" - - // Add it to the Notification center - let notification = UNNotificationRequest( - identifier: notificationId, - content: notificationContent, - trigger: nil - ) - let center = UNUserNotificationCenter.current() - center.add(notification) - - return true - } - - private func handleClearErrorNotifications() { - let center = UNUserNotificationCenter.current() - center.removeDeliveredNotifications(withIdentifiers: [notificationId]) - center.removePendingNotificationRequests(withIdentifiers: [notificationId]) - } -} - diff --git a/mobile/ios/Runner/Info.plist b/mobile/ios/Runner/Info.plist index b5d7d780a6..9d194ad665 100644 --- a/mobile/ios/Runner/Info.plist +++ b/mobile/ios/Runner/Info.plist @@ -8,8 +8,6 @@ app.alextran.immich.background.refreshUpload app.alextran.immich.background.processingUpload - app.alextran.immich.backgroundFetch - app.alextran.immich.backgroundProcessing CADisableMinimumFrameDurationOnPhone diff --git a/mobile/lib/constants/constants.dart b/mobile/lib/constants/constants.dart index 9d28941b8f..1748a2a57d 100644 --- a/mobile/lib/constants/constants.dart +++ b/mobile/lib/constants/constants.dart @@ -1,9 +1,5 @@ import 'dart:io'; -const int noDbId = -9223372036854775808; // from Isar -const double downloadCompleted = -1; -const double downloadFailed = -2; - const String kMobileMetadataKey = "mobile-app"; // Number of log entries to retain on app start @@ -47,9 +43,6 @@ const List<(String, String)> kWidgetNames = [ ('com.immich.widget.memory', 'app.alextran.immich.widget.MemoryReceiver'), ]; -const double kUploadStatusFailed = -1.0; -const double kUploadStatusCanceled = -2.0; - const int kMinMonthsToEnableScrubberSnap = 12; const String kImmichAppStoreLink = "https://apps.apple.com/app/immich/id1613945652"; diff --git a/mobile/lib/constants/enums.dart b/mobile/lib/constants/enums.dart index 32ef9bbbed..877145c322 100644 --- a/mobile/lib/constants/enums.dart +++ b/mobile/lib/constants/enums.dart @@ -11,8 +11,6 @@ enum TextSearchType { context, filename, description, ocr } enum AssetVisibilityEnum { timeline, hidden, archive, locked } -enum SortUserBy { id } - enum ActionSource { timeline, viewer } enum CleanupStep { selectDate, scan, delete } diff --git a/mobile/lib/domain/interfaces/db.interface.dart b/mobile/lib/domain/interfaces/db.interface.dart deleted file mode 100644 index 5645d15c47..0000000000 --- a/mobile/lib/domain/interfaces/db.interface.dart +++ /dev/null @@ -1,3 +0,0 @@ -abstract interface class IDatabaseRepository { - Future transaction(Future Function() callback); -} diff --git a/mobile/lib/domain/models/device_asset.model.dart b/mobile/lib/domain/models/device_asset.model.dart deleted file mode 100644 index a404f5a9e2..0000000000 --- a/mobile/lib/domain/models/device_asset.model.dart +++ /dev/null @@ -1,34 +0,0 @@ -import 'dart:typed_data'; - -class DeviceAsset { - final String assetId; - final Uint8List hash; - final DateTime modifiedTime; - - const DeviceAsset({required this.assetId, required this.hash, required this.modifiedTime}); - - @override - bool operator ==(covariant DeviceAsset other) { - if (identical(this, other)) return true; - - return other.assetId == assetId && other.hash == hash && other.modifiedTime == modifiedTime; - } - - @override - int get hashCode { - return assetId.hashCode ^ hash.hashCode ^ modifiedTime.hashCode; - } - - @override - String toString() { - return 'DeviceAsset(assetId: $assetId, hash: $hash, modifiedTime: $modifiedTime)'; - } - - DeviceAsset copyWith({String? assetId, Uint8List? hash, DateTime? modifiedTime}) { - return DeviceAsset( - assetId: assetId ?? this.assetId, - hash: hash ?? this.hash, - modifiedTime: modifiedTime ?? this.modifiedTime, - ); - } -} diff --git a/mobile/lib/domain/services/asset.service.dart b/mobile/lib/domain/services/asset.service.dart index 198733b3c8..7fa8c13fd8 100644 --- a/mobile/lib/domain/services/asset.service.dart +++ b/mobile/lib/domain/services/asset.service.dart @@ -1,12 +1,9 @@ import 'package:immich_mobile/domain/models/album/local_album.model.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/domain/models/exif.model.dart'; -import 'package:immich_mobile/extensions/platform_extensions.dart'; import 'package:immich_mobile/infrastructure/repositories/local_asset.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/remote_asset.repository.dart'; -typedef _AssetVideoDimension = ({double? width, double? height, bool isFlipped}); - class AssetService { final RemoteAssetRepository _remoteAssetRepository; final DriftLocalAssetRepository _localAssetRepository; @@ -58,49 +55,6 @@ class AssetService { return _remoteAssetRepository.getExif(id); } - Future getAspectRatio(BaseAsset asset) async { - final dimension = asset is LocalAsset - ? await _getLocalAssetDimensions(asset) - : await _getRemoteAssetDimensions(asset as RemoteAsset); - - if (dimension.width == null || dimension.height == null || dimension.height == 0) { - return 1.0; - } - - return dimension.isFlipped ? dimension.height! / dimension.width! : dimension.width! / dimension.height!; - } - - Future<_AssetVideoDimension> _getLocalAssetDimensions(LocalAsset asset) async { - double? width = asset.width?.toDouble(); - double? height = asset.height?.toDouble(); - int orientation = asset.orientation; - - if (width == null || height == null) { - final fetched = await _localAssetRepository.get(asset.id); - width = fetched?.width?.toDouble(); - height = fetched?.height?.toDouble(); - orientation = fetched?.orientation ?? 0; - } - - // On Android, local assets need orientation correction for 90°/270° rotations - // On iOS, the Photos framework pre-corrects dimensions - final isFlipped = CurrentPlatform.isAndroid && (orientation == 90 || orientation == 270); - return (width: width, height: height, isFlipped: isFlipped); - } - - Future<_AssetVideoDimension> _getRemoteAssetDimensions(RemoteAsset asset) async { - double? width = asset.width?.toDouble(); - double? height = asset.height?.toDouble(); - - if (width == null || height == null) { - final fetched = await _remoteAssetRepository.get(asset.id); - width = fetched?.width?.toDouble(); - height = fetched?.height?.toDouble(); - } - - return (width: width, height: height, isFlipped: false); - } - Future> getPlaces(String userId) { return _remoteAssetRepository.getPlaces(userId); } diff --git a/mobile/lib/domain/services/background_worker.service.dart b/mobile/lib/domain/services/background_worker.service.dart index 93a2a14127..d4da3e31a4 100644 --- a/mobile/lib/domain/services/background_worker.service.dart +++ b/mobile/lib/domain/services/background_worker.service.dart @@ -16,19 +16,16 @@ import 'package:immich_mobile/platform/background_worker_lock_api.g.dart'; import 'package:immich_mobile/providers/app_settings.provider.dart'; import 'package:immich_mobile/providers/background_sync.provider.dart'; import 'package:immich_mobile/providers/backup/drift_backup.provider.dart'; -import 'package:immich_mobile/providers/db.provider.dart'; import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; import 'package:immich_mobile/providers/infrastructure/platform.provider.dart' show nativeSyncApiProvider; import 'package:immich_mobile/providers/user.provider.dart'; -import 'package:immich_mobile/repositories/file_media.repository.dart'; import 'package:immich_mobile/services/app_settings.service.dart'; import 'package:immich_mobile/services/auth.service.dart'; -import 'package:immich_mobile/services/localization.service.dart'; import 'package:immich_mobile/services/foreground_upload.service.dart'; +import 'package:immich_mobile/services/localization.service.dart'; import 'package:immich_mobile/utils/bootstrap.dart'; import 'package:immich_mobile/utils/debug_print.dart'; import 'package:immich_mobile/wm_executor.dart'; -import 'package:isar/isar.dart'; import 'package:logging/logging.dart'; class BackgroundWorkerFgService { @@ -58,7 +55,6 @@ class BackgroundWorkerFgService { class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi { ProviderContainer? _ref; - final Isar _isar; final Drift _drift; final DriftLogger _driftLogger; final BackgroundWorkerBgHostApi _backgroundHostApi; @@ -67,18 +63,11 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi { bool _isCleanedUp = false; - BackgroundWorkerBgService({required Isar isar, required Drift drift, required DriftLogger driftLogger}) - : _isar = isar, - _drift = drift, + BackgroundWorkerBgService({required Drift drift, required DriftLogger driftLogger}) + : _drift = drift, _driftLogger = driftLogger, _backgroundHostApi = BackgroundWorkerBgHostApi() { - _ref = ProviderContainer( - overrides: [ - dbProvider.overrideWithValue(isar), - isarProvider.overrideWithValue(isar), - driftProvider.overrideWith(driftOverride(drift)), - ], - ); + _ref = ProviderContainer(overrides: [driftProvider.overrideWith(driftOverride(drift))]); BackgroundWorkerFlutterApi.setUp(this); } @@ -102,7 +91,6 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi { ), FileDownloader().trackTasksInGroup(kDownloadGroupLivePhoto, markDownloadedComplete: false), FileDownloader().trackTasks(), - _ref?.read(fileMediaRepositoryProvider).enableBackgroundAccess(), ].nonNulls, ); @@ -209,9 +197,6 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi { backgroundSyncManager?.cancel(), ]; - if (_isar.isOpen) { - cleanupFutures.add(_isar.close()); - } await Future.wait(cleanupFutures.nonNulls); _logger.info("Background worker resources cleaned up"); } catch (error, stack) { @@ -301,7 +286,6 @@ Future backgroundSyncNativeEntrypoint() async { WidgetsFlutterBinding.ensureInitialized(); DartPluginRegistrant.ensureInitialized(); - final (isar, drift, logDB) = await Bootstrap.initDB(); - await Bootstrap.initDomain(isar, drift, logDB, shouldBufferLogs: false, listenStoreUpdates: false); - await BackgroundWorkerBgService(isar: isar, drift: drift, driftLogger: logDB).init(); + final (drift, logDB) = await Bootstrap.initDomain(shouldBufferLogs: false, listenStoreUpdates: false); + await BackgroundWorkerBgService(drift: drift, driftLogger: logDB).init(); } diff --git a/mobile/lib/domain/services/log.service.dart b/mobile/lib/domain/services/log.service.dart index 64010b9220..b58ee89535 100644 --- a/mobile/lib/domain/services/log.service.dart +++ b/mobile/lib/domain/services/log.service.dart @@ -15,7 +15,7 @@ import 'package:logging/logging.dart'; /// via [IStoreRepository] class LogService { final LogRepository _logRepository; - final IStoreRepository _storeRepository; + final DriftStoreRepository _storeRepository; final List _msgBuffer = []; @@ -38,7 +38,7 @@ class LogService { static Future init({ required LogRepository logRepository, - required IStoreRepository storeRepository, + required DriftStoreRepository storeRepository, bool shouldBuffer = true, }) async { _instance ??= await create( @@ -51,7 +51,7 @@ class LogService { static Future create({ required LogRepository logRepository, - required IStoreRepository storeRepository, + required DriftStoreRepository storeRepository, bool shouldBuffer = true, }) async { final instance = LogService._(logRepository, storeRepository, shouldBuffer); diff --git a/mobile/lib/domain/services/store.service.dart b/mobile/lib/domain/services/store.service.dart index 0098c3d262..b325ffd631 100644 --- a/mobile/lib/domain/services/store.service.dart +++ b/mobile/lib/domain/services/store.service.dart @@ -6,13 +6,13 @@ import 'package:immich_mobile/infrastructure/repositories/store.repository.dart' /// Provides access to a persistent key-value store with an in-memory cache. /// Listens for repository changes to keep the cache updated. class StoreService { - final IStoreRepository _storeRepository; + final DriftStoreRepository _storeRepository; /// In-memory cache. Keys are [StoreKey.id] final Map _cache = {}; StreamSubscription>? _storeUpdateSubscription; - StoreService._({required IStoreRepository isarStoreRepository}) : _storeRepository = isarStoreRepository; + StoreService._({required DriftStoreRepository isarStoreRepository}) : _storeRepository = isarStoreRepository; // TODO: Temporary typedef to make minimal changes. Remove this and make the presentation layer access store through a provider static StoreService? _instance; @@ -24,12 +24,12 @@ class StoreService { } // TODO: Replace the implementation with the one from create after removing the typedef - static Future init({required IStoreRepository storeRepository, bool listenUpdates = true}) async { + static Future init({required DriftStoreRepository storeRepository, bool listenUpdates = true}) async { _instance ??= await create(storeRepository: storeRepository, listenUpdates: listenUpdates); return _instance!; } - static Future create({required IStoreRepository storeRepository, bool listenUpdates = true}) async { + static Future create({required DriftStoreRepository storeRepository, bool listenUpdates = true}) async { final instance = StoreService._(isarStoreRepository: storeRepository); await instance.populateCache(); if (listenUpdates) { @@ -91,8 +91,6 @@ class StoreService { await _storeRepository.deleteAll(); _cache.clear(); } - - bool get isBetaTimelineEnabled => tryGet(StoreKey.betaTimeline) ?? true; } class StoreKeyNotFoundException implements Exception { diff --git a/mobile/lib/domain/services/user.service.dart b/mobile/lib/domain/services/user.service.dart index d347d8aa4f..1f9c015ad7 100644 --- a/mobile/lib/domain/services/user.service.dart +++ b/mobile/lib/domain/services/user.service.dart @@ -4,23 +4,17 @@ import 'dart:typed_data'; import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/domain/models/user.model.dart'; import 'package:immich_mobile/domain/services/store.service.dart'; -import 'package:immich_mobile/infrastructure/repositories/user.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/user_api.repository.dart'; import 'package:logging/logging.dart'; class UserService { final Logger _log = Logger("UserService"); - final IsarUserRepository _isarUserRepository; final UserApiRepository _userApiRepository; final StoreService _storeService; - UserService({ - required IsarUserRepository isarUserRepository, - required UserApiRepository userApiRepository, - required StoreService storeService, - }) : _isarUserRepository = isarUserRepository, - _userApiRepository = userApiRepository, - _storeService = storeService; + UserService({required UserApiRepository userApiRepository, required StoreService storeService}) + : _userApiRepository = userApiRepository, + _storeService = storeService; UserDto getMyUser() { return _storeService.get(StoreKey.currentUser); @@ -38,7 +32,6 @@ class UserService { final user = await _userApiRepository.getMyUser(); if (user == null) return null; await _storeService.put(StoreKey.currentUser, user); - await _isarUserRepository.update(user); return user; } @@ -47,19 +40,10 @@ class UserService { final path = await _userApiRepository.createProfileImage(name: name, data: image); final updatedUser = getMyUser(); await _storeService.put(StoreKey.currentUser, updatedUser); - await _isarUserRepository.update(updatedUser); return path; } catch (e) { _log.warning("Failed to upload profile image", e); return null; } } - - Future> getAll() async { - return await _isarUserRepository.getAll(); - } - - Future deleteAll() { - return _isarUserRepository.deleteAll(); - } } diff --git a/mobile/lib/entities/README.md b/mobile/lib/entities/README.md deleted file mode 100644 index c2ad4876e3..0000000000 --- a/mobile/lib/entities/README.md +++ /dev/null @@ -1 +0,0 @@ -This directory contains entity that is stored in the local storage. \ No newline at end of file diff --git a/mobile/lib/entities/album.entity.dart b/mobile/lib/entities/album.entity.dart deleted file mode 100644 index 2ca0d50dcc..0000000000 --- a/mobile/lib/entities/album.entity.dart +++ /dev/null @@ -1,192 +0,0 @@ -import 'package:flutter/foundation.dart'; -import 'package:immich_mobile/constants/enums.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/infrastructure/entities/user.entity.dart'; -import 'package:immich_mobile/utils/datetime_comparison.dart'; -import 'package:isar/isar.dart'; -// ignore: implementation_imports -import 'package:isar/src/common/isar_links_common.dart'; -import 'package:openapi/api.dart'; - -part 'album.entity.g.dart'; - -@Collection(inheritance: false) -class Album { - @protected - Album({ - this.remoteId, - this.localId, - required this.name, - required this.createdAt, - required this.modifiedAt, - this.description, - this.startDate, - this.endDate, - this.lastModifiedAssetTimestamp, - required this.shared, - required this.activityEnabled, - this.sortOrder = SortOrder.desc, - }); - - // fields stored in DB - Id id = Isar.autoIncrement; - @Index(unique: false, replace: false, type: IndexType.hash) - String? remoteId; - @Index(unique: false, replace: false, type: IndexType.hash) - String? localId; - String name; - String? description; - DateTime createdAt; - DateTime modifiedAt; - DateTime? startDate; - DateTime? endDate; - DateTime? lastModifiedAssetTimestamp; - bool shared; - bool activityEnabled; - @enumerated - SortOrder sortOrder; - final IsarLink owner = IsarLink(); - final IsarLink thumbnail = IsarLink(); - final IsarLinks sharedUsers = IsarLinks(); - final IsarLinks assets = IsarLinks(); - - // transient fields - @ignore - bool isAll = false; - - @ignore - String? remoteThumbnailAssetId; - - @ignore - int remoteAssetCount = 0; - - // getters - @ignore - bool get isRemote => remoteId != null; - - @ignore - bool get isLocal => localId != null; - - @ignore - int get assetCount => assets.length; - - @ignore - String? get ownerId => owner.value?.id; - - @ignore - String? get ownerName { - // Guard null owner - if (owner.value == null) { - return null; - } - - final name = []; - if (owner.value?.name != null) { - name.add(owner.value!.name); - } - - return name.join(' '); - } - - @ignore - String get eTagKeyAssetCount => "device-album-$localId-asset-count"; - - // the following getter are needed because Isar links do not make data - // accessible in an object freshly created (not loaded from DB) - - @ignore - Iterable get remoteUsers => - sharedUsers.isEmpty ? (sharedUsers as IsarLinksCommon).addedObjects : sharedUsers; - - @ignore - Iterable get remoteAssets => assets.isEmpty ? (assets as IsarLinksCommon).addedObjects : assets; - - @override - bool operator ==(other) { - if (other is! Album) return false; - return id == other.id && - remoteId == other.remoteId && - localId == other.localId && - name == other.name && - description == other.description && - createdAt.isAtSameMomentAs(other.createdAt) && - modifiedAt.isAtSameMomentAs(other.modifiedAt) && - isAtSameMomentAs(startDate, other.startDate) && - isAtSameMomentAs(endDate, other.endDate) && - isAtSameMomentAs(lastModifiedAssetTimestamp, other.lastModifiedAssetTimestamp) && - shared == other.shared && - activityEnabled == other.activityEnabled && - owner.value == other.owner.value && - thumbnail.value == other.thumbnail.value && - sharedUsers.length == other.sharedUsers.length && - assets.length == other.assets.length; - } - - @override - @ignore - int get hashCode => - id.hashCode ^ - remoteId.hashCode ^ - localId.hashCode ^ - name.hashCode ^ - createdAt.hashCode ^ - modifiedAt.hashCode ^ - startDate.hashCode ^ - endDate.hashCode ^ - description.hashCode ^ - lastModifiedAssetTimestamp.hashCode ^ - shared.hashCode ^ - activityEnabled.hashCode ^ - owner.value.hashCode ^ - thumbnail.value.hashCode ^ - sharedUsers.length.hashCode ^ - assets.length.hashCode; - - static Future remote(AlbumResponseDto dto) async { - final Isar db = Isar.getInstance()!; - final Album a = Album( - remoteId: dto.id, - name: dto.albumName, - createdAt: dto.createdAt, - modifiedAt: dto.updatedAt, - description: dto.description, - lastModifiedAssetTimestamp: dto.lastModifiedAssetTimestamp, - shared: dto.shared, - startDate: dto.startDate, - endDate: dto.endDate, - activityEnabled: dto.isActivityEnabled, - ); - a.remoteAssetCount = dto.assetCount; - a.owner.value = await db.users.getById(dto.ownerId); - if (dto.order != null) { - a.sortOrder = dto.order == AssetOrder.asc ? SortOrder.asc : SortOrder.desc; - } - - if (dto.albumThumbnailAssetId != null) { - a.thumbnail.value = await db.assets.where().remoteIdEqualTo(dto.albumThumbnailAssetId).findFirst(); - } - if (dto.albumUsers.isNotEmpty) { - final users = await db.users.getAllById(dto.albumUsers.map((e) => e.user.id).toList(growable: false)); - a.sharedUsers.addAll(users.cast()); - } - if (dto.assets.isNotEmpty) { - final assets = await db.assets.getAllByRemoteId(dto.assets.map((e) => e.id)); - a.assets.addAll(assets); - } - return a; - } - - @override - String toString() => 'remoteId: $remoteId name: $name description: $description'; -} - -extension AssetsHelper on IsarCollection { - Future store(Album a) async { - await put(a); - await a.owner.save(); - await a.thumbnail.save(); - await a.sharedUsers.save(); - await a.assets.save(); - return a; - } -} diff --git a/mobile/lib/entities/album.entity.g.dart b/mobile/lib/entities/album.entity.g.dart deleted file mode 100644 index ecbbab48c2..0000000000 --- a/mobile/lib/entities/album.entity.g.dart +++ /dev/null @@ -1,2240 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'album.entity.dart'; - -// ************************************************************************** -// IsarCollectionGenerator -// ************************************************************************** - -// coverage:ignore-file -// ignore_for_file: duplicate_ignore, non_constant_identifier_names, constant_identifier_names, invalid_use_of_protected_member, unnecessary_cast, prefer_const_constructors, lines_longer_than_80_chars, require_trailing_commas, inference_failure_on_function_invocation, unnecessary_parenthesis, unnecessary_raw_strings, unnecessary_null_checks, join_return_with_assignment, prefer_final_locals, avoid_js_rounded_ints, avoid_positional_boolean_parameters, always_specify_types - -extension GetAlbumCollection on Isar { - IsarCollection get albums => this.collection(); -} - -const AlbumSchema = CollectionSchema( - name: r'Album', - id: -1355968412107120937, - properties: { - r'activityEnabled': PropertySchema( - id: 0, - name: r'activityEnabled', - type: IsarType.bool, - ), - r'createdAt': PropertySchema( - id: 1, - name: r'createdAt', - type: IsarType.dateTime, - ), - r'description': PropertySchema( - id: 2, - name: r'description', - type: IsarType.string, - ), - r'endDate': PropertySchema( - id: 3, - name: r'endDate', - type: IsarType.dateTime, - ), - r'lastModifiedAssetTimestamp': PropertySchema( - id: 4, - name: r'lastModifiedAssetTimestamp', - type: IsarType.dateTime, - ), - r'localId': PropertySchema(id: 5, name: r'localId', type: IsarType.string), - r'modifiedAt': PropertySchema( - id: 6, - name: r'modifiedAt', - type: IsarType.dateTime, - ), - r'name': PropertySchema(id: 7, name: r'name', type: IsarType.string), - r'remoteId': PropertySchema( - id: 8, - name: r'remoteId', - type: IsarType.string, - ), - r'shared': PropertySchema(id: 9, name: r'shared', type: IsarType.bool), - r'sortOrder': PropertySchema( - id: 10, - name: r'sortOrder', - type: IsarType.byte, - enumMap: _AlbumsortOrderEnumValueMap, - ), - r'startDate': PropertySchema( - id: 11, - name: r'startDate', - type: IsarType.dateTime, - ), - }, - - estimateSize: _albumEstimateSize, - serialize: _albumSerialize, - deserialize: _albumDeserialize, - deserializeProp: _albumDeserializeProp, - idName: r'id', - indexes: { - r'remoteId': IndexSchema( - id: 6301175856541681032, - name: r'remoteId', - unique: false, - replace: false, - properties: [ - IndexPropertySchema( - name: r'remoteId', - type: IndexType.hash, - caseSensitive: true, - ), - ], - ), - r'localId': IndexSchema( - id: 1199848425898359622, - name: r'localId', - unique: false, - replace: false, - properties: [ - IndexPropertySchema( - name: r'localId', - type: IndexType.hash, - caseSensitive: true, - ), - ], - ), - }, - links: { - r'owner': LinkSchema( - id: 8272576585804958029, - name: r'owner', - target: r'User', - single: true, - ), - r'thumbnail': LinkSchema( - id: 4055421409629988258, - name: r'thumbnail', - target: r'Asset', - single: true, - ), - r'sharedUsers': LinkSchema( - id: 8972835302564625434, - name: r'sharedUsers', - target: r'User', - single: false, - ), - r'assets': LinkSchema( - id: 1059358332698388152, - name: r'assets', - target: r'Asset', - single: false, - ), - }, - embeddedSchemas: {}, - - getId: _albumGetId, - getLinks: _albumGetLinks, - attach: _albumAttach, - version: '3.3.0-dev.3', -); - -int _albumEstimateSize( - Album object, - List offsets, - Map> allOffsets, -) { - var bytesCount = offsets.last; - { - final value = object.description; - if (value != null) { - bytesCount += 3 + value.length * 3; - } - } - { - final value = object.localId; - if (value != null) { - bytesCount += 3 + value.length * 3; - } - } - bytesCount += 3 + object.name.length * 3; - { - final value = object.remoteId; - if (value != null) { - bytesCount += 3 + value.length * 3; - } - } - return bytesCount; -} - -void _albumSerialize( - Album object, - IsarWriter writer, - List offsets, - Map> allOffsets, -) { - writer.writeBool(offsets[0], object.activityEnabled); - writer.writeDateTime(offsets[1], object.createdAt); - writer.writeString(offsets[2], object.description); - writer.writeDateTime(offsets[3], object.endDate); - writer.writeDateTime(offsets[4], object.lastModifiedAssetTimestamp); - writer.writeString(offsets[5], object.localId); - writer.writeDateTime(offsets[6], object.modifiedAt); - writer.writeString(offsets[7], object.name); - writer.writeString(offsets[8], object.remoteId); - writer.writeBool(offsets[9], object.shared); - writer.writeByte(offsets[10], object.sortOrder.index); - writer.writeDateTime(offsets[11], object.startDate); -} - -Album _albumDeserialize( - Id id, - IsarReader reader, - List offsets, - Map> allOffsets, -) { - final object = Album( - activityEnabled: reader.readBool(offsets[0]), - createdAt: reader.readDateTime(offsets[1]), - description: reader.readStringOrNull(offsets[2]), - endDate: reader.readDateTimeOrNull(offsets[3]), - lastModifiedAssetTimestamp: reader.readDateTimeOrNull(offsets[4]), - localId: reader.readStringOrNull(offsets[5]), - modifiedAt: reader.readDateTime(offsets[6]), - name: reader.readString(offsets[7]), - remoteId: reader.readStringOrNull(offsets[8]), - shared: reader.readBool(offsets[9]), - sortOrder: - _AlbumsortOrderValueEnumMap[reader.readByteOrNull(offsets[10])] ?? - SortOrder.desc, - startDate: reader.readDateTimeOrNull(offsets[11]), - ); - object.id = id; - return object; -} - -P _albumDeserializeProp

( - IsarReader reader, - int propertyId, - int offset, - Map> allOffsets, -) { - switch (propertyId) { - case 0: - return (reader.readBool(offset)) as P; - case 1: - return (reader.readDateTime(offset)) as P; - case 2: - return (reader.readStringOrNull(offset)) as P; - case 3: - return (reader.readDateTimeOrNull(offset)) as P; - case 4: - return (reader.readDateTimeOrNull(offset)) as P; - case 5: - return (reader.readStringOrNull(offset)) as P; - case 6: - return (reader.readDateTime(offset)) as P; - case 7: - return (reader.readString(offset)) as P; - case 8: - return (reader.readStringOrNull(offset)) as P; - case 9: - return (reader.readBool(offset)) as P; - case 10: - return (_AlbumsortOrderValueEnumMap[reader.readByteOrNull(offset)] ?? - SortOrder.desc) - as P; - case 11: - return (reader.readDateTimeOrNull(offset)) as P; - default: - throw IsarError('Unknown property with id $propertyId'); - } -} - -const _AlbumsortOrderEnumValueMap = {'asc': 0, 'desc': 1}; -const _AlbumsortOrderValueEnumMap = {0: SortOrder.asc, 1: SortOrder.desc}; - -Id _albumGetId(Album object) { - return object.id; -} - -List> _albumGetLinks(Album object) { - return [object.owner, object.thumbnail, object.sharedUsers, object.assets]; -} - -void _albumAttach(IsarCollection col, Id id, Album object) { - object.id = id; - object.owner.attach(col, col.isar.collection(), r'owner', id); - object.thumbnail.attach(col, col.isar.collection(), r'thumbnail', id); - object.sharedUsers.attach( - col, - col.isar.collection(), - r'sharedUsers', - id, - ); - object.assets.attach(col, col.isar.collection(), r'assets', id); -} - -extension AlbumQueryWhereSort on QueryBuilder { - QueryBuilder anyId() { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause(const IdWhereClause.any()); - }); - } -} - -extension AlbumQueryWhere on QueryBuilder { - QueryBuilder idEqualTo(Id id) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause(IdWhereClause.between(lower: id, upper: id)); - }); - } - - QueryBuilder idNotEqualTo(Id id) { - return QueryBuilder.apply(this, (query) { - if (query.whereSort == Sort.asc) { - return query - .addWhereClause( - IdWhereClause.lessThan(upper: id, includeUpper: false), - ) - .addWhereClause( - IdWhereClause.greaterThan(lower: id, includeLower: false), - ); - } else { - return query - .addWhereClause( - IdWhereClause.greaterThan(lower: id, includeLower: false), - ) - .addWhereClause( - IdWhereClause.lessThan(upper: id, includeUpper: false), - ); - } - }); - } - - QueryBuilder idGreaterThan( - Id id, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.greaterThan(lower: id, includeLower: include), - ); - }); - } - - QueryBuilder idLessThan( - Id id, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.lessThan(upper: id, includeUpper: include), - ); - }); - } - - QueryBuilder idBetween( - Id lowerId, - Id upperId, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.between( - lower: lowerId, - includeLower: includeLower, - upper: upperId, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder remoteIdIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IndexWhereClause.equalTo(indexName: r'remoteId', value: [null]), - ); - }); - } - - QueryBuilder remoteIdIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IndexWhereClause.between( - indexName: r'remoteId', - lower: [null], - includeLower: false, - upper: [], - ), - ); - }); - } - - QueryBuilder remoteIdEqualTo( - String? remoteId, - ) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IndexWhereClause.equalTo(indexName: r'remoteId', value: [remoteId]), - ); - }); - } - - QueryBuilder remoteIdNotEqualTo( - String? remoteId, - ) { - return QueryBuilder.apply(this, (query) { - if (query.whereSort == Sort.asc) { - return query - .addWhereClause( - IndexWhereClause.between( - indexName: r'remoteId', - lower: [], - upper: [remoteId], - includeUpper: false, - ), - ) - .addWhereClause( - IndexWhereClause.between( - indexName: r'remoteId', - lower: [remoteId], - includeLower: false, - upper: [], - ), - ); - } else { - return query - .addWhereClause( - IndexWhereClause.between( - indexName: r'remoteId', - lower: [remoteId], - includeLower: false, - upper: [], - ), - ) - .addWhereClause( - IndexWhereClause.between( - indexName: r'remoteId', - lower: [], - upper: [remoteId], - includeUpper: false, - ), - ); - } - }); - } - - QueryBuilder localIdIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IndexWhereClause.equalTo(indexName: r'localId', value: [null]), - ); - }); - } - - QueryBuilder localIdIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IndexWhereClause.between( - indexName: r'localId', - lower: [null], - includeLower: false, - upper: [], - ), - ); - }); - } - - QueryBuilder localIdEqualTo( - String? localId, - ) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IndexWhereClause.equalTo(indexName: r'localId', value: [localId]), - ); - }); - } - - QueryBuilder localIdNotEqualTo( - String? localId, - ) { - return QueryBuilder.apply(this, (query) { - if (query.whereSort == Sort.asc) { - return query - .addWhereClause( - IndexWhereClause.between( - indexName: r'localId', - lower: [], - upper: [localId], - includeUpper: false, - ), - ) - .addWhereClause( - IndexWhereClause.between( - indexName: r'localId', - lower: [localId], - includeLower: false, - upper: [], - ), - ); - } else { - return query - .addWhereClause( - IndexWhereClause.between( - indexName: r'localId', - lower: [localId], - includeLower: false, - upper: [], - ), - ) - .addWhereClause( - IndexWhereClause.between( - indexName: r'localId', - lower: [], - upper: [localId], - includeUpper: false, - ), - ); - } - }); - } -} - -extension AlbumQueryFilter on QueryBuilder { - QueryBuilder activityEnabledEqualTo( - bool value, - ) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'activityEnabled', value: value), - ); - }); - } - - QueryBuilder createdAtEqualTo( - DateTime value, - ) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'createdAt', value: value), - ); - }); - } - - QueryBuilder createdAtGreaterThan( - DateTime value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'createdAt', - value: value, - ), - ); - }); - } - - QueryBuilder createdAtLessThan( - DateTime value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'createdAt', - value: value, - ), - ); - }); - } - - QueryBuilder createdAtBetween( - DateTime lower, - DateTime upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'createdAt', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder descriptionIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNull(property: r'description'), - ); - }); - } - - QueryBuilder descriptionIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNotNull(property: r'description'), - ); - }); - } - - QueryBuilder descriptionEqualTo( - String? value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'description', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder descriptionGreaterThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'description', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder descriptionLessThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'description', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder descriptionBetween( - String? lower, - String? upper, { - bool includeLower = true, - bool includeUpper = true, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'description', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder descriptionStartsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.startsWith( - property: r'description', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder descriptionEndsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.endsWith( - property: r'description', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder descriptionContains( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.contains( - property: r'description', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder descriptionMatches( - String pattern, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.matches( - property: r'description', - wildcard: pattern, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder descriptionIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'description', value: ''), - ); - }); - } - - QueryBuilder descriptionIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan(property: r'description', value: ''), - ); - }); - } - - QueryBuilder endDateIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNull(property: r'endDate'), - ); - }); - } - - QueryBuilder endDateIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNotNull(property: r'endDate'), - ); - }); - } - - QueryBuilder endDateEqualTo( - DateTime? value, - ) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'endDate', value: value), - ); - }); - } - - QueryBuilder endDateGreaterThan( - DateTime? value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'endDate', - value: value, - ), - ); - }); - } - - QueryBuilder endDateLessThan( - DateTime? value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'endDate', - value: value, - ), - ); - }); - } - - QueryBuilder endDateBetween( - DateTime? lower, - DateTime? upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'endDate', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder idEqualTo(Id value) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'id', value: value), - ); - }); - } - - QueryBuilder idGreaterThan( - Id value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'id', - value: value, - ), - ); - }); - } - - QueryBuilder idLessThan( - Id value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'id', - value: value, - ), - ); - }); - } - - QueryBuilder idBetween( - Id lower, - Id upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'id', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder - lastModifiedAssetTimestampIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNull(property: r'lastModifiedAssetTimestamp'), - ); - }); - } - - QueryBuilder - lastModifiedAssetTimestampIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNotNull( - property: r'lastModifiedAssetTimestamp', - ), - ); - }); - } - - QueryBuilder - lastModifiedAssetTimestampEqualTo(DateTime? value) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'lastModifiedAssetTimestamp', - value: value, - ), - ); - }); - } - - QueryBuilder - lastModifiedAssetTimestampGreaterThan( - DateTime? value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'lastModifiedAssetTimestamp', - value: value, - ), - ); - }); - } - - QueryBuilder - lastModifiedAssetTimestampLessThan(DateTime? value, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'lastModifiedAssetTimestamp', - value: value, - ), - ); - }); - } - - QueryBuilder - lastModifiedAssetTimestampBetween( - DateTime? lower, - DateTime? upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'lastModifiedAssetTimestamp', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder localIdIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNull(property: r'localId'), - ); - }); - } - - QueryBuilder localIdIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNotNull(property: r'localId'), - ); - }); - } - - QueryBuilder localIdEqualTo( - String? value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'localId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder localIdGreaterThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'localId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder localIdLessThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'localId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder localIdBetween( - String? lower, - String? upper, { - bool includeLower = true, - bool includeUpper = true, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'localId', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder localIdStartsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.startsWith( - property: r'localId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder localIdEndsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.endsWith( - property: r'localId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder localIdContains( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.contains( - property: r'localId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder localIdMatches( - String pattern, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.matches( - property: r'localId', - wildcard: pattern, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder localIdIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'localId', value: ''), - ); - }); - } - - QueryBuilder localIdIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan(property: r'localId', value: ''), - ); - }); - } - - QueryBuilder modifiedAtEqualTo( - DateTime value, - ) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'modifiedAt', value: value), - ); - }); - } - - QueryBuilder modifiedAtGreaterThan( - DateTime value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'modifiedAt', - value: value, - ), - ); - }); - } - - QueryBuilder modifiedAtLessThan( - DateTime value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'modifiedAt', - value: value, - ), - ); - }); - } - - QueryBuilder modifiedAtBetween( - DateTime lower, - DateTime upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'modifiedAt', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder nameEqualTo( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'name', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder nameGreaterThan( - String value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'name', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder nameLessThan( - String value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'name', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder nameBetween( - String lower, - String upper, { - bool includeLower = true, - bool includeUpper = true, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'name', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder nameStartsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.startsWith( - property: r'name', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder nameEndsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.endsWith( - property: r'name', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder nameContains( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.contains( - property: r'name', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder nameMatches( - String pattern, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.matches( - property: r'name', - wildcard: pattern, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder nameIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'name', value: ''), - ); - }); - } - - QueryBuilder nameIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan(property: r'name', value: ''), - ); - }); - } - - QueryBuilder remoteIdIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNull(property: r'remoteId'), - ); - }); - } - - QueryBuilder remoteIdIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNotNull(property: r'remoteId'), - ); - }); - } - - QueryBuilder remoteIdEqualTo( - String? value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'remoteId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder remoteIdGreaterThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'remoteId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder remoteIdLessThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'remoteId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder remoteIdBetween( - String? lower, - String? upper, { - bool includeLower = true, - bool includeUpper = true, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'remoteId', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder remoteIdStartsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.startsWith( - property: r'remoteId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder remoteIdEndsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.endsWith( - property: r'remoteId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder remoteIdContains( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.contains( - property: r'remoteId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder remoteIdMatches( - String pattern, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.matches( - property: r'remoteId', - wildcard: pattern, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder remoteIdIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'remoteId', value: ''), - ); - }); - } - - QueryBuilder remoteIdIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan(property: r'remoteId', value: ''), - ); - }); - } - - QueryBuilder sharedEqualTo(bool value) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'shared', value: value), - ); - }); - } - - QueryBuilder sortOrderEqualTo( - SortOrder value, - ) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'sortOrder', value: value), - ); - }); - } - - QueryBuilder sortOrderGreaterThan( - SortOrder value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'sortOrder', - value: value, - ), - ); - }); - } - - QueryBuilder sortOrderLessThan( - SortOrder value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'sortOrder', - value: value, - ), - ); - }); - } - - QueryBuilder sortOrderBetween( - SortOrder lower, - SortOrder upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'sortOrder', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder startDateIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNull(property: r'startDate'), - ); - }); - } - - QueryBuilder startDateIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNotNull(property: r'startDate'), - ); - }); - } - - QueryBuilder startDateEqualTo( - DateTime? value, - ) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'startDate', value: value), - ); - }); - } - - QueryBuilder startDateGreaterThan( - DateTime? value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'startDate', - value: value, - ), - ); - }); - } - - QueryBuilder startDateLessThan( - DateTime? value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'startDate', - value: value, - ), - ); - }); - } - - QueryBuilder startDateBetween( - DateTime? lower, - DateTime? upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'startDate', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } -} - -extension AlbumQueryObject on QueryBuilder {} - -extension AlbumQueryLinks on QueryBuilder { - QueryBuilder owner(FilterQuery q) { - return QueryBuilder.apply(this, (query) { - return query.link(q, r'owner'); - }); - } - - QueryBuilder ownerIsNull() { - return QueryBuilder.apply(this, (query) { - return query.linkLength(r'owner', 0, true, 0, true); - }); - } - - QueryBuilder thumbnail( - FilterQuery q, - ) { - return QueryBuilder.apply(this, (query) { - return query.link(q, r'thumbnail'); - }); - } - - QueryBuilder thumbnailIsNull() { - return QueryBuilder.apply(this, (query) { - return query.linkLength(r'thumbnail', 0, true, 0, true); - }); - } - - QueryBuilder sharedUsers( - FilterQuery q, - ) { - return QueryBuilder.apply(this, (query) { - return query.link(q, r'sharedUsers'); - }); - } - - QueryBuilder sharedUsersLengthEqualTo( - int length, - ) { - return QueryBuilder.apply(this, (query) { - return query.linkLength(r'sharedUsers', length, true, length, true); - }); - } - - QueryBuilder sharedUsersIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.linkLength(r'sharedUsers', 0, true, 0, true); - }); - } - - QueryBuilder sharedUsersIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.linkLength(r'sharedUsers', 0, false, 999999, true); - }); - } - - QueryBuilder sharedUsersLengthLessThan( - int length, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.linkLength(r'sharedUsers', 0, true, length, include); - }); - } - - QueryBuilder - sharedUsersLengthGreaterThan(int length, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.linkLength(r'sharedUsers', length, include, 999999, true); - }); - } - - QueryBuilder sharedUsersLengthBetween( - int lower, - int upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.linkLength( - r'sharedUsers', - lower, - includeLower, - upper, - includeUpper, - ); - }); - } - - QueryBuilder assets( - FilterQuery q, - ) { - return QueryBuilder.apply(this, (query) { - return query.link(q, r'assets'); - }); - } - - QueryBuilder assetsLengthEqualTo( - int length, - ) { - return QueryBuilder.apply(this, (query) { - return query.linkLength(r'assets', length, true, length, true); - }); - } - - QueryBuilder assetsIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.linkLength(r'assets', 0, true, 0, true); - }); - } - - QueryBuilder assetsIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.linkLength(r'assets', 0, false, 999999, true); - }); - } - - QueryBuilder assetsLengthLessThan( - int length, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.linkLength(r'assets', 0, true, length, include); - }); - } - - QueryBuilder assetsLengthGreaterThan( - int length, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.linkLength(r'assets', length, include, 999999, true); - }); - } - - QueryBuilder assetsLengthBetween( - int lower, - int upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.linkLength( - r'assets', - lower, - includeLower, - upper, - includeUpper, - ); - }); - } -} - -extension AlbumQuerySortBy on QueryBuilder { - QueryBuilder sortByActivityEnabled() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'activityEnabled', Sort.asc); - }); - } - - QueryBuilder sortByActivityEnabledDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'activityEnabled', Sort.desc); - }); - } - - QueryBuilder sortByCreatedAt() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'createdAt', Sort.asc); - }); - } - - QueryBuilder sortByCreatedAtDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'createdAt', Sort.desc); - }); - } - - QueryBuilder sortByDescription() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'description', Sort.asc); - }); - } - - QueryBuilder sortByDescriptionDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'description', Sort.desc); - }); - } - - QueryBuilder sortByEndDate() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'endDate', Sort.asc); - }); - } - - QueryBuilder sortByEndDateDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'endDate', Sort.desc); - }); - } - - QueryBuilder sortByLastModifiedAssetTimestamp() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'lastModifiedAssetTimestamp', Sort.asc); - }); - } - - QueryBuilder - sortByLastModifiedAssetTimestampDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'lastModifiedAssetTimestamp', Sort.desc); - }); - } - - QueryBuilder sortByLocalId() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'localId', Sort.asc); - }); - } - - QueryBuilder sortByLocalIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'localId', Sort.desc); - }); - } - - QueryBuilder sortByModifiedAt() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'modifiedAt', Sort.asc); - }); - } - - QueryBuilder sortByModifiedAtDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'modifiedAt', Sort.desc); - }); - } - - QueryBuilder sortByName() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'name', Sort.asc); - }); - } - - QueryBuilder sortByNameDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'name', Sort.desc); - }); - } - - QueryBuilder sortByRemoteId() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'remoteId', Sort.asc); - }); - } - - QueryBuilder sortByRemoteIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'remoteId', Sort.desc); - }); - } - - QueryBuilder sortByShared() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'shared', Sort.asc); - }); - } - - QueryBuilder sortBySharedDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'shared', Sort.desc); - }); - } - - QueryBuilder sortBySortOrder() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'sortOrder', Sort.asc); - }); - } - - QueryBuilder sortBySortOrderDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'sortOrder', Sort.desc); - }); - } - - QueryBuilder sortByStartDate() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'startDate', Sort.asc); - }); - } - - QueryBuilder sortByStartDateDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'startDate', Sort.desc); - }); - } -} - -extension AlbumQuerySortThenBy on QueryBuilder { - QueryBuilder thenByActivityEnabled() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'activityEnabled', Sort.asc); - }); - } - - QueryBuilder thenByActivityEnabledDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'activityEnabled', Sort.desc); - }); - } - - QueryBuilder thenByCreatedAt() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'createdAt', Sort.asc); - }); - } - - QueryBuilder thenByCreatedAtDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'createdAt', Sort.desc); - }); - } - - QueryBuilder thenByDescription() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'description', Sort.asc); - }); - } - - QueryBuilder thenByDescriptionDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'description', Sort.desc); - }); - } - - QueryBuilder thenByEndDate() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'endDate', Sort.asc); - }); - } - - QueryBuilder thenByEndDateDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'endDate', Sort.desc); - }); - } - - QueryBuilder thenById() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'id', Sort.asc); - }); - } - - QueryBuilder thenByIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'id', Sort.desc); - }); - } - - QueryBuilder thenByLastModifiedAssetTimestamp() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'lastModifiedAssetTimestamp', Sort.asc); - }); - } - - QueryBuilder - thenByLastModifiedAssetTimestampDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'lastModifiedAssetTimestamp', Sort.desc); - }); - } - - QueryBuilder thenByLocalId() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'localId', Sort.asc); - }); - } - - QueryBuilder thenByLocalIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'localId', Sort.desc); - }); - } - - QueryBuilder thenByModifiedAt() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'modifiedAt', Sort.asc); - }); - } - - QueryBuilder thenByModifiedAtDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'modifiedAt', Sort.desc); - }); - } - - QueryBuilder thenByName() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'name', Sort.asc); - }); - } - - QueryBuilder thenByNameDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'name', Sort.desc); - }); - } - - QueryBuilder thenByRemoteId() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'remoteId', Sort.asc); - }); - } - - QueryBuilder thenByRemoteIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'remoteId', Sort.desc); - }); - } - - QueryBuilder thenByShared() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'shared', Sort.asc); - }); - } - - QueryBuilder thenBySharedDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'shared', Sort.desc); - }); - } - - QueryBuilder thenBySortOrder() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'sortOrder', Sort.asc); - }); - } - - QueryBuilder thenBySortOrderDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'sortOrder', Sort.desc); - }); - } - - QueryBuilder thenByStartDate() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'startDate', Sort.asc); - }); - } - - QueryBuilder thenByStartDateDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'startDate', Sort.desc); - }); - } -} - -extension AlbumQueryWhereDistinct on QueryBuilder { - QueryBuilder distinctByActivityEnabled() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'activityEnabled'); - }); - } - - QueryBuilder distinctByCreatedAt() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'createdAt'); - }); - } - - QueryBuilder distinctByDescription({ - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'description', caseSensitive: caseSensitive); - }); - } - - QueryBuilder distinctByEndDate() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'endDate'); - }); - } - - QueryBuilder distinctByLastModifiedAssetTimestamp() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'lastModifiedAssetTimestamp'); - }); - } - - QueryBuilder distinctByLocalId({ - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'localId', caseSensitive: caseSensitive); - }); - } - - QueryBuilder distinctByModifiedAt() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'modifiedAt'); - }); - } - - QueryBuilder distinctByName({ - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'name', caseSensitive: caseSensitive); - }); - } - - QueryBuilder distinctByRemoteId({ - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'remoteId', caseSensitive: caseSensitive); - }); - } - - QueryBuilder distinctByShared() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'shared'); - }); - } - - QueryBuilder distinctBySortOrder() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'sortOrder'); - }); - } - - QueryBuilder distinctByStartDate() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'startDate'); - }); - } -} - -extension AlbumQueryProperty on QueryBuilder { - QueryBuilder idProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'id'); - }); - } - - QueryBuilder activityEnabledProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'activityEnabled'); - }); - } - - QueryBuilder createdAtProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'createdAt'); - }); - } - - QueryBuilder descriptionProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'description'); - }); - } - - QueryBuilder endDateProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'endDate'); - }); - } - - QueryBuilder - lastModifiedAssetTimestampProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'lastModifiedAssetTimestamp'); - }); - } - - QueryBuilder localIdProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'localId'); - }); - } - - QueryBuilder modifiedAtProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'modifiedAt'); - }); - } - - QueryBuilder nameProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'name'); - }); - } - - QueryBuilder remoteIdProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'remoteId'); - }); - } - - QueryBuilder sharedProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'shared'); - }); - } - - QueryBuilder sortOrderProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'sortOrder'); - }); - } - - QueryBuilder startDateProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'startDate'); - }); - } -} diff --git a/mobile/lib/entities/android_device_asset.entity.dart b/mobile/lib/entities/android_device_asset.entity.dart deleted file mode 100644 index 792de346b9..0000000000 --- a/mobile/lib/entities/android_device_asset.entity.dart +++ /dev/null @@ -1,10 +0,0 @@ -import 'package:immich_mobile/entities/device_asset.entity.dart'; -import 'package:isar/isar.dart'; - -part 'android_device_asset.entity.g.dart'; - -@Collection() -class AndroidDeviceAsset extends DeviceAsset { - AndroidDeviceAsset({required this.id, required super.hash}); - Id id; -} diff --git a/mobile/lib/entities/android_device_asset.entity.g.dart b/mobile/lib/entities/android_device_asset.entity.g.dart deleted file mode 100644 index f8b1e32c72..0000000000 --- a/mobile/lib/entities/android_device_asset.entity.g.dart +++ /dev/null @@ -1,463 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'android_device_asset.entity.dart'; - -// ************************************************************************** -// IsarCollectionGenerator -// ************************************************************************** - -// coverage:ignore-file -// ignore_for_file: duplicate_ignore, non_constant_identifier_names, constant_identifier_names, invalid_use_of_protected_member, unnecessary_cast, prefer_const_constructors, lines_longer_than_80_chars, require_trailing_commas, inference_failure_on_function_invocation, unnecessary_parenthesis, unnecessary_raw_strings, unnecessary_null_checks, join_return_with_assignment, prefer_final_locals, avoid_js_rounded_ints, avoid_positional_boolean_parameters, always_specify_types - -extension GetAndroidDeviceAssetCollection on Isar { - IsarCollection get androidDeviceAssets => - this.collection(); -} - -const AndroidDeviceAssetSchema = CollectionSchema( - name: r'AndroidDeviceAsset', - id: -6758387181232899335, - properties: { - r'hash': PropertySchema(id: 0, name: r'hash', type: IsarType.byteList), - }, - - estimateSize: _androidDeviceAssetEstimateSize, - serialize: _androidDeviceAssetSerialize, - deserialize: _androidDeviceAssetDeserialize, - deserializeProp: _androidDeviceAssetDeserializeProp, - idName: r'id', - indexes: { - r'hash': IndexSchema( - id: -7973251393006690288, - name: r'hash', - unique: false, - replace: false, - properties: [ - IndexPropertySchema( - name: r'hash', - type: IndexType.hash, - caseSensitive: false, - ), - ], - ), - }, - links: {}, - embeddedSchemas: {}, - - getId: _androidDeviceAssetGetId, - getLinks: _androidDeviceAssetGetLinks, - attach: _androidDeviceAssetAttach, - version: '3.3.0-dev.3', -); - -int _androidDeviceAssetEstimateSize( - AndroidDeviceAsset object, - List offsets, - Map> allOffsets, -) { - var bytesCount = offsets.last; - bytesCount += 3 + object.hash.length; - return bytesCount; -} - -void _androidDeviceAssetSerialize( - AndroidDeviceAsset object, - IsarWriter writer, - List offsets, - Map> allOffsets, -) { - writer.writeByteList(offsets[0], object.hash); -} - -AndroidDeviceAsset _androidDeviceAssetDeserialize( - Id id, - IsarReader reader, - List offsets, - Map> allOffsets, -) { - final object = AndroidDeviceAsset( - hash: reader.readByteList(offsets[0]) ?? [], - id: id, - ); - return object; -} - -P _androidDeviceAssetDeserializeProp

( - IsarReader reader, - int propertyId, - int offset, - Map> allOffsets, -) { - switch (propertyId) { - case 0: - return (reader.readByteList(offset) ?? []) as P; - default: - throw IsarError('Unknown property with id $propertyId'); - } -} - -Id _androidDeviceAssetGetId(AndroidDeviceAsset object) { - return object.id; -} - -List> _androidDeviceAssetGetLinks( - AndroidDeviceAsset object, -) { - return []; -} - -void _androidDeviceAssetAttach( - IsarCollection col, - Id id, - AndroidDeviceAsset object, -) { - object.id = id; -} - -extension AndroidDeviceAssetQueryWhereSort - on QueryBuilder { - QueryBuilder anyId() { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause(const IdWhereClause.any()); - }); - } -} - -extension AndroidDeviceAssetQueryWhere - on QueryBuilder { - QueryBuilder - idEqualTo(Id id) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause(IdWhereClause.between(lower: id, upper: id)); - }); - } - - QueryBuilder - idNotEqualTo(Id id) { - return QueryBuilder.apply(this, (query) { - if (query.whereSort == Sort.asc) { - return query - .addWhereClause( - IdWhereClause.lessThan(upper: id, includeUpper: false), - ) - .addWhereClause( - IdWhereClause.greaterThan(lower: id, includeLower: false), - ); - } else { - return query - .addWhereClause( - IdWhereClause.greaterThan(lower: id, includeLower: false), - ) - .addWhereClause( - IdWhereClause.lessThan(upper: id, includeUpper: false), - ); - } - }); - } - - QueryBuilder - idGreaterThan(Id id, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.greaterThan(lower: id, includeLower: include), - ); - }); - } - - QueryBuilder - idLessThan(Id id, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.lessThan(upper: id, includeUpper: include), - ); - }); - } - - QueryBuilder - idBetween( - Id lowerId, - Id upperId, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.between( - lower: lowerId, - includeLower: includeLower, - upper: upperId, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder - hashEqualTo(List hash) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IndexWhereClause.equalTo(indexName: r'hash', value: [hash]), - ); - }); - } - - QueryBuilder - hashNotEqualTo(List hash) { - return QueryBuilder.apply(this, (query) { - if (query.whereSort == Sort.asc) { - return query - .addWhereClause( - IndexWhereClause.between( - indexName: r'hash', - lower: [], - upper: [hash], - includeUpper: false, - ), - ) - .addWhereClause( - IndexWhereClause.between( - indexName: r'hash', - lower: [hash], - includeLower: false, - upper: [], - ), - ); - } else { - return query - .addWhereClause( - IndexWhereClause.between( - indexName: r'hash', - lower: [hash], - includeLower: false, - upper: [], - ), - ) - .addWhereClause( - IndexWhereClause.between( - indexName: r'hash', - lower: [], - upper: [hash], - includeUpper: false, - ), - ); - } - }); - } -} - -extension AndroidDeviceAssetQueryFilter - on QueryBuilder { - QueryBuilder - hashElementEqualTo(int value) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'hash', value: value), - ); - }); - } - - QueryBuilder - hashElementGreaterThan(int value, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'hash', - value: value, - ), - ); - }); - } - - QueryBuilder - hashElementLessThan(int value, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'hash', - value: value, - ), - ); - }); - } - - QueryBuilder - hashElementBetween( - int lower, - int upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'hash', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder - hashLengthEqualTo(int length) { - return QueryBuilder.apply(this, (query) { - return query.listLength(r'hash', length, true, length, true); - }); - } - - QueryBuilder - hashIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.listLength(r'hash', 0, true, 0, true); - }); - } - - QueryBuilder - hashIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.listLength(r'hash', 0, false, 999999, true); - }); - } - - QueryBuilder - hashLengthLessThan(int length, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.listLength(r'hash', 0, true, length, include); - }); - } - - QueryBuilder - hashLengthGreaterThan(int length, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.listLength(r'hash', length, include, 999999, true); - }); - } - - QueryBuilder - hashLengthBetween( - int lower, - int upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.listLength( - r'hash', - lower, - includeLower, - upper, - includeUpper, - ); - }); - } - - QueryBuilder - idEqualTo(Id value) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'id', value: value), - ); - }); - } - - QueryBuilder - idGreaterThan(Id value, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'id', - value: value, - ), - ); - }); - } - - QueryBuilder - idLessThan(Id value, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'id', - value: value, - ), - ); - }); - } - - QueryBuilder - idBetween( - Id lower, - Id upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'id', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } -} - -extension AndroidDeviceAssetQueryObject - on QueryBuilder {} - -extension AndroidDeviceAssetQueryLinks - on QueryBuilder {} - -extension AndroidDeviceAssetQuerySortBy - on QueryBuilder {} - -extension AndroidDeviceAssetQuerySortThenBy - on QueryBuilder { - QueryBuilder - thenById() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'id', Sort.asc); - }); - } - - QueryBuilder - thenByIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'id', Sort.desc); - }); - } -} - -extension AndroidDeviceAssetQueryWhereDistinct - on QueryBuilder { - QueryBuilder - distinctByHash() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'hash'); - }); - } -} - -extension AndroidDeviceAssetQueryProperty - on QueryBuilder { - QueryBuilder idProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'id'); - }); - } - - QueryBuilder, QQueryOperations> hashProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'hash'); - }); - } -} diff --git a/mobile/lib/entities/asset.entity.dart b/mobile/lib/entities/asset.entity.dart deleted file mode 100644 index 0d549457a1..0000000000 --- a/mobile/lib/entities/asset.entity.dart +++ /dev/null @@ -1,575 +0,0 @@ -import 'dart:convert'; -import 'dart:io'; - -import 'package:immich_mobile/constants/enums.dart'; -import 'package:immich_mobile/domain/models/exif.model.dart'; -import 'package:immich_mobile/extensions/string_extensions.dart'; -import 'package:immich_mobile/infrastructure/entities/exif.entity.dart' as entity; -import 'package:immich_mobile/infrastructure/utils/exif.converter.dart'; -import 'package:immich_mobile/utils/diff.dart'; -import 'package:immich_mobile/utils/hash.dart'; -import 'package:isar/isar.dart'; -import 'package:openapi/api.dart'; -import 'package:path/path.dart' as p; -import 'package:photo_manager/photo_manager.dart' show AssetEntity; - -part 'asset.entity.g.dart'; - -/// Asset (online or local) -@Collection(inheritance: false) -class Asset { - Asset.remote(AssetResponseDto remote) - : remoteId = remote.id, - checksum = remote.checksum, - fileCreatedAt = remote.fileCreatedAt, - fileModifiedAt = remote.fileModifiedAt, - updatedAt = remote.updatedAt, - durationInSeconds = remote.duration.toDuration()?.inSeconds ?? 0, - type = remote.type.toAssetType(), - fileName = remote.originalFileName, - height = remote.exifInfo?.exifImageHeight?.toInt(), - width = remote.exifInfo?.exifImageWidth?.toInt(), - livePhotoVideoId = remote.livePhotoVideoId, - ownerId = fastHash(remote.ownerId), - exifInfo = remote.exifInfo == null ? null : ExifDtoConverter.fromDto(remote.exifInfo!), - isFavorite = remote.isFavorite, - isArchived = remote.isArchived, - isTrashed = remote.isTrashed, - isOffline = remote.isOffline, - // workaround to nullify stackPrimaryAssetId for the parent asset until we refactor the mobile app - // stack handling to properly handle it - stackPrimaryAssetId = remote.stack?.primaryAssetId == remote.id ? null : remote.stack?.primaryAssetId, - stackCount = remote.stack?.assetCount ?? 0, - stackId = remote.stack?.id, - thumbhash = remote.thumbhash, - visibility = getVisibility(remote.visibility); - - Asset({ - this.id = Isar.autoIncrement, - required this.checksum, - this.remoteId, - required this.localId, - required this.ownerId, - required this.fileCreatedAt, - required this.fileModifiedAt, - required this.updatedAt, - required this.durationInSeconds, - required this.type, - this.width, - this.height, - required this.fileName, - this.livePhotoVideoId, - this.exifInfo, - this.isFavorite = false, - this.isArchived = false, - this.isTrashed = false, - this.stackId, - this.stackPrimaryAssetId, - this.stackCount = 0, - this.isOffline = false, - this.thumbhash, - this.visibility = AssetVisibilityEnum.timeline, - }); - - @ignore - AssetEntity? _local; - - @ignore - AssetEntity? get local { - if (isLocal && _local == null) { - _local = AssetEntity( - id: localId!, - typeInt: isImage ? 1 : 2, - width: width ?? 0, - height: height ?? 0, - duration: durationInSeconds, - createDateSecond: fileCreatedAt.millisecondsSinceEpoch ~/ 1000, - modifiedDateSecond: fileModifiedAt.millisecondsSinceEpoch ~/ 1000, - title: fileName, - ); - } - return _local; - } - - set local(AssetEntity? assetEntity) => _local = assetEntity; - - @ignore - bool _didUpdateLocal = false; - - @ignore - Future get localAsync async { - final local = this.local; - if (local == null) { - throw Exception('Asset $fileName has no local data'); - } - - final updatedLocal = _didUpdateLocal ? local : await local.obtainForNewProperties(); - if (updatedLocal == null) { - throw Exception('Could not fetch local data for $fileName'); - } - - this.local = updatedLocal; - _didUpdateLocal = true; - return updatedLocal; - } - - Id id = Isar.autoIncrement; - - /// stores the raw SHA1 bytes as a base64 String - /// because Isar cannot sort lists of byte arrays - String checksum; - - String? thumbhash; - - @Index(unique: false, replace: false, type: IndexType.hash) - String? remoteId; - - @Index(unique: false, replace: false, type: IndexType.hash) - String? localId; - - @Index(unique: true, replace: false, composite: [CompositeIndex("checksum", type: IndexType.hash)]) - int ownerId; - - DateTime fileCreatedAt; - - DateTime fileModifiedAt; - - DateTime updatedAt; - - int durationInSeconds; - - @Enumerated(EnumType.ordinal) - AssetType type; - - short? width; - - short? height; - - String fileName; - - String? livePhotoVideoId; - - bool isFavorite; - - bool isArchived; - - bool isTrashed; - - bool isOffline; - - @ignore - ExifInfo? exifInfo; - - String? stackId; - - String? stackPrimaryAssetId; - - int stackCount; - - @Enumerated(EnumType.ordinal) - AssetVisibilityEnum visibility; - - /// Returns null if the asset has no sync access to the exif info - @ignore - double? get aspectRatio { - final orientatedWidth = this.orientatedWidth; - final orientatedHeight = this.orientatedHeight; - - if (orientatedWidth != null && orientatedHeight != null && orientatedWidth > 0 && orientatedHeight > 0) { - return orientatedWidth.toDouble() / orientatedHeight.toDouble(); - } - - return null; - } - - /// `true` if this [Asset] is present on the device - @ignore - bool get isLocal => localId != null; - - @ignore - bool get isInDb => id != Isar.autoIncrement; - - @ignore - String get name => p.withoutExtension(fileName); - - /// `true` if this [Asset] is present on the server - @ignore - bool get isRemote => remoteId != null; - - @ignore - bool get isImage => type == AssetType.image; - - @ignore - bool get isVideo => type == AssetType.video; - - @ignore - bool get isMotionPhoto => livePhotoVideoId != null; - - @ignore - AssetState get storage { - if (isRemote && isLocal) { - return AssetState.merged; - } else if (isRemote) { - return AssetState.remote; - } else if (isLocal) { - return AssetState.local; - } else { - throw Exception("Asset has illegal state: $this"); - } - } - - @ignore - Duration get duration => Duration(seconds: durationInSeconds); - - // ignore: invalid_annotation_target - @ignore - set byteHash(List hash) => checksum = base64.encode(hash); - - /// Returns null if the asset has no sync access to the exif info - @ignore - @pragma('vm:prefer-inline') - bool? get isFlipped { - final exifInfo = this.exifInfo; - if (exifInfo != null) { - return exifInfo.isFlipped; - } - - if (_didUpdateLocal && Platform.isAndroid) { - final local = this.local; - if (local == null) { - throw Exception('Asset $fileName has no local data'); - } - return local.orientation == 90 || local.orientation == 270; - } - - return null; - } - - /// Returns null if the asset has no sync access to the exif info - @ignore - @pragma('vm:prefer-inline') - int? get orientatedHeight { - final isFlipped = this.isFlipped; - if (isFlipped == null) { - return null; - } - - return isFlipped ? width : height; - } - - /// Returns null if the asset has no sync access to the exif info - @ignore - @pragma('vm:prefer-inline') - int? get orientatedWidth { - final isFlipped = this.isFlipped; - if (isFlipped == null) { - return null; - } - - return isFlipped ? height : width; - } - - @override - bool operator ==(other) { - if (other is! Asset) return false; - if (identical(this, other)) return true; - return id == other.id && - checksum == other.checksum && - remoteId == other.remoteId && - localId == other.localId && - ownerId == other.ownerId && - fileCreatedAt.isAtSameMomentAs(other.fileCreatedAt) && - fileModifiedAt.isAtSameMomentAs(other.fileModifiedAt) && - updatedAt.isAtSameMomentAs(other.updatedAt) && - durationInSeconds == other.durationInSeconds && - type == other.type && - width == other.width && - height == other.height && - fileName == other.fileName && - livePhotoVideoId == other.livePhotoVideoId && - isFavorite == other.isFavorite && - isLocal == other.isLocal && - isArchived == other.isArchived && - isTrashed == other.isTrashed && - stackCount == other.stackCount && - stackPrimaryAssetId == other.stackPrimaryAssetId && - stackId == other.stackId; - } - - @override - @ignore - int get hashCode => - id.hashCode ^ - checksum.hashCode ^ - remoteId.hashCode ^ - localId.hashCode ^ - ownerId.hashCode ^ - fileCreatedAt.hashCode ^ - fileModifiedAt.hashCode ^ - updatedAt.hashCode ^ - durationInSeconds.hashCode ^ - type.hashCode ^ - width.hashCode ^ - height.hashCode ^ - fileName.hashCode ^ - livePhotoVideoId.hashCode ^ - isFavorite.hashCode ^ - isLocal.hashCode ^ - isArchived.hashCode ^ - isTrashed.hashCode ^ - stackCount.hashCode ^ - stackPrimaryAssetId.hashCode ^ - stackId.hashCode; - - /// Returns `true` if this [Asset] can updated with values from parameter [a] - bool canUpdate(Asset a) { - assert(isInDb); - assert(checksum == a.checksum); - assert(a.storage != AssetState.merged); - return a.updatedAt.isAfter(updatedAt) || - a.isRemote && !isRemote || - a.isLocal && !isLocal || - width == null && a.width != null || - height == null && a.height != null || - livePhotoVideoId == null && a.livePhotoVideoId != null || - isFavorite != a.isFavorite || - isArchived != a.isArchived || - isTrashed != a.isTrashed || - isOffline != a.isOffline || - a.exifInfo?.latitude != exifInfo?.latitude || - a.exifInfo?.longitude != exifInfo?.longitude || - // no local stack count or different count from remote - a.thumbhash != thumbhash || - stackId != a.stackId || - stackCount != a.stackCount || - stackPrimaryAssetId == null && a.stackPrimaryAssetId != null || - visibility != a.visibility; - } - - /// Returns a new [Asset] with values from this and merged & updated with [a] - Asset updatedCopy(Asset a) { - assert(canUpdate(a)); - if (a.updatedAt.isAfter(updatedAt)) { - // take most values from newer asset - // keep vales that can never be set by the asset not in DB - if (a.isRemote) { - return a.copyWith( - id: id, - localId: localId, - width: a.width ?? width, - height: a.height ?? height, - exifInfo: a.exifInfo?.copyWith(assetId: id) ?? exifInfo, - ); - } else if (isRemote) { - return copyWith( - localId: localId ?? a.localId, - width: width ?? a.width, - height: height ?? a.height, - exifInfo: exifInfo ?? a.exifInfo?.copyWith(assetId: id), - ); - } else { - // TODO: Revisit this and remove all bool field assignments - return a.copyWith( - id: id, - remoteId: remoteId, - livePhotoVideoId: livePhotoVideoId, - // workaround to nullify stackPrimaryAssetId for the parent asset until we refactor the mobile app - // stack handling to properly handle it - stackId: stackId, - stackPrimaryAssetId: stackPrimaryAssetId == remoteId ? null : stackPrimaryAssetId, - stackCount: stackCount, - isFavorite: isFavorite, - isArchived: isArchived, - isTrashed: isTrashed, - isOffline: isOffline, - ); - } - } else { - // fill in potentially missing values, i.e. merge assets - if (a.isRemote) { - // values from remote take precedence - return copyWith( - remoteId: a.remoteId, - width: a.width, - height: a.height, - livePhotoVideoId: a.livePhotoVideoId, - // workaround to nullify stackPrimaryAssetId for the parent asset until we refactor the mobile app - // stack handling to properly handle it - stackId: a.stackId, - stackPrimaryAssetId: a.stackPrimaryAssetId == a.remoteId ? null : a.stackPrimaryAssetId, - stackCount: a.stackCount, - // isFavorite + isArchived are not set by device-only assets - isFavorite: a.isFavorite, - isArchived: a.isArchived, - isTrashed: a.isTrashed, - isOffline: a.isOffline, - exifInfo: a.exifInfo?.copyWith(assetId: id) ?? exifInfo, - thumbhash: a.thumbhash, - ); - } else { - // add only missing values (and set isLocal to true) - return copyWith( - localId: localId ?? a.localId, - width: width ?? a.width, - height: height ?? a.height, - exifInfo: exifInfo ?? a.exifInfo?.copyWith(assetId: id), // updated to use assetId - ); - } - } - } - - Asset copyWith({ - Id? id, - String? checksum, - String? remoteId, - String? localId, - int? ownerId, - DateTime? fileCreatedAt, - DateTime? fileModifiedAt, - DateTime? updatedAt, - int? durationInSeconds, - AssetType? type, - short? width, - short? height, - String? fileName, - String? livePhotoVideoId, - bool? isFavorite, - bool? isArchived, - bool? isTrashed, - bool? isOffline, - ExifInfo? exifInfo, - String? stackId, - String? stackPrimaryAssetId, - int? stackCount, - String? thumbhash, - AssetVisibilityEnum? visibility, - }) => Asset( - id: id ?? this.id, - checksum: checksum ?? this.checksum, - remoteId: remoteId ?? this.remoteId, - localId: localId ?? this.localId, - ownerId: ownerId ?? this.ownerId, - fileCreatedAt: fileCreatedAt ?? this.fileCreatedAt, - fileModifiedAt: fileModifiedAt ?? this.fileModifiedAt, - updatedAt: updatedAt ?? this.updatedAt, - durationInSeconds: durationInSeconds ?? this.durationInSeconds, - type: type ?? this.type, - width: width ?? this.width, - height: height ?? this.height, - fileName: fileName ?? this.fileName, - livePhotoVideoId: livePhotoVideoId ?? this.livePhotoVideoId, - isFavorite: isFavorite ?? this.isFavorite, - isArchived: isArchived ?? this.isArchived, - isTrashed: isTrashed ?? this.isTrashed, - isOffline: isOffline ?? this.isOffline, - exifInfo: exifInfo ?? this.exifInfo, - stackId: stackId ?? this.stackId, - stackPrimaryAssetId: stackPrimaryAssetId ?? this.stackPrimaryAssetId, - stackCount: stackCount ?? this.stackCount, - thumbhash: thumbhash ?? this.thumbhash, - visibility: visibility ?? this.visibility, - ); - - Future put(Isar db) async { - await db.assets.put(this); - if (exifInfo != null) { - await db.exifInfos.put(entity.ExifInfo.fromDto(exifInfo!.copyWith(assetId: id))); - } - } - - static int compareById(Asset a, Asset b) => a.id.compareTo(b.id); - - static int compareByLocalId(Asset a, Asset b) => compareToNullable(a.localId, b.localId); - - static int compareByChecksum(Asset a, Asset b) => a.checksum.compareTo(b.checksum); - - static int compareByOwnerChecksum(Asset a, Asset b) { - final int ownerIdOrder = a.ownerId.compareTo(b.ownerId); - if (ownerIdOrder != 0) return ownerIdOrder; - return compareByChecksum(a, b); - } - - static int compareByOwnerChecksumCreatedModified(Asset a, Asset b) { - final int ownerIdOrder = a.ownerId.compareTo(b.ownerId); - if (ownerIdOrder != 0) return ownerIdOrder; - final int checksumOrder = compareByChecksum(a, b); - if (checksumOrder != 0) return checksumOrder; - final int createdOrder = a.fileCreatedAt.compareTo(b.fileCreatedAt); - if (createdOrder != 0) return createdOrder; - return a.fileModifiedAt.compareTo(b.fileModifiedAt); - } - - @override - String toString() { - return """ -{ - "id": ${id == Isar.autoIncrement ? '"N/A"' : id}, - "remoteId": "${remoteId ?? "N/A"}", - "localId": "${localId ?? "N/A"}", - "checksum": "$checksum", - "ownerId": $ownerId, - "livePhotoVideoId": "${livePhotoVideoId ?? "N/A"}", - "stackId": "${stackId ?? "N/A"}", - "stackPrimaryAssetId": "${stackPrimaryAssetId ?? "N/A"}", - "stackCount": "$stackCount", - "fileCreatedAt": "$fileCreatedAt", - "fileModifiedAt": "$fileModifiedAt", - "updatedAt": "$updatedAt", - "durationInSeconds": $durationInSeconds, - "type": "$type", - "fileName": "$fileName", - "isFavorite": $isFavorite, - "isRemote": $isRemote, - "storage": "$storage", - "width": ${width ?? "N/A"}, - "height": ${height ?? "N/A"}, - "isArchived": $isArchived, - "isTrashed": $isTrashed, - "isOffline": $isOffline, - "visibility": "$visibility", -}"""; - } - - static getVisibility(AssetVisibility visibility) => switch (visibility) { - AssetVisibility.archive => AssetVisibilityEnum.archive, - AssetVisibility.hidden => AssetVisibilityEnum.hidden, - AssetVisibility.locked => AssetVisibilityEnum.locked, - AssetVisibility.timeline || _ => AssetVisibilityEnum.timeline, - }; -} - -enum AssetType { - // do not change this order! - other, - image, - video, - audio, -} - -extension AssetTypeEnumHelper on AssetTypeEnum { - AssetType toAssetType() => switch (this) { - AssetTypeEnum.IMAGE => AssetType.image, - AssetTypeEnum.VIDEO => AssetType.video, - AssetTypeEnum.AUDIO => AssetType.audio, - AssetTypeEnum.OTHER => AssetType.other, - _ => throw Exception(), - }; -} - -/// Describes where the information of this asset came from: -/// only from the local device, only from the remote server or merged from both -enum AssetState { local, remote, merged } - -extension AssetsHelper on IsarCollection { - Future deleteAllByRemoteId(Iterable ids) => ids.isEmpty ? Future.value(0) : remote(ids).deleteAll(); - Future deleteAllByLocalId(Iterable ids) => ids.isEmpty ? Future.value(0) : local(ids).deleteAll(); - Future> getAllByRemoteId(Iterable ids) => ids.isEmpty ? Future.value([]) : remote(ids).findAll(); - Future> getAllByLocalId(Iterable ids) => ids.isEmpty ? Future.value([]) : local(ids).findAll(); - Future getByRemoteId(String id) => where().remoteIdEqualTo(id).findFirst(); - - QueryBuilder remote(Iterable ids) => - where().anyOf(ids, (q, String e) => q.remoteIdEqualTo(e)); - QueryBuilder local(Iterable ids) { - return where().anyOf(ids, (q, String e) => q.localIdEqualTo(e)); - } -} diff --git a/mobile/lib/entities/asset.entity.g.dart b/mobile/lib/entities/asset.entity.g.dart deleted file mode 100644 index db6bc72331..0000000000 --- a/mobile/lib/entities/asset.entity.g.dart +++ /dev/null @@ -1,3711 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'asset.entity.dart'; - -// ************************************************************************** -// IsarCollectionGenerator -// ************************************************************************** - -// coverage:ignore-file -// ignore_for_file: duplicate_ignore, non_constant_identifier_names, constant_identifier_names, invalid_use_of_protected_member, unnecessary_cast, prefer_const_constructors, lines_longer_than_80_chars, require_trailing_commas, inference_failure_on_function_invocation, unnecessary_parenthesis, unnecessary_raw_strings, unnecessary_null_checks, join_return_with_assignment, prefer_final_locals, avoid_js_rounded_ints, avoid_positional_boolean_parameters, always_specify_types - -extension GetAssetCollection on Isar { - IsarCollection get assets => this.collection(); -} - -const AssetSchema = CollectionSchema( - name: r'Asset', - id: -2933289051367723566, - properties: { - r'checksum': PropertySchema( - id: 0, - name: r'checksum', - type: IsarType.string, - ), - r'durationInSeconds': PropertySchema( - id: 1, - name: r'durationInSeconds', - type: IsarType.long, - ), - r'fileCreatedAt': PropertySchema( - id: 2, - name: r'fileCreatedAt', - type: IsarType.dateTime, - ), - r'fileModifiedAt': PropertySchema( - id: 3, - name: r'fileModifiedAt', - type: IsarType.dateTime, - ), - r'fileName': PropertySchema( - id: 4, - name: r'fileName', - type: IsarType.string, - ), - r'height': PropertySchema(id: 5, name: r'height', type: IsarType.int), - r'isArchived': PropertySchema( - id: 6, - name: r'isArchived', - type: IsarType.bool, - ), - r'isFavorite': PropertySchema( - id: 7, - name: r'isFavorite', - type: IsarType.bool, - ), - r'isOffline': PropertySchema( - id: 8, - name: r'isOffline', - type: IsarType.bool, - ), - r'isTrashed': PropertySchema( - id: 9, - name: r'isTrashed', - type: IsarType.bool, - ), - r'livePhotoVideoId': PropertySchema( - id: 10, - name: r'livePhotoVideoId', - type: IsarType.string, - ), - r'localId': PropertySchema(id: 11, name: r'localId', type: IsarType.string), - r'ownerId': PropertySchema(id: 12, name: r'ownerId', type: IsarType.long), - r'remoteId': PropertySchema( - id: 13, - name: r'remoteId', - type: IsarType.string, - ), - r'stackCount': PropertySchema( - id: 14, - name: r'stackCount', - type: IsarType.long, - ), - r'stackId': PropertySchema(id: 15, name: r'stackId', type: IsarType.string), - r'stackPrimaryAssetId': PropertySchema( - id: 16, - name: r'stackPrimaryAssetId', - type: IsarType.string, - ), - r'thumbhash': PropertySchema( - id: 17, - name: r'thumbhash', - type: IsarType.string, - ), - r'type': PropertySchema( - id: 18, - name: r'type', - type: IsarType.byte, - enumMap: _AssettypeEnumValueMap, - ), - r'updatedAt': PropertySchema( - id: 19, - name: r'updatedAt', - type: IsarType.dateTime, - ), - r'visibility': PropertySchema( - id: 20, - name: r'visibility', - type: IsarType.byte, - enumMap: _AssetvisibilityEnumValueMap, - ), - r'width': PropertySchema(id: 21, name: r'width', type: IsarType.int), - }, - - estimateSize: _assetEstimateSize, - serialize: _assetSerialize, - deserialize: _assetDeserialize, - deserializeProp: _assetDeserializeProp, - idName: r'id', - indexes: { - r'remoteId': IndexSchema( - id: 6301175856541681032, - name: r'remoteId', - unique: false, - replace: false, - properties: [ - IndexPropertySchema( - name: r'remoteId', - type: IndexType.hash, - caseSensitive: true, - ), - ], - ), - r'localId': IndexSchema( - id: 1199848425898359622, - name: r'localId', - unique: false, - replace: false, - properties: [ - IndexPropertySchema( - name: r'localId', - type: IndexType.hash, - caseSensitive: true, - ), - ], - ), - r'ownerId_checksum': IndexSchema( - id: -3295822444433175883, - name: r'ownerId_checksum', - unique: true, - replace: false, - properties: [ - IndexPropertySchema( - name: r'ownerId', - type: IndexType.value, - caseSensitive: false, - ), - IndexPropertySchema( - name: r'checksum', - type: IndexType.hash, - caseSensitive: true, - ), - ], - ), - }, - links: {}, - embeddedSchemas: {}, - - getId: _assetGetId, - getLinks: _assetGetLinks, - attach: _assetAttach, - version: '3.3.0-dev.3', -); - -int _assetEstimateSize( - Asset object, - List offsets, - Map> allOffsets, -) { - var bytesCount = offsets.last; - bytesCount += 3 + object.checksum.length * 3; - bytesCount += 3 + object.fileName.length * 3; - { - final value = object.livePhotoVideoId; - if (value != null) { - bytesCount += 3 + value.length * 3; - } - } - { - final value = object.localId; - if (value != null) { - bytesCount += 3 + value.length * 3; - } - } - { - final value = object.remoteId; - if (value != null) { - bytesCount += 3 + value.length * 3; - } - } - { - final value = object.stackId; - if (value != null) { - bytesCount += 3 + value.length * 3; - } - } - { - final value = object.stackPrimaryAssetId; - if (value != null) { - bytesCount += 3 + value.length * 3; - } - } - { - final value = object.thumbhash; - if (value != null) { - bytesCount += 3 + value.length * 3; - } - } - return bytesCount; -} - -void _assetSerialize( - Asset object, - IsarWriter writer, - List offsets, - Map> allOffsets, -) { - writer.writeString(offsets[0], object.checksum); - writer.writeLong(offsets[1], object.durationInSeconds); - writer.writeDateTime(offsets[2], object.fileCreatedAt); - writer.writeDateTime(offsets[3], object.fileModifiedAt); - writer.writeString(offsets[4], object.fileName); - writer.writeInt(offsets[5], object.height); - writer.writeBool(offsets[6], object.isArchived); - writer.writeBool(offsets[7], object.isFavorite); - writer.writeBool(offsets[8], object.isOffline); - writer.writeBool(offsets[9], object.isTrashed); - writer.writeString(offsets[10], object.livePhotoVideoId); - writer.writeString(offsets[11], object.localId); - writer.writeLong(offsets[12], object.ownerId); - writer.writeString(offsets[13], object.remoteId); - writer.writeLong(offsets[14], object.stackCount); - writer.writeString(offsets[15], object.stackId); - writer.writeString(offsets[16], object.stackPrimaryAssetId); - writer.writeString(offsets[17], object.thumbhash); - writer.writeByte(offsets[18], object.type.index); - writer.writeDateTime(offsets[19], object.updatedAt); - writer.writeByte(offsets[20], object.visibility.index); - writer.writeInt(offsets[21], object.width); -} - -Asset _assetDeserialize( - Id id, - IsarReader reader, - List offsets, - Map> allOffsets, -) { - final object = Asset( - checksum: reader.readString(offsets[0]), - durationInSeconds: reader.readLong(offsets[1]), - fileCreatedAt: reader.readDateTime(offsets[2]), - fileModifiedAt: reader.readDateTime(offsets[3]), - fileName: reader.readString(offsets[4]), - height: reader.readIntOrNull(offsets[5]), - id: id, - isArchived: reader.readBoolOrNull(offsets[6]) ?? false, - isFavorite: reader.readBoolOrNull(offsets[7]) ?? false, - isOffline: reader.readBoolOrNull(offsets[8]) ?? false, - isTrashed: reader.readBoolOrNull(offsets[9]) ?? false, - livePhotoVideoId: reader.readStringOrNull(offsets[10]), - localId: reader.readStringOrNull(offsets[11]), - ownerId: reader.readLong(offsets[12]), - remoteId: reader.readStringOrNull(offsets[13]), - stackCount: reader.readLongOrNull(offsets[14]) ?? 0, - stackId: reader.readStringOrNull(offsets[15]), - stackPrimaryAssetId: reader.readStringOrNull(offsets[16]), - thumbhash: reader.readStringOrNull(offsets[17]), - type: - _AssettypeValueEnumMap[reader.readByteOrNull(offsets[18])] ?? - AssetType.other, - updatedAt: reader.readDateTime(offsets[19]), - visibility: - _AssetvisibilityValueEnumMap[reader.readByteOrNull(offsets[20])] ?? - AssetVisibilityEnum.timeline, - width: reader.readIntOrNull(offsets[21]), - ); - return object; -} - -P _assetDeserializeProp

( - IsarReader reader, - int propertyId, - int offset, - Map> allOffsets, -) { - switch (propertyId) { - case 0: - return (reader.readString(offset)) as P; - case 1: - return (reader.readLong(offset)) as P; - case 2: - return (reader.readDateTime(offset)) as P; - case 3: - return (reader.readDateTime(offset)) as P; - case 4: - return (reader.readString(offset)) as P; - case 5: - return (reader.readIntOrNull(offset)) as P; - case 6: - return (reader.readBoolOrNull(offset) ?? false) as P; - case 7: - return (reader.readBoolOrNull(offset) ?? false) as P; - case 8: - return (reader.readBoolOrNull(offset) ?? false) as P; - case 9: - return (reader.readBoolOrNull(offset) ?? false) as P; - case 10: - return (reader.readStringOrNull(offset)) as P; - case 11: - return (reader.readStringOrNull(offset)) as P; - case 12: - return (reader.readLong(offset)) as P; - case 13: - return (reader.readStringOrNull(offset)) as P; - case 14: - return (reader.readLongOrNull(offset) ?? 0) as P; - case 15: - return (reader.readStringOrNull(offset)) as P; - case 16: - return (reader.readStringOrNull(offset)) as P; - case 17: - return (reader.readStringOrNull(offset)) as P; - case 18: - return (_AssettypeValueEnumMap[reader.readByteOrNull(offset)] ?? - AssetType.other) - as P; - case 19: - return (reader.readDateTime(offset)) as P; - case 20: - return (_AssetvisibilityValueEnumMap[reader.readByteOrNull(offset)] ?? - AssetVisibilityEnum.timeline) - as P; - case 21: - return (reader.readIntOrNull(offset)) as P; - default: - throw IsarError('Unknown property with id $propertyId'); - } -} - -const _AssettypeEnumValueMap = {'other': 0, 'image': 1, 'video': 2, 'audio': 3}; -const _AssettypeValueEnumMap = { - 0: AssetType.other, - 1: AssetType.image, - 2: AssetType.video, - 3: AssetType.audio, -}; -const _AssetvisibilityEnumValueMap = { - 'timeline': 0, - 'hidden': 1, - 'archive': 2, - 'locked': 3, -}; -const _AssetvisibilityValueEnumMap = { - 0: AssetVisibilityEnum.timeline, - 1: AssetVisibilityEnum.hidden, - 2: AssetVisibilityEnum.archive, - 3: AssetVisibilityEnum.locked, -}; - -Id _assetGetId(Asset object) { - return object.id; -} - -List> _assetGetLinks(Asset object) { - return []; -} - -void _assetAttach(IsarCollection col, Id id, Asset object) { - object.id = id; -} - -extension AssetByIndex on IsarCollection { - Future getByOwnerIdChecksum(int ownerId, String checksum) { - return getByIndex(r'ownerId_checksum', [ownerId, checksum]); - } - - Asset? getByOwnerIdChecksumSync(int ownerId, String checksum) { - return getByIndexSync(r'ownerId_checksum', [ownerId, checksum]); - } - - Future deleteByOwnerIdChecksum(int ownerId, String checksum) { - return deleteByIndex(r'ownerId_checksum', [ownerId, checksum]); - } - - bool deleteByOwnerIdChecksumSync(int ownerId, String checksum) { - return deleteByIndexSync(r'ownerId_checksum', [ownerId, checksum]); - } - - Future> getAllByOwnerIdChecksum( - List ownerIdValues, - List checksumValues, - ) { - final len = ownerIdValues.length; - assert( - checksumValues.length == len, - 'All index values must have the same length', - ); - final values = >[]; - for (var i = 0; i < len; i++) { - values.add([ownerIdValues[i], checksumValues[i]]); - } - - return getAllByIndex(r'ownerId_checksum', values); - } - - List getAllByOwnerIdChecksumSync( - List ownerIdValues, - List checksumValues, - ) { - final len = ownerIdValues.length; - assert( - checksumValues.length == len, - 'All index values must have the same length', - ); - final values = >[]; - for (var i = 0; i < len; i++) { - values.add([ownerIdValues[i], checksumValues[i]]); - } - - return getAllByIndexSync(r'ownerId_checksum', values); - } - - Future deleteAllByOwnerIdChecksum( - List ownerIdValues, - List checksumValues, - ) { - final len = ownerIdValues.length; - assert( - checksumValues.length == len, - 'All index values must have the same length', - ); - final values = >[]; - for (var i = 0; i < len; i++) { - values.add([ownerIdValues[i], checksumValues[i]]); - } - - return deleteAllByIndex(r'ownerId_checksum', values); - } - - int deleteAllByOwnerIdChecksumSync( - List ownerIdValues, - List checksumValues, - ) { - final len = ownerIdValues.length; - assert( - checksumValues.length == len, - 'All index values must have the same length', - ); - final values = >[]; - for (var i = 0; i < len; i++) { - values.add([ownerIdValues[i], checksumValues[i]]); - } - - return deleteAllByIndexSync(r'ownerId_checksum', values); - } - - Future putByOwnerIdChecksum(Asset object) { - return putByIndex(r'ownerId_checksum', object); - } - - Id putByOwnerIdChecksumSync(Asset object, {bool saveLinks = true}) { - return putByIndexSync(r'ownerId_checksum', object, saveLinks: saveLinks); - } - - Future> putAllByOwnerIdChecksum(List objects) { - return putAllByIndex(r'ownerId_checksum', objects); - } - - List putAllByOwnerIdChecksumSync( - List objects, { - bool saveLinks = true, - }) { - return putAllByIndexSync( - r'ownerId_checksum', - objects, - saveLinks: saveLinks, - ); - } -} - -extension AssetQueryWhereSort on QueryBuilder { - QueryBuilder anyId() { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause(const IdWhereClause.any()); - }); - } -} - -extension AssetQueryWhere on QueryBuilder { - QueryBuilder idEqualTo(Id id) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause(IdWhereClause.between(lower: id, upper: id)); - }); - } - - QueryBuilder idNotEqualTo(Id id) { - return QueryBuilder.apply(this, (query) { - if (query.whereSort == Sort.asc) { - return query - .addWhereClause( - IdWhereClause.lessThan(upper: id, includeUpper: false), - ) - .addWhereClause( - IdWhereClause.greaterThan(lower: id, includeLower: false), - ); - } else { - return query - .addWhereClause( - IdWhereClause.greaterThan(lower: id, includeLower: false), - ) - .addWhereClause( - IdWhereClause.lessThan(upper: id, includeUpper: false), - ); - } - }); - } - - QueryBuilder idGreaterThan( - Id id, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.greaterThan(lower: id, includeLower: include), - ); - }); - } - - QueryBuilder idLessThan( - Id id, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.lessThan(upper: id, includeUpper: include), - ); - }); - } - - QueryBuilder idBetween( - Id lowerId, - Id upperId, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.between( - lower: lowerId, - includeLower: includeLower, - upper: upperId, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder remoteIdIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IndexWhereClause.equalTo(indexName: r'remoteId', value: [null]), - ); - }); - } - - QueryBuilder remoteIdIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IndexWhereClause.between( - indexName: r'remoteId', - lower: [null], - includeLower: false, - upper: [], - ), - ); - }); - } - - QueryBuilder remoteIdEqualTo( - String? remoteId, - ) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IndexWhereClause.equalTo(indexName: r'remoteId', value: [remoteId]), - ); - }); - } - - QueryBuilder remoteIdNotEqualTo( - String? remoteId, - ) { - return QueryBuilder.apply(this, (query) { - if (query.whereSort == Sort.asc) { - return query - .addWhereClause( - IndexWhereClause.between( - indexName: r'remoteId', - lower: [], - upper: [remoteId], - includeUpper: false, - ), - ) - .addWhereClause( - IndexWhereClause.between( - indexName: r'remoteId', - lower: [remoteId], - includeLower: false, - upper: [], - ), - ); - } else { - return query - .addWhereClause( - IndexWhereClause.between( - indexName: r'remoteId', - lower: [remoteId], - includeLower: false, - upper: [], - ), - ) - .addWhereClause( - IndexWhereClause.between( - indexName: r'remoteId', - lower: [], - upper: [remoteId], - includeUpper: false, - ), - ); - } - }); - } - - QueryBuilder localIdIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IndexWhereClause.equalTo(indexName: r'localId', value: [null]), - ); - }); - } - - QueryBuilder localIdIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IndexWhereClause.between( - indexName: r'localId', - lower: [null], - includeLower: false, - upper: [], - ), - ); - }); - } - - QueryBuilder localIdEqualTo( - String? localId, - ) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IndexWhereClause.equalTo(indexName: r'localId', value: [localId]), - ); - }); - } - - QueryBuilder localIdNotEqualTo( - String? localId, - ) { - return QueryBuilder.apply(this, (query) { - if (query.whereSort == Sort.asc) { - return query - .addWhereClause( - IndexWhereClause.between( - indexName: r'localId', - lower: [], - upper: [localId], - includeUpper: false, - ), - ) - .addWhereClause( - IndexWhereClause.between( - indexName: r'localId', - lower: [localId], - includeLower: false, - upper: [], - ), - ); - } else { - return query - .addWhereClause( - IndexWhereClause.between( - indexName: r'localId', - lower: [localId], - includeLower: false, - upper: [], - ), - ) - .addWhereClause( - IndexWhereClause.between( - indexName: r'localId', - lower: [], - upper: [localId], - includeUpper: false, - ), - ); - } - }); - } - - QueryBuilder ownerIdEqualToAnyChecksum( - int ownerId, - ) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IndexWhereClause.equalTo( - indexName: r'ownerId_checksum', - value: [ownerId], - ), - ); - }); - } - - QueryBuilder ownerIdNotEqualToAnyChecksum( - int ownerId, - ) { - return QueryBuilder.apply(this, (query) { - if (query.whereSort == Sort.asc) { - return query - .addWhereClause( - IndexWhereClause.between( - indexName: r'ownerId_checksum', - lower: [], - upper: [ownerId], - includeUpper: false, - ), - ) - .addWhereClause( - IndexWhereClause.between( - indexName: r'ownerId_checksum', - lower: [ownerId], - includeLower: false, - upper: [], - ), - ); - } else { - return query - .addWhereClause( - IndexWhereClause.between( - indexName: r'ownerId_checksum', - lower: [ownerId], - includeLower: false, - upper: [], - ), - ) - .addWhereClause( - IndexWhereClause.between( - indexName: r'ownerId_checksum', - lower: [], - upper: [ownerId], - includeUpper: false, - ), - ); - } - }); - } - - QueryBuilder ownerIdGreaterThanAnyChecksum( - int ownerId, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IndexWhereClause.between( - indexName: r'ownerId_checksum', - lower: [ownerId], - includeLower: include, - upper: [], - ), - ); - }); - } - - QueryBuilder ownerIdLessThanAnyChecksum( - int ownerId, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IndexWhereClause.between( - indexName: r'ownerId_checksum', - lower: [], - upper: [ownerId], - includeUpper: include, - ), - ); - }); - } - - QueryBuilder ownerIdBetweenAnyChecksum( - int lowerOwnerId, - int upperOwnerId, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IndexWhereClause.between( - indexName: r'ownerId_checksum', - lower: [lowerOwnerId], - includeLower: includeLower, - upper: [upperOwnerId], - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder ownerIdChecksumEqualTo( - int ownerId, - String checksum, - ) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IndexWhereClause.equalTo( - indexName: r'ownerId_checksum', - value: [ownerId, checksum], - ), - ); - }); - } - - QueryBuilder - ownerIdEqualToChecksumNotEqualTo(int ownerId, String checksum) { - return QueryBuilder.apply(this, (query) { - if (query.whereSort == Sort.asc) { - return query - .addWhereClause( - IndexWhereClause.between( - indexName: r'ownerId_checksum', - lower: [ownerId], - upper: [ownerId, checksum], - includeUpper: false, - ), - ) - .addWhereClause( - IndexWhereClause.between( - indexName: r'ownerId_checksum', - lower: [ownerId, checksum], - includeLower: false, - upper: [ownerId], - ), - ); - } else { - return query - .addWhereClause( - IndexWhereClause.between( - indexName: r'ownerId_checksum', - lower: [ownerId, checksum], - includeLower: false, - upper: [ownerId], - ), - ) - .addWhereClause( - IndexWhereClause.between( - indexName: r'ownerId_checksum', - lower: [ownerId], - upper: [ownerId, checksum], - includeUpper: false, - ), - ); - } - }); - } -} - -extension AssetQueryFilter on QueryBuilder { - QueryBuilder checksumEqualTo( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'checksum', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder checksumGreaterThan( - String value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'checksum', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder checksumLessThan( - String value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'checksum', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder checksumBetween( - String lower, - String upper, { - bool includeLower = true, - bool includeUpper = true, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'checksum', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder checksumStartsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.startsWith( - property: r'checksum', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder checksumEndsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.endsWith( - property: r'checksum', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder checksumContains( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.contains( - property: r'checksum', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder checksumMatches( - String pattern, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.matches( - property: r'checksum', - wildcard: pattern, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder checksumIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'checksum', value: ''), - ); - }); - } - - QueryBuilder checksumIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan(property: r'checksum', value: ''), - ); - }); - } - - QueryBuilder durationInSecondsEqualTo( - int value, - ) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'durationInSeconds', value: value), - ); - }); - } - - QueryBuilder - durationInSecondsGreaterThan(int value, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'durationInSeconds', - value: value, - ), - ); - }); - } - - QueryBuilder durationInSecondsLessThan( - int value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'durationInSeconds', - value: value, - ), - ); - }); - } - - QueryBuilder durationInSecondsBetween( - int lower, - int upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'durationInSeconds', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder fileCreatedAtEqualTo( - DateTime value, - ) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'fileCreatedAt', value: value), - ); - }); - } - - QueryBuilder fileCreatedAtGreaterThan( - DateTime value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'fileCreatedAt', - value: value, - ), - ); - }); - } - - QueryBuilder fileCreatedAtLessThan( - DateTime value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'fileCreatedAt', - value: value, - ), - ); - }); - } - - QueryBuilder fileCreatedAtBetween( - DateTime lower, - DateTime upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'fileCreatedAt', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder fileModifiedAtEqualTo( - DateTime value, - ) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'fileModifiedAt', value: value), - ); - }); - } - - QueryBuilder fileModifiedAtGreaterThan( - DateTime value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'fileModifiedAt', - value: value, - ), - ); - }); - } - - QueryBuilder fileModifiedAtLessThan( - DateTime value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'fileModifiedAt', - value: value, - ), - ); - }); - } - - QueryBuilder fileModifiedAtBetween( - DateTime lower, - DateTime upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'fileModifiedAt', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder fileNameEqualTo( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'fileName', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder fileNameGreaterThan( - String value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'fileName', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder fileNameLessThan( - String value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'fileName', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder fileNameBetween( - String lower, - String upper, { - bool includeLower = true, - bool includeUpper = true, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'fileName', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder fileNameStartsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.startsWith( - property: r'fileName', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder fileNameEndsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.endsWith( - property: r'fileName', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder fileNameContains( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.contains( - property: r'fileName', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder fileNameMatches( - String pattern, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.matches( - property: r'fileName', - wildcard: pattern, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder fileNameIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'fileName', value: ''), - ); - }); - } - - QueryBuilder fileNameIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan(property: r'fileName', value: ''), - ); - }); - } - - QueryBuilder heightIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNull(property: r'height'), - ); - }); - } - - QueryBuilder heightIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNotNull(property: r'height'), - ); - }); - } - - QueryBuilder heightEqualTo(int? value) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'height', value: value), - ); - }); - } - - QueryBuilder heightGreaterThan( - int? value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'height', - value: value, - ), - ); - }); - } - - QueryBuilder heightLessThan( - int? value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'height', - value: value, - ), - ); - }); - } - - QueryBuilder heightBetween( - int? lower, - int? upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'height', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder idEqualTo(Id value) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'id', value: value), - ); - }); - } - - QueryBuilder idGreaterThan( - Id value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'id', - value: value, - ), - ); - }); - } - - QueryBuilder idLessThan( - Id value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'id', - value: value, - ), - ); - }); - } - - QueryBuilder idBetween( - Id lower, - Id upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'id', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder isArchivedEqualTo( - bool value, - ) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'isArchived', value: value), - ); - }); - } - - QueryBuilder isFavoriteEqualTo( - bool value, - ) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'isFavorite', value: value), - ); - }); - } - - QueryBuilder isOfflineEqualTo( - bool value, - ) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'isOffline', value: value), - ); - }); - } - - QueryBuilder isTrashedEqualTo( - bool value, - ) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'isTrashed', value: value), - ); - }); - } - - QueryBuilder livePhotoVideoIdIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNull(property: r'livePhotoVideoId'), - ); - }); - } - - QueryBuilder - livePhotoVideoIdIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNotNull(property: r'livePhotoVideoId'), - ); - }); - } - - QueryBuilder livePhotoVideoIdEqualTo( - String? value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'livePhotoVideoId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder livePhotoVideoIdGreaterThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'livePhotoVideoId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder livePhotoVideoIdLessThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'livePhotoVideoId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder livePhotoVideoIdBetween( - String? lower, - String? upper, { - bool includeLower = true, - bool includeUpper = true, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'livePhotoVideoId', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder livePhotoVideoIdStartsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.startsWith( - property: r'livePhotoVideoId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder livePhotoVideoIdEndsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.endsWith( - property: r'livePhotoVideoId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder livePhotoVideoIdContains( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.contains( - property: r'livePhotoVideoId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder livePhotoVideoIdMatches( - String pattern, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.matches( - property: r'livePhotoVideoId', - wildcard: pattern, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder livePhotoVideoIdIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'livePhotoVideoId', value: ''), - ); - }); - } - - QueryBuilder - livePhotoVideoIdIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan(property: r'livePhotoVideoId', value: ''), - ); - }); - } - - QueryBuilder localIdIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNull(property: r'localId'), - ); - }); - } - - QueryBuilder localIdIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNotNull(property: r'localId'), - ); - }); - } - - QueryBuilder localIdEqualTo( - String? value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'localId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder localIdGreaterThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'localId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder localIdLessThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'localId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder localIdBetween( - String? lower, - String? upper, { - bool includeLower = true, - bool includeUpper = true, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'localId', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder localIdStartsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.startsWith( - property: r'localId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder localIdEndsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.endsWith( - property: r'localId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder localIdContains( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.contains( - property: r'localId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder localIdMatches( - String pattern, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.matches( - property: r'localId', - wildcard: pattern, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder localIdIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'localId', value: ''), - ); - }); - } - - QueryBuilder localIdIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan(property: r'localId', value: ''), - ); - }); - } - - QueryBuilder ownerIdEqualTo(int value) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'ownerId', value: value), - ); - }); - } - - QueryBuilder ownerIdGreaterThan( - int value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'ownerId', - value: value, - ), - ); - }); - } - - QueryBuilder ownerIdLessThan( - int value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'ownerId', - value: value, - ), - ); - }); - } - - QueryBuilder ownerIdBetween( - int lower, - int upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'ownerId', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder remoteIdIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNull(property: r'remoteId'), - ); - }); - } - - QueryBuilder remoteIdIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNotNull(property: r'remoteId'), - ); - }); - } - - QueryBuilder remoteIdEqualTo( - String? value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'remoteId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder remoteIdGreaterThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'remoteId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder remoteIdLessThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'remoteId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder remoteIdBetween( - String? lower, - String? upper, { - bool includeLower = true, - bool includeUpper = true, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'remoteId', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder remoteIdStartsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.startsWith( - property: r'remoteId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder remoteIdEndsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.endsWith( - property: r'remoteId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder remoteIdContains( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.contains( - property: r'remoteId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder remoteIdMatches( - String pattern, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.matches( - property: r'remoteId', - wildcard: pattern, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder remoteIdIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'remoteId', value: ''), - ); - }); - } - - QueryBuilder remoteIdIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan(property: r'remoteId', value: ''), - ); - }); - } - - QueryBuilder stackCountEqualTo( - int value, - ) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'stackCount', value: value), - ); - }); - } - - QueryBuilder stackCountGreaterThan( - int value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'stackCount', - value: value, - ), - ); - }); - } - - QueryBuilder stackCountLessThan( - int value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'stackCount', - value: value, - ), - ); - }); - } - - QueryBuilder stackCountBetween( - int lower, - int upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'stackCount', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder stackIdIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNull(property: r'stackId'), - ); - }); - } - - QueryBuilder stackIdIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNotNull(property: r'stackId'), - ); - }); - } - - QueryBuilder stackIdEqualTo( - String? value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'stackId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder stackIdGreaterThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'stackId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder stackIdLessThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'stackId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder stackIdBetween( - String? lower, - String? upper, { - bool includeLower = true, - bool includeUpper = true, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'stackId', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder stackIdStartsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.startsWith( - property: r'stackId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder stackIdEndsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.endsWith( - property: r'stackId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder stackIdContains( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.contains( - property: r'stackId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder stackIdMatches( - String pattern, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.matches( - property: r'stackId', - wildcard: pattern, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder stackIdIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'stackId', value: ''), - ); - }); - } - - QueryBuilder stackIdIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan(property: r'stackId', value: ''), - ); - }); - } - - QueryBuilder - stackPrimaryAssetIdIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNull(property: r'stackPrimaryAssetId'), - ); - }); - } - - QueryBuilder - stackPrimaryAssetIdIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNotNull(property: r'stackPrimaryAssetId'), - ); - }); - } - - QueryBuilder stackPrimaryAssetIdEqualTo( - String? value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'stackPrimaryAssetId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - stackPrimaryAssetIdGreaterThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'stackPrimaryAssetId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder stackPrimaryAssetIdLessThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'stackPrimaryAssetId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder stackPrimaryAssetIdBetween( - String? lower, - String? upper, { - bool includeLower = true, - bool includeUpper = true, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'stackPrimaryAssetId', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - stackPrimaryAssetIdStartsWith(String value, {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.startsWith( - property: r'stackPrimaryAssetId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder stackPrimaryAssetIdEndsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.endsWith( - property: r'stackPrimaryAssetId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder stackPrimaryAssetIdContains( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.contains( - property: r'stackPrimaryAssetId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder stackPrimaryAssetIdMatches( - String pattern, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.matches( - property: r'stackPrimaryAssetId', - wildcard: pattern, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - stackPrimaryAssetIdIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'stackPrimaryAssetId', value: ''), - ); - }); - } - - QueryBuilder - stackPrimaryAssetIdIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - property: r'stackPrimaryAssetId', - value: '', - ), - ); - }); - } - - QueryBuilder thumbhashIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNull(property: r'thumbhash'), - ); - }); - } - - QueryBuilder thumbhashIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNotNull(property: r'thumbhash'), - ); - }); - } - - QueryBuilder thumbhashEqualTo( - String? value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'thumbhash', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder thumbhashGreaterThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'thumbhash', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder thumbhashLessThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'thumbhash', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder thumbhashBetween( - String? lower, - String? upper, { - bool includeLower = true, - bool includeUpper = true, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'thumbhash', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder thumbhashStartsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.startsWith( - property: r'thumbhash', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder thumbhashEndsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.endsWith( - property: r'thumbhash', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder thumbhashContains( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.contains( - property: r'thumbhash', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder thumbhashMatches( - String pattern, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.matches( - property: r'thumbhash', - wildcard: pattern, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder thumbhashIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'thumbhash', value: ''), - ); - }); - } - - QueryBuilder thumbhashIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan(property: r'thumbhash', value: ''), - ); - }); - } - - QueryBuilder typeEqualTo( - AssetType value, - ) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'type', value: value), - ); - }); - } - - QueryBuilder typeGreaterThan( - AssetType value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'type', - value: value, - ), - ); - }); - } - - QueryBuilder typeLessThan( - AssetType value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'type', - value: value, - ), - ); - }); - } - - QueryBuilder typeBetween( - AssetType lower, - AssetType upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'type', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder updatedAtEqualTo( - DateTime value, - ) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'updatedAt', value: value), - ); - }); - } - - QueryBuilder updatedAtGreaterThan( - DateTime value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'updatedAt', - value: value, - ), - ); - }); - } - - QueryBuilder updatedAtLessThan( - DateTime value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'updatedAt', - value: value, - ), - ); - }); - } - - QueryBuilder updatedAtBetween( - DateTime lower, - DateTime upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'updatedAt', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder visibilityEqualTo( - AssetVisibilityEnum value, - ) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'visibility', value: value), - ); - }); - } - - QueryBuilder visibilityGreaterThan( - AssetVisibilityEnum value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'visibility', - value: value, - ), - ); - }); - } - - QueryBuilder visibilityLessThan( - AssetVisibilityEnum value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'visibility', - value: value, - ), - ); - }); - } - - QueryBuilder visibilityBetween( - AssetVisibilityEnum lower, - AssetVisibilityEnum upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'visibility', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder widthIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNull(property: r'width'), - ); - }); - } - - QueryBuilder widthIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNotNull(property: r'width'), - ); - }); - } - - QueryBuilder widthEqualTo(int? value) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'width', value: value), - ); - }); - } - - QueryBuilder widthGreaterThan( - int? value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'width', - value: value, - ), - ); - }); - } - - QueryBuilder widthLessThan( - int? value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'width', - value: value, - ), - ); - }); - } - - QueryBuilder widthBetween( - int? lower, - int? upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'width', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } -} - -extension AssetQueryObject on QueryBuilder {} - -extension AssetQueryLinks on QueryBuilder {} - -extension AssetQuerySortBy on QueryBuilder { - QueryBuilder sortByChecksum() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'checksum', Sort.asc); - }); - } - - QueryBuilder sortByChecksumDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'checksum', Sort.desc); - }); - } - - QueryBuilder sortByDurationInSeconds() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'durationInSeconds', Sort.asc); - }); - } - - QueryBuilder sortByDurationInSecondsDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'durationInSeconds', Sort.desc); - }); - } - - QueryBuilder sortByFileCreatedAt() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'fileCreatedAt', Sort.asc); - }); - } - - QueryBuilder sortByFileCreatedAtDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'fileCreatedAt', Sort.desc); - }); - } - - QueryBuilder sortByFileModifiedAt() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'fileModifiedAt', Sort.asc); - }); - } - - QueryBuilder sortByFileModifiedAtDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'fileModifiedAt', Sort.desc); - }); - } - - QueryBuilder sortByFileName() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'fileName', Sort.asc); - }); - } - - QueryBuilder sortByFileNameDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'fileName', Sort.desc); - }); - } - - QueryBuilder sortByHeight() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'height', Sort.asc); - }); - } - - QueryBuilder sortByHeightDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'height', Sort.desc); - }); - } - - QueryBuilder sortByIsArchived() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isArchived', Sort.asc); - }); - } - - QueryBuilder sortByIsArchivedDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isArchived', Sort.desc); - }); - } - - QueryBuilder sortByIsFavorite() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isFavorite', Sort.asc); - }); - } - - QueryBuilder sortByIsFavoriteDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isFavorite', Sort.desc); - }); - } - - QueryBuilder sortByIsOffline() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isOffline', Sort.asc); - }); - } - - QueryBuilder sortByIsOfflineDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isOffline', Sort.desc); - }); - } - - QueryBuilder sortByIsTrashed() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isTrashed', Sort.asc); - }); - } - - QueryBuilder sortByIsTrashedDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isTrashed', Sort.desc); - }); - } - - QueryBuilder sortByLivePhotoVideoId() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'livePhotoVideoId', Sort.asc); - }); - } - - QueryBuilder sortByLivePhotoVideoIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'livePhotoVideoId', Sort.desc); - }); - } - - QueryBuilder sortByLocalId() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'localId', Sort.asc); - }); - } - - QueryBuilder sortByLocalIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'localId', Sort.desc); - }); - } - - QueryBuilder sortByOwnerId() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'ownerId', Sort.asc); - }); - } - - QueryBuilder sortByOwnerIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'ownerId', Sort.desc); - }); - } - - QueryBuilder sortByRemoteId() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'remoteId', Sort.asc); - }); - } - - QueryBuilder sortByRemoteIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'remoteId', Sort.desc); - }); - } - - QueryBuilder sortByStackCount() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'stackCount', Sort.asc); - }); - } - - QueryBuilder sortByStackCountDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'stackCount', Sort.desc); - }); - } - - QueryBuilder sortByStackId() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'stackId', Sort.asc); - }); - } - - QueryBuilder sortByStackIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'stackId', Sort.desc); - }); - } - - QueryBuilder sortByStackPrimaryAssetId() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'stackPrimaryAssetId', Sort.asc); - }); - } - - QueryBuilder sortByStackPrimaryAssetIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'stackPrimaryAssetId', Sort.desc); - }); - } - - QueryBuilder sortByThumbhash() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'thumbhash', Sort.asc); - }); - } - - QueryBuilder sortByThumbhashDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'thumbhash', Sort.desc); - }); - } - - QueryBuilder sortByType() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'type', Sort.asc); - }); - } - - QueryBuilder sortByTypeDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'type', Sort.desc); - }); - } - - QueryBuilder sortByUpdatedAt() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'updatedAt', Sort.asc); - }); - } - - QueryBuilder sortByUpdatedAtDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'updatedAt', Sort.desc); - }); - } - - QueryBuilder sortByVisibility() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'visibility', Sort.asc); - }); - } - - QueryBuilder sortByVisibilityDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'visibility', Sort.desc); - }); - } - - QueryBuilder sortByWidth() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'width', Sort.asc); - }); - } - - QueryBuilder sortByWidthDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'width', Sort.desc); - }); - } -} - -extension AssetQuerySortThenBy on QueryBuilder { - QueryBuilder thenByChecksum() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'checksum', Sort.asc); - }); - } - - QueryBuilder thenByChecksumDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'checksum', Sort.desc); - }); - } - - QueryBuilder thenByDurationInSeconds() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'durationInSeconds', Sort.asc); - }); - } - - QueryBuilder thenByDurationInSecondsDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'durationInSeconds', Sort.desc); - }); - } - - QueryBuilder thenByFileCreatedAt() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'fileCreatedAt', Sort.asc); - }); - } - - QueryBuilder thenByFileCreatedAtDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'fileCreatedAt', Sort.desc); - }); - } - - QueryBuilder thenByFileModifiedAt() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'fileModifiedAt', Sort.asc); - }); - } - - QueryBuilder thenByFileModifiedAtDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'fileModifiedAt', Sort.desc); - }); - } - - QueryBuilder thenByFileName() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'fileName', Sort.asc); - }); - } - - QueryBuilder thenByFileNameDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'fileName', Sort.desc); - }); - } - - QueryBuilder thenByHeight() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'height', Sort.asc); - }); - } - - QueryBuilder thenByHeightDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'height', Sort.desc); - }); - } - - QueryBuilder thenById() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'id', Sort.asc); - }); - } - - QueryBuilder thenByIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'id', Sort.desc); - }); - } - - QueryBuilder thenByIsArchived() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isArchived', Sort.asc); - }); - } - - QueryBuilder thenByIsArchivedDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isArchived', Sort.desc); - }); - } - - QueryBuilder thenByIsFavorite() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isFavorite', Sort.asc); - }); - } - - QueryBuilder thenByIsFavoriteDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isFavorite', Sort.desc); - }); - } - - QueryBuilder thenByIsOffline() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isOffline', Sort.asc); - }); - } - - QueryBuilder thenByIsOfflineDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isOffline', Sort.desc); - }); - } - - QueryBuilder thenByIsTrashed() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isTrashed', Sort.asc); - }); - } - - QueryBuilder thenByIsTrashedDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isTrashed', Sort.desc); - }); - } - - QueryBuilder thenByLivePhotoVideoId() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'livePhotoVideoId', Sort.asc); - }); - } - - QueryBuilder thenByLivePhotoVideoIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'livePhotoVideoId', Sort.desc); - }); - } - - QueryBuilder thenByLocalId() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'localId', Sort.asc); - }); - } - - QueryBuilder thenByLocalIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'localId', Sort.desc); - }); - } - - QueryBuilder thenByOwnerId() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'ownerId', Sort.asc); - }); - } - - QueryBuilder thenByOwnerIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'ownerId', Sort.desc); - }); - } - - QueryBuilder thenByRemoteId() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'remoteId', Sort.asc); - }); - } - - QueryBuilder thenByRemoteIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'remoteId', Sort.desc); - }); - } - - QueryBuilder thenByStackCount() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'stackCount', Sort.asc); - }); - } - - QueryBuilder thenByStackCountDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'stackCount', Sort.desc); - }); - } - - QueryBuilder thenByStackId() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'stackId', Sort.asc); - }); - } - - QueryBuilder thenByStackIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'stackId', Sort.desc); - }); - } - - QueryBuilder thenByStackPrimaryAssetId() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'stackPrimaryAssetId', Sort.asc); - }); - } - - QueryBuilder thenByStackPrimaryAssetIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'stackPrimaryAssetId', Sort.desc); - }); - } - - QueryBuilder thenByThumbhash() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'thumbhash', Sort.asc); - }); - } - - QueryBuilder thenByThumbhashDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'thumbhash', Sort.desc); - }); - } - - QueryBuilder thenByType() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'type', Sort.asc); - }); - } - - QueryBuilder thenByTypeDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'type', Sort.desc); - }); - } - - QueryBuilder thenByUpdatedAt() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'updatedAt', Sort.asc); - }); - } - - QueryBuilder thenByUpdatedAtDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'updatedAt', Sort.desc); - }); - } - - QueryBuilder thenByVisibility() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'visibility', Sort.asc); - }); - } - - QueryBuilder thenByVisibilityDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'visibility', Sort.desc); - }); - } - - QueryBuilder thenByWidth() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'width', Sort.asc); - }); - } - - QueryBuilder thenByWidthDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'width', Sort.desc); - }); - } -} - -extension AssetQueryWhereDistinct on QueryBuilder { - QueryBuilder distinctByChecksum({ - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'checksum', caseSensitive: caseSensitive); - }); - } - - QueryBuilder distinctByDurationInSeconds() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'durationInSeconds'); - }); - } - - QueryBuilder distinctByFileCreatedAt() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'fileCreatedAt'); - }); - } - - QueryBuilder distinctByFileModifiedAt() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'fileModifiedAt'); - }); - } - - QueryBuilder distinctByFileName({ - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'fileName', caseSensitive: caseSensitive); - }); - } - - QueryBuilder distinctByHeight() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'height'); - }); - } - - QueryBuilder distinctByIsArchived() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'isArchived'); - }); - } - - QueryBuilder distinctByIsFavorite() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'isFavorite'); - }); - } - - QueryBuilder distinctByIsOffline() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'isOffline'); - }); - } - - QueryBuilder distinctByIsTrashed() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'isTrashed'); - }); - } - - QueryBuilder distinctByLivePhotoVideoId({ - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy( - r'livePhotoVideoId', - caseSensitive: caseSensitive, - ); - }); - } - - QueryBuilder distinctByLocalId({ - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'localId', caseSensitive: caseSensitive); - }); - } - - QueryBuilder distinctByOwnerId() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'ownerId'); - }); - } - - QueryBuilder distinctByRemoteId({ - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'remoteId', caseSensitive: caseSensitive); - }); - } - - QueryBuilder distinctByStackCount() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'stackCount'); - }); - } - - QueryBuilder distinctByStackId({ - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'stackId', caseSensitive: caseSensitive); - }); - } - - QueryBuilder distinctByStackPrimaryAssetId({ - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy( - r'stackPrimaryAssetId', - caseSensitive: caseSensitive, - ); - }); - } - - QueryBuilder distinctByThumbhash({ - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'thumbhash', caseSensitive: caseSensitive); - }); - } - - QueryBuilder distinctByType() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'type'); - }); - } - - QueryBuilder distinctByUpdatedAt() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'updatedAt'); - }); - } - - QueryBuilder distinctByVisibility() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'visibility'); - }); - } - - QueryBuilder distinctByWidth() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'width'); - }); - } -} - -extension AssetQueryProperty on QueryBuilder { - QueryBuilder idProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'id'); - }); - } - - QueryBuilder checksumProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'checksum'); - }); - } - - QueryBuilder durationInSecondsProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'durationInSeconds'); - }); - } - - QueryBuilder fileCreatedAtProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'fileCreatedAt'); - }); - } - - QueryBuilder fileModifiedAtProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'fileModifiedAt'); - }); - } - - QueryBuilder fileNameProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'fileName'); - }); - } - - QueryBuilder heightProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'height'); - }); - } - - QueryBuilder isArchivedProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'isArchived'); - }); - } - - QueryBuilder isFavoriteProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'isFavorite'); - }); - } - - QueryBuilder isOfflineProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'isOffline'); - }); - } - - QueryBuilder isTrashedProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'isTrashed'); - }); - } - - QueryBuilder livePhotoVideoIdProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'livePhotoVideoId'); - }); - } - - QueryBuilder localIdProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'localId'); - }); - } - - QueryBuilder ownerIdProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'ownerId'); - }); - } - - QueryBuilder remoteIdProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'remoteId'); - }); - } - - QueryBuilder stackCountProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'stackCount'); - }); - } - - QueryBuilder stackIdProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'stackId'); - }); - } - - QueryBuilder stackPrimaryAssetIdProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'stackPrimaryAssetId'); - }); - } - - QueryBuilder thumbhashProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'thumbhash'); - }); - } - - QueryBuilder typeProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'type'); - }); - } - - QueryBuilder updatedAtProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'updatedAt'); - }); - } - - QueryBuilder - visibilityProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'visibility'); - }); - } - - QueryBuilder widthProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'width'); - }); - } -} diff --git a/mobile/lib/entities/backup_album.entity.dart b/mobile/lib/entities/backup_album.entity.dart deleted file mode 100644 index ad2a5d6718..0000000000 --- a/mobile/lib/entities/backup_album.entity.dart +++ /dev/null @@ -1,22 +0,0 @@ -import 'package:immich_mobile/utils/hash.dart'; -import 'package:isar/isar.dart'; - -part 'backup_album.entity.g.dart'; - -@Collection(inheritance: false) -class BackupAlbum { - String id; - DateTime lastBackup; - @Enumerated(EnumType.ordinal) - BackupSelection selection; - - BackupAlbum(this.id, this.lastBackup, this.selection); - - Id get isarId => fastHash(id); - - BackupAlbum copyWith({String? id, DateTime? lastBackup, BackupSelection? selection}) { - return BackupAlbum(id ?? this.id, lastBackup ?? this.lastBackup, selection ?? this.selection); - } -} - -enum BackupSelection { none, select, exclude } diff --git a/mobile/lib/entities/backup_album.entity.g.dart b/mobile/lib/entities/backup_album.entity.g.dart deleted file mode 100644 index 583aa55c4d..0000000000 --- a/mobile/lib/entities/backup_album.entity.g.dart +++ /dev/null @@ -1,679 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'backup_album.entity.dart'; - -// ************************************************************************** -// IsarCollectionGenerator -// ************************************************************************** - -// coverage:ignore-file -// ignore_for_file: duplicate_ignore, non_constant_identifier_names, constant_identifier_names, invalid_use_of_protected_member, unnecessary_cast, prefer_const_constructors, lines_longer_than_80_chars, require_trailing_commas, inference_failure_on_function_invocation, unnecessary_parenthesis, unnecessary_raw_strings, unnecessary_null_checks, join_return_with_assignment, prefer_final_locals, avoid_js_rounded_ints, avoid_positional_boolean_parameters, always_specify_types - -extension GetBackupAlbumCollection on Isar { - IsarCollection get backupAlbums => this.collection(); -} - -const BackupAlbumSchema = CollectionSchema( - name: r'BackupAlbum', - id: 8308487201128361847, - properties: { - r'id': PropertySchema(id: 0, name: r'id', type: IsarType.string), - r'lastBackup': PropertySchema( - id: 1, - name: r'lastBackup', - type: IsarType.dateTime, - ), - r'selection': PropertySchema( - id: 2, - name: r'selection', - type: IsarType.byte, - enumMap: _BackupAlbumselectionEnumValueMap, - ), - }, - - estimateSize: _backupAlbumEstimateSize, - serialize: _backupAlbumSerialize, - deserialize: _backupAlbumDeserialize, - deserializeProp: _backupAlbumDeserializeProp, - idName: r'isarId', - indexes: {}, - links: {}, - embeddedSchemas: {}, - - getId: _backupAlbumGetId, - getLinks: _backupAlbumGetLinks, - attach: _backupAlbumAttach, - version: '3.3.0-dev.3', -); - -int _backupAlbumEstimateSize( - BackupAlbum object, - List offsets, - Map> allOffsets, -) { - var bytesCount = offsets.last; - bytesCount += 3 + object.id.length * 3; - return bytesCount; -} - -void _backupAlbumSerialize( - BackupAlbum object, - IsarWriter writer, - List offsets, - Map> allOffsets, -) { - writer.writeString(offsets[0], object.id); - writer.writeDateTime(offsets[1], object.lastBackup); - writer.writeByte(offsets[2], object.selection.index); -} - -BackupAlbum _backupAlbumDeserialize( - Id id, - IsarReader reader, - List offsets, - Map> allOffsets, -) { - final object = BackupAlbum( - reader.readString(offsets[0]), - reader.readDateTime(offsets[1]), - _BackupAlbumselectionValueEnumMap[reader.readByteOrNull(offsets[2])] ?? - BackupSelection.none, - ); - return object; -} - -P _backupAlbumDeserializeProp

( - IsarReader reader, - int propertyId, - int offset, - Map> allOffsets, -) { - switch (propertyId) { - case 0: - return (reader.readString(offset)) as P; - case 1: - return (reader.readDateTime(offset)) as P; - case 2: - return (_BackupAlbumselectionValueEnumMap[reader.readByteOrNull( - offset, - )] ?? - BackupSelection.none) - as P; - default: - throw IsarError('Unknown property with id $propertyId'); - } -} - -const _BackupAlbumselectionEnumValueMap = { - 'none': 0, - 'select': 1, - 'exclude': 2, -}; -const _BackupAlbumselectionValueEnumMap = { - 0: BackupSelection.none, - 1: BackupSelection.select, - 2: BackupSelection.exclude, -}; - -Id _backupAlbumGetId(BackupAlbum object) { - return object.isarId; -} - -List> _backupAlbumGetLinks(BackupAlbum object) { - return []; -} - -void _backupAlbumAttach( - IsarCollection col, - Id id, - BackupAlbum object, -) {} - -extension BackupAlbumQueryWhereSort - on QueryBuilder { - QueryBuilder anyIsarId() { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause(const IdWhereClause.any()); - }); - } -} - -extension BackupAlbumQueryWhere - on QueryBuilder { - QueryBuilder isarIdEqualTo( - Id isarId, - ) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.between(lower: isarId, upper: isarId), - ); - }); - } - - QueryBuilder isarIdNotEqualTo( - Id isarId, - ) { - return QueryBuilder.apply(this, (query) { - if (query.whereSort == Sort.asc) { - return query - .addWhereClause( - IdWhereClause.lessThan(upper: isarId, includeUpper: false), - ) - .addWhereClause( - IdWhereClause.greaterThan(lower: isarId, includeLower: false), - ); - } else { - return query - .addWhereClause( - IdWhereClause.greaterThan(lower: isarId, includeLower: false), - ) - .addWhereClause( - IdWhereClause.lessThan(upper: isarId, includeUpper: false), - ); - } - }); - } - - QueryBuilder isarIdGreaterThan( - Id isarId, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.greaterThan(lower: isarId, includeLower: include), - ); - }); - } - - QueryBuilder isarIdLessThan( - Id isarId, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.lessThan(upper: isarId, includeUpper: include), - ); - }); - } - - QueryBuilder isarIdBetween( - Id lowerIsarId, - Id upperIsarId, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.between( - lower: lowerIsarId, - includeLower: includeLower, - upper: upperIsarId, - includeUpper: includeUpper, - ), - ); - }); - } -} - -extension BackupAlbumQueryFilter - on QueryBuilder { - QueryBuilder idEqualTo( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'id', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder idGreaterThan( - String value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'id', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder idLessThan( - String value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'id', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder idBetween( - String lower, - String upper, { - bool includeLower = true, - bool includeUpper = true, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'id', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder idStartsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.startsWith( - property: r'id', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder idEndsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.endsWith( - property: r'id', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder idContains( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.contains( - property: r'id', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder idMatches( - String pattern, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.matches( - property: r'id', - wildcard: pattern, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder idIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'id', value: ''), - ); - }); - } - - QueryBuilder idIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan(property: r'id', value: ''), - ); - }); - } - - QueryBuilder isarIdEqualTo( - Id value, - ) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'isarId', value: value), - ); - }); - } - - QueryBuilder - isarIdGreaterThan(Id value, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'isarId', - value: value, - ), - ); - }); - } - - QueryBuilder isarIdLessThan( - Id value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'isarId', - value: value, - ), - ); - }); - } - - QueryBuilder isarIdBetween( - Id lower, - Id upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'isarId', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder - lastBackupEqualTo(DateTime value) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'lastBackup', value: value), - ); - }); - } - - QueryBuilder - lastBackupGreaterThan(DateTime value, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'lastBackup', - value: value, - ), - ); - }); - } - - QueryBuilder - lastBackupLessThan(DateTime value, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'lastBackup', - value: value, - ), - ); - }); - } - - QueryBuilder - lastBackupBetween( - DateTime lower, - DateTime upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'lastBackup', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder - selectionEqualTo(BackupSelection value) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'selection', value: value), - ); - }); - } - - QueryBuilder - selectionGreaterThan(BackupSelection value, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'selection', - value: value, - ), - ); - }); - } - - QueryBuilder - selectionLessThan(BackupSelection value, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'selection', - value: value, - ), - ); - }); - } - - QueryBuilder - selectionBetween( - BackupSelection lower, - BackupSelection upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'selection', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } -} - -extension BackupAlbumQueryObject - on QueryBuilder {} - -extension BackupAlbumQueryLinks - on QueryBuilder {} - -extension BackupAlbumQuerySortBy - on QueryBuilder { - QueryBuilder sortById() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'id', Sort.asc); - }); - } - - QueryBuilder sortByIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'id', Sort.desc); - }); - } - - QueryBuilder sortByLastBackup() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'lastBackup', Sort.asc); - }); - } - - QueryBuilder sortByLastBackupDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'lastBackup', Sort.desc); - }); - } - - QueryBuilder sortBySelection() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'selection', Sort.asc); - }); - } - - QueryBuilder sortBySelectionDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'selection', Sort.desc); - }); - } -} - -extension BackupAlbumQuerySortThenBy - on QueryBuilder { - QueryBuilder thenById() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'id', Sort.asc); - }); - } - - QueryBuilder thenByIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'id', Sort.desc); - }); - } - - QueryBuilder thenByIsarId() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isarId', Sort.asc); - }); - } - - QueryBuilder thenByIsarIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isarId', Sort.desc); - }); - } - - QueryBuilder thenByLastBackup() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'lastBackup', Sort.asc); - }); - } - - QueryBuilder thenByLastBackupDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'lastBackup', Sort.desc); - }); - } - - QueryBuilder thenBySelection() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'selection', Sort.asc); - }); - } - - QueryBuilder thenBySelectionDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'selection', Sort.desc); - }); - } -} - -extension BackupAlbumQueryWhereDistinct - on QueryBuilder { - QueryBuilder distinctById({ - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'id', caseSensitive: caseSensitive); - }); - } - - QueryBuilder distinctByLastBackup() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'lastBackup'); - }); - } - - QueryBuilder distinctBySelection() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'selection'); - }); - } -} - -extension BackupAlbumQueryProperty - on QueryBuilder { - QueryBuilder isarIdProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'isarId'); - }); - } - - QueryBuilder idProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'id'); - }); - } - - QueryBuilder lastBackupProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'lastBackup'); - }); - } - - QueryBuilder - selectionProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'selection'); - }); - } -} diff --git a/mobile/lib/entities/device_asset.entity.dart b/mobile/lib/entities/device_asset.entity.dart deleted file mode 100644 index 0973dd4ff8..0000000000 --- a/mobile/lib/entities/device_asset.entity.dart +++ /dev/null @@ -1,8 +0,0 @@ -import 'package:isar/isar.dart'; - -class DeviceAsset { - DeviceAsset({required this.hash}); - - @Index(unique: false, type: IndexType.hash) - List hash; -} diff --git a/mobile/lib/entities/duplicated_asset.entity.dart b/mobile/lib/entities/duplicated_asset.entity.dart deleted file mode 100644 index 9368dc1a52..0000000000 --- a/mobile/lib/entities/duplicated_asset.entity.dart +++ /dev/null @@ -1,11 +0,0 @@ -import 'package:immich_mobile/utils/hash.dart'; -import 'package:isar/isar.dart'; - -part 'duplicated_asset.entity.g.dart'; - -@Collection(inheritance: false) -class DuplicatedAsset { - String id; - DuplicatedAsset(this.id); - Id get isarId => fastHash(id); -} diff --git a/mobile/lib/entities/duplicated_asset.entity.g.dart b/mobile/lib/entities/duplicated_asset.entity.g.dart deleted file mode 100644 index 80d2f344e6..0000000000 --- a/mobile/lib/entities/duplicated_asset.entity.g.dart +++ /dev/null @@ -1,444 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'duplicated_asset.entity.dart'; - -// ************************************************************************** -// IsarCollectionGenerator -// ************************************************************************** - -// coverage:ignore-file -// ignore_for_file: duplicate_ignore, non_constant_identifier_names, constant_identifier_names, invalid_use_of_protected_member, unnecessary_cast, prefer_const_constructors, lines_longer_than_80_chars, require_trailing_commas, inference_failure_on_function_invocation, unnecessary_parenthesis, unnecessary_raw_strings, unnecessary_null_checks, join_return_with_assignment, prefer_final_locals, avoid_js_rounded_ints, avoid_positional_boolean_parameters, always_specify_types - -extension GetDuplicatedAssetCollection on Isar { - IsarCollection get duplicatedAssets => this.collection(); -} - -const DuplicatedAssetSchema = CollectionSchema( - name: r'DuplicatedAsset', - id: -2679334728174694496, - properties: { - r'id': PropertySchema(id: 0, name: r'id', type: IsarType.string), - }, - - estimateSize: _duplicatedAssetEstimateSize, - serialize: _duplicatedAssetSerialize, - deserialize: _duplicatedAssetDeserialize, - deserializeProp: _duplicatedAssetDeserializeProp, - idName: r'isarId', - indexes: {}, - links: {}, - embeddedSchemas: {}, - - getId: _duplicatedAssetGetId, - getLinks: _duplicatedAssetGetLinks, - attach: _duplicatedAssetAttach, - version: '3.3.0-dev.3', -); - -int _duplicatedAssetEstimateSize( - DuplicatedAsset object, - List offsets, - Map> allOffsets, -) { - var bytesCount = offsets.last; - bytesCount += 3 + object.id.length * 3; - return bytesCount; -} - -void _duplicatedAssetSerialize( - DuplicatedAsset object, - IsarWriter writer, - List offsets, - Map> allOffsets, -) { - writer.writeString(offsets[0], object.id); -} - -DuplicatedAsset _duplicatedAssetDeserialize( - Id id, - IsarReader reader, - List offsets, - Map> allOffsets, -) { - final object = DuplicatedAsset(reader.readString(offsets[0])); - return object; -} - -P _duplicatedAssetDeserializeProp

( - IsarReader reader, - int propertyId, - int offset, - Map> allOffsets, -) { - switch (propertyId) { - case 0: - return (reader.readString(offset)) as P; - default: - throw IsarError('Unknown property with id $propertyId'); - } -} - -Id _duplicatedAssetGetId(DuplicatedAsset object) { - return object.isarId; -} - -List> _duplicatedAssetGetLinks(DuplicatedAsset object) { - return []; -} - -void _duplicatedAssetAttach( - IsarCollection col, - Id id, - DuplicatedAsset object, -) {} - -extension DuplicatedAssetQueryWhereSort - on QueryBuilder { - QueryBuilder anyIsarId() { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause(const IdWhereClause.any()); - }); - } -} - -extension DuplicatedAssetQueryWhere - on QueryBuilder { - QueryBuilder - isarIdEqualTo(Id isarId) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.between(lower: isarId, upper: isarId), - ); - }); - } - - QueryBuilder - isarIdNotEqualTo(Id isarId) { - return QueryBuilder.apply(this, (query) { - if (query.whereSort == Sort.asc) { - return query - .addWhereClause( - IdWhereClause.lessThan(upper: isarId, includeUpper: false), - ) - .addWhereClause( - IdWhereClause.greaterThan(lower: isarId, includeLower: false), - ); - } else { - return query - .addWhereClause( - IdWhereClause.greaterThan(lower: isarId, includeLower: false), - ) - .addWhereClause( - IdWhereClause.lessThan(upper: isarId, includeUpper: false), - ); - } - }); - } - - QueryBuilder - isarIdGreaterThan(Id isarId, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.greaterThan(lower: isarId, includeLower: include), - ); - }); - } - - QueryBuilder - isarIdLessThan(Id isarId, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.lessThan(upper: isarId, includeUpper: include), - ); - }); - } - - QueryBuilder - isarIdBetween( - Id lowerIsarId, - Id upperIsarId, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.between( - lower: lowerIsarId, - includeLower: includeLower, - upper: upperIsarId, - includeUpper: includeUpper, - ), - ); - }); - } -} - -extension DuplicatedAssetQueryFilter - on QueryBuilder { - QueryBuilder - idEqualTo(String value, {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'id', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - idGreaterThan( - String value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'id', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - idLessThan(String value, {bool include = false, bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'id', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - idBetween( - String lower, - String upper, { - bool includeLower = true, - bool includeUpper = true, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'id', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - idStartsWith(String value, {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.startsWith( - property: r'id', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - idEndsWith(String value, {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.endsWith( - property: r'id', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - idContains(String value, {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.contains( - property: r'id', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - idMatches(String pattern, {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.matches( - property: r'id', - wildcard: pattern, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - idIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'id', value: ''), - ); - }); - } - - QueryBuilder - idIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan(property: r'id', value: ''), - ); - }); - } - - QueryBuilder - isarIdEqualTo(Id value) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'isarId', value: value), - ); - }); - } - - QueryBuilder - isarIdGreaterThan(Id value, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'isarId', - value: value, - ), - ); - }); - } - - QueryBuilder - isarIdLessThan(Id value, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'isarId', - value: value, - ), - ); - }); - } - - QueryBuilder - isarIdBetween( - Id lower, - Id upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'isarId', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } -} - -extension DuplicatedAssetQueryObject - on QueryBuilder {} - -extension DuplicatedAssetQueryLinks - on QueryBuilder {} - -extension DuplicatedAssetQuerySortBy - on QueryBuilder { - QueryBuilder sortById() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'id', Sort.asc); - }); - } - - QueryBuilder sortByIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'id', Sort.desc); - }); - } -} - -extension DuplicatedAssetQuerySortThenBy - on QueryBuilder { - QueryBuilder thenById() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'id', Sort.asc); - }); - } - - QueryBuilder thenByIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'id', Sort.desc); - }); - } - - QueryBuilder thenByIsarId() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isarId', Sort.asc); - }); - } - - QueryBuilder - thenByIsarIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isarId', Sort.desc); - }); - } -} - -extension DuplicatedAssetQueryWhereDistinct - on QueryBuilder { - QueryBuilder distinctById({ - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'id', caseSensitive: caseSensitive); - }); - } -} - -extension DuplicatedAssetQueryProperty - on QueryBuilder { - QueryBuilder isarIdProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'isarId'); - }); - } - - QueryBuilder idProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'id'); - }); - } -} diff --git a/mobile/lib/entities/etag.entity.dart b/mobile/lib/entities/etag.entity.dart deleted file mode 100644 index 3b8ef39c61..0000000000 --- a/mobile/lib/entities/etag.entity.dart +++ /dev/null @@ -1,14 +0,0 @@ -import 'package:immich_mobile/utils/hash.dart'; -import 'package:isar/isar.dart'; - -part 'etag.entity.g.dart'; - -@Collection(inheritance: false) -class ETag { - ETag({required this.id, this.assetCount, this.time}); - Id get isarId => fastHash(id); - @Index(unique: true, replace: true, type: IndexType.hash) - String id; - int? assetCount; - DateTime? time; -} diff --git a/mobile/lib/entities/etag.entity.g.dart b/mobile/lib/entities/etag.entity.g.dart deleted file mode 100644 index 03b4ea9918..0000000000 --- a/mobile/lib/entities/etag.entity.g.dart +++ /dev/null @@ -1,796 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'etag.entity.dart'; - -// ************************************************************************** -// IsarCollectionGenerator -// ************************************************************************** - -// coverage:ignore-file -// ignore_for_file: duplicate_ignore, non_constant_identifier_names, constant_identifier_names, invalid_use_of_protected_member, unnecessary_cast, prefer_const_constructors, lines_longer_than_80_chars, require_trailing_commas, inference_failure_on_function_invocation, unnecessary_parenthesis, unnecessary_raw_strings, unnecessary_null_checks, join_return_with_assignment, prefer_final_locals, avoid_js_rounded_ints, avoid_positional_boolean_parameters, always_specify_types - -extension GetETagCollection on Isar { - IsarCollection get eTags => this.collection(); -} - -const ETagSchema = CollectionSchema( - name: r'ETag', - id: -644290296585643859, - properties: { - r'assetCount': PropertySchema( - id: 0, - name: r'assetCount', - type: IsarType.long, - ), - r'id': PropertySchema(id: 1, name: r'id', type: IsarType.string), - r'time': PropertySchema(id: 2, name: r'time', type: IsarType.dateTime), - }, - - estimateSize: _eTagEstimateSize, - serialize: _eTagSerialize, - deserialize: _eTagDeserialize, - deserializeProp: _eTagDeserializeProp, - idName: r'isarId', - indexes: { - r'id': IndexSchema( - id: -3268401673993471357, - name: r'id', - unique: true, - replace: true, - properties: [ - IndexPropertySchema( - name: r'id', - type: IndexType.hash, - caseSensitive: true, - ), - ], - ), - }, - links: {}, - embeddedSchemas: {}, - - getId: _eTagGetId, - getLinks: _eTagGetLinks, - attach: _eTagAttach, - version: '3.3.0-dev.3', -); - -int _eTagEstimateSize( - ETag object, - List offsets, - Map> allOffsets, -) { - var bytesCount = offsets.last; - bytesCount += 3 + object.id.length * 3; - return bytesCount; -} - -void _eTagSerialize( - ETag object, - IsarWriter writer, - List offsets, - Map> allOffsets, -) { - writer.writeLong(offsets[0], object.assetCount); - writer.writeString(offsets[1], object.id); - writer.writeDateTime(offsets[2], object.time); -} - -ETag _eTagDeserialize( - Id id, - IsarReader reader, - List offsets, - Map> allOffsets, -) { - final object = ETag( - assetCount: reader.readLongOrNull(offsets[0]), - id: reader.readString(offsets[1]), - time: reader.readDateTimeOrNull(offsets[2]), - ); - return object; -} - -P _eTagDeserializeProp

( - IsarReader reader, - int propertyId, - int offset, - Map> allOffsets, -) { - switch (propertyId) { - case 0: - return (reader.readLongOrNull(offset)) as P; - case 1: - return (reader.readString(offset)) as P; - case 2: - return (reader.readDateTimeOrNull(offset)) as P; - default: - throw IsarError('Unknown property with id $propertyId'); - } -} - -Id _eTagGetId(ETag object) { - return object.isarId; -} - -List> _eTagGetLinks(ETag object) { - return []; -} - -void _eTagAttach(IsarCollection col, Id id, ETag object) {} - -extension ETagByIndex on IsarCollection { - Future getById(String id) { - return getByIndex(r'id', [id]); - } - - ETag? getByIdSync(String id) { - return getByIndexSync(r'id', [id]); - } - - Future deleteById(String id) { - return deleteByIndex(r'id', [id]); - } - - bool deleteByIdSync(String id) { - return deleteByIndexSync(r'id', [id]); - } - - Future> getAllById(List idValues) { - final values = idValues.map((e) => [e]).toList(); - return getAllByIndex(r'id', values); - } - - List getAllByIdSync(List idValues) { - final values = idValues.map((e) => [e]).toList(); - return getAllByIndexSync(r'id', values); - } - - Future deleteAllById(List idValues) { - final values = idValues.map((e) => [e]).toList(); - return deleteAllByIndex(r'id', values); - } - - int deleteAllByIdSync(List idValues) { - final values = idValues.map((e) => [e]).toList(); - return deleteAllByIndexSync(r'id', values); - } - - Future putById(ETag object) { - return putByIndex(r'id', object); - } - - Id putByIdSync(ETag object, {bool saveLinks = true}) { - return putByIndexSync(r'id', object, saveLinks: saveLinks); - } - - Future> putAllById(List objects) { - return putAllByIndex(r'id', objects); - } - - List putAllByIdSync(List objects, {bool saveLinks = true}) { - return putAllByIndexSync(r'id', objects, saveLinks: saveLinks); - } -} - -extension ETagQueryWhereSort on QueryBuilder { - QueryBuilder anyIsarId() { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause(const IdWhereClause.any()); - }); - } -} - -extension ETagQueryWhere on QueryBuilder { - QueryBuilder isarIdEqualTo(Id isarId) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.between(lower: isarId, upper: isarId), - ); - }); - } - - QueryBuilder isarIdNotEqualTo(Id isarId) { - return QueryBuilder.apply(this, (query) { - if (query.whereSort == Sort.asc) { - return query - .addWhereClause( - IdWhereClause.lessThan(upper: isarId, includeUpper: false), - ) - .addWhereClause( - IdWhereClause.greaterThan(lower: isarId, includeLower: false), - ); - } else { - return query - .addWhereClause( - IdWhereClause.greaterThan(lower: isarId, includeLower: false), - ) - .addWhereClause( - IdWhereClause.lessThan(upper: isarId, includeUpper: false), - ); - } - }); - } - - QueryBuilder isarIdGreaterThan( - Id isarId, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.greaterThan(lower: isarId, includeLower: include), - ); - }); - } - - QueryBuilder isarIdLessThan( - Id isarId, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.lessThan(upper: isarId, includeUpper: include), - ); - }); - } - - QueryBuilder isarIdBetween( - Id lowerIsarId, - Id upperIsarId, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.between( - lower: lowerIsarId, - includeLower: includeLower, - upper: upperIsarId, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder idEqualTo(String id) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IndexWhereClause.equalTo(indexName: r'id', value: [id]), - ); - }); - } - - QueryBuilder idNotEqualTo(String id) { - return QueryBuilder.apply(this, (query) { - if (query.whereSort == Sort.asc) { - return query - .addWhereClause( - IndexWhereClause.between( - indexName: r'id', - lower: [], - upper: [id], - includeUpper: false, - ), - ) - .addWhereClause( - IndexWhereClause.between( - indexName: r'id', - lower: [id], - includeLower: false, - upper: [], - ), - ); - } else { - return query - .addWhereClause( - IndexWhereClause.between( - indexName: r'id', - lower: [id], - includeLower: false, - upper: [], - ), - ) - .addWhereClause( - IndexWhereClause.between( - indexName: r'id', - lower: [], - upper: [id], - includeUpper: false, - ), - ); - } - }); - } -} - -extension ETagQueryFilter on QueryBuilder { - QueryBuilder assetCountIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNull(property: r'assetCount'), - ); - }); - } - - QueryBuilder assetCountIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNotNull(property: r'assetCount'), - ); - }); - } - - QueryBuilder assetCountEqualTo( - int? value, - ) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'assetCount', value: value), - ); - }); - } - - QueryBuilder assetCountGreaterThan( - int? value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'assetCount', - value: value, - ), - ); - }); - } - - QueryBuilder assetCountLessThan( - int? value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'assetCount', - value: value, - ), - ); - }); - } - - QueryBuilder assetCountBetween( - int? lower, - int? upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'assetCount', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder idEqualTo( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'id', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder idGreaterThan( - String value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'id', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder idLessThan( - String value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'id', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder idBetween( - String lower, - String upper, { - bool includeLower = true, - bool includeUpper = true, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'id', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder idStartsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.startsWith( - property: r'id', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder idEndsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.endsWith( - property: r'id', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder idContains( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.contains( - property: r'id', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder idMatches( - String pattern, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.matches( - property: r'id', - wildcard: pattern, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder idIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'id', value: ''), - ); - }); - } - - QueryBuilder idIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan(property: r'id', value: ''), - ); - }); - } - - QueryBuilder isarIdEqualTo(Id value) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'isarId', value: value), - ); - }); - } - - QueryBuilder isarIdGreaterThan( - Id value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'isarId', - value: value, - ), - ); - }); - } - - QueryBuilder isarIdLessThan( - Id value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'isarId', - value: value, - ), - ); - }); - } - - QueryBuilder isarIdBetween( - Id lower, - Id upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'isarId', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder timeIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNull(property: r'time'), - ); - }); - } - - QueryBuilder timeIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNotNull(property: r'time'), - ); - }); - } - - QueryBuilder timeEqualTo(DateTime? value) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'time', value: value), - ); - }); - } - - QueryBuilder timeGreaterThan( - DateTime? value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'time', - value: value, - ), - ); - }); - } - - QueryBuilder timeLessThan( - DateTime? value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'time', - value: value, - ), - ); - }); - } - - QueryBuilder timeBetween( - DateTime? lower, - DateTime? upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'time', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } -} - -extension ETagQueryObject on QueryBuilder {} - -extension ETagQueryLinks on QueryBuilder {} - -extension ETagQuerySortBy on QueryBuilder { - QueryBuilder sortByAssetCount() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'assetCount', Sort.asc); - }); - } - - QueryBuilder sortByAssetCountDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'assetCount', Sort.desc); - }); - } - - QueryBuilder sortById() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'id', Sort.asc); - }); - } - - QueryBuilder sortByIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'id', Sort.desc); - }); - } - - QueryBuilder sortByTime() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'time', Sort.asc); - }); - } - - QueryBuilder sortByTimeDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'time', Sort.desc); - }); - } -} - -extension ETagQuerySortThenBy on QueryBuilder { - QueryBuilder thenByAssetCount() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'assetCount', Sort.asc); - }); - } - - QueryBuilder thenByAssetCountDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'assetCount', Sort.desc); - }); - } - - QueryBuilder thenById() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'id', Sort.asc); - }); - } - - QueryBuilder thenByIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'id', Sort.desc); - }); - } - - QueryBuilder thenByIsarId() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isarId', Sort.asc); - }); - } - - QueryBuilder thenByIsarIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isarId', Sort.desc); - }); - } - - QueryBuilder thenByTime() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'time', Sort.asc); - }); - } - - QueryBuilder thenByTimeDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'time', Sort.desc); - }); - } -} - -extension ETagQueryWhereDistinct on QueryBuilder { - QueryBuilder distinctByAssetCount() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'assetCount'); - }); - } - - QueryBuilder distinctById({ - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'id', caseSensitive: caseSensitive); - }); - } - - QueryBuilder distinctByTime() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'time'); - }); - } -} - -extension ETagQueryProperty on QueryBuilder { - QueryBuilder isarIdProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'isarId'); - }); - } - - QueryBuilder assetCountProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'assetCount'); - }); - } - - QueryBuilder idProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'id'); - }); - } - - QueryBuilder timeProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'time'); - }); - } -} diff --git a/mobile/lib/entities/ios_device_asset.entity.dart b/mobile/lib/entities/ios_device_asset.entity.dart deleted file mode 100644 index dfd0a660f8..0000000000 --- a/mobile/lib/entities/ios_device_asset.entity.dart +++ /dev/null @@ -1,14 +0,0 @@ -import 'package:immich_mobile/entities/device_asset.entity.dart'; -import 'package:immich_mobile/utils/hash.dart'; -import 'package:isar/isar.dart'; - -part 'ios_device_asset.entity.g.dart'; - -@Collection() -class IOSDeviceAsset extends DeviceAsset { - IOSDeviceAsset({required this.id, required super.hash}); - - @Index(replace: true, unique: true, type: IndexType.hash) - String id; - Id get isarId => fastHash(id); -} diff --git a/mobile/lib/entities/ios_device_asset.entity.g.dart b/mobile/lib/entities/ios_device_asset.entity.g.dart deleted file mode 100644 index 252fe127bb..0000000000 --- a/mobile/lib/entities/ios_device_asset.entity.g.dart +++ /dev/null @@ -1,766 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'ios_device_asset.entity.dart'; - -// ************************************************************************** -// IsarCollectionGenerator -// ************************************************************************** - -// coverage:ignore-file -// ignore_for_file: duplicate_ignore, non_constant_identifier_names, constant_identifier_names, invalid_use_of_protected_member, unnecessary_cast, prefer_const_constructors, lines_longer_than_80_chars, require_trailing_commas, inference_failure_on_function_invocation, unnecessary_parenthesis, unnecessary_raw_strings, unnecessary_null_checks, join_return_with_assignment, prefer_final_locals, avoid_js_rounded_ints, avoid_positional_boolean_parameters, always_specify_types - -extension GetIOSDeviceAssetCollection on Isar { - IsarCollection get iOSDeviceAssets => this.collection(); -} - -const IOSDeviceAssetSchema = CollectionSchema( - name: r'IOSDeviceAsset', - id: -1671546753821948030, - properties: { - r'hash': PropertySchema(id: 0, name: r'hash', type: IsarType.byteList), - r'id': PropertySchema(id: 1, name: r'id', type: IsarType.string), - }, - - estimateSize: _iOSDeviceAssetEstimateSize, - serialize: _iOSDeviceAssetSerialize, - deserialize: _iOSDeviceAssetDeserialize, - deserializeProp: _iOSDeviceAssetDeserializeProp, - idName: r'isarId', - indexes: { - r'id': IndexSchema( - id: -3268401673993471357, - name: r'id', - unique: true, - replace: true, - properties: [ - IndexPropertySchema( - name: r'id', - type: IndexType.hash, - caseSensitive: true, - ), - ], - ), - r'hash': IndexSchema( - id: -7973251393006690288, - name: r'hash', - unique: false, - replace: false, - properties: [ - IndexPropertySchema( - name: r'hash', - type: IndexType.hash, - caseSensitive: false, - ), - ], - ), - }, - links: {}, - embeddedSchemas: {}, - - getId: _iOSDeviceAssetGetId, - getLinks: _iOSDeviceAssetGetLinks, - attach: _iOSDeviceAssetAttach, - version: '3.3.0-dev.3', -); - -int _iOSDeviceAssetEstimateSize( - IOSDeviceAsset object, - List offsets, - Map> allOffsets, -) { - var bytesCount = offsets.last; - bytesCount += 3 + object.hash.length; - bytesCount += 3 + object.id.length * 3; - return bytesCount; -} - -void _iOSDeviceAssetSerialize( - IOSDeviceAsset object, - IsarWriter writer, - List offsets, - Map> allOffsets, -) { - writer.writeByteList(offsets[0], object.hash); - writer.writeString(offsets[1], object.id); -} - -IOSDeviceAsset _iOSDeviceAssetDeserialize( - Id id, - IsarReader reader, - List offsets, - Map> allOffsets, -) { - final object = IOSDeviceAsset( - hash: reader.readByteList(offsets[0]) ?? [], - id: reader.readString(offsets[1]), - ); - return object; -} - -P _iOSDeviceAssetDeserializeProp

( - IsarReader reader, - int propertyId, - int offset, - Map> allOffsets, -) { - switch (propertyId) { - case 0: - return (reader.readByteList(offset) ?? []) as P; - case 1: - return (reader.readString(offset)) as P; - default: - throw IsarError('Unknown property with id $propertyId'); - } -} - -Id _iOSDeviceAssetGetId(IOSDeviceAsset object) { - return object.isarId; -} - -List> _iOSDeviceAssetGetLinks(IOSDeviceAsset object) { - return []; -} - -void _iOSDeviceAssetAttach( - IsarCollection col, - Id id, - IOSDeviceAsset object, -) {} - -extension IOSDeviceAssetByIndex on IsarCollection { - Future getById(String id) { - return getByIndex(r'id', [id]); - } - - IOSDeviceAsset? getByIdSync(String id) { - return getByIndexSync(r'id', [id]); - } - - Future deleteById(String id) { - return deleteByIndex(r'id', [id]); - } - - bool deleteByIdSync(String id) { - return deleteByIndexSync(r'id', [id]); - } - - Future> getAllById(List idValues) { - final values = idValues.map((e) => [e]).toList(); - return getAllByIndex(r'id', values); - } - - List getAllByIdSync(List idValues) { - final values = idValues.map((e) => [e]).toList(); - return getAllByIndexSync(r'id', values); - } - - Future deleteAllById(List idValues) { - final values = idValues.map((e) => [e]).toList(); - return deleteAllByIndex(r'id', values); - } - - int deleteAllByIdSync(List idValues) { - final values = idValues.map((e) => [e]).toList(); - return deleteAllByIndexSync(r'id', values); - } - - Future putById(IOSDeviceAsset object) { - return putByIndex(r'id', object); - } - - Id putByIdSync(IOSDeviceAsset object, {bool saveLinks = true}) { - return putByIndexSync(r'id', object, saveLinks: saveLinks); - } - - Future> putAllById(List objects) { - return putAllByIndex(r'id', objects); - } - - List putAllByIdSync( - List objects, { - bool saveLinks = true, - }) { - return putAllByIndexSync(r'id', objects, saveLinks: saveLinks); - } -} - -extension IOSDeviceAssetQueryWhereSort - on QueryBuilder { - QueryBuilder anyIsarId() { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause(const IdWhereClause.any()); - }); - } -} - -extension IOSDeviceAssetQueryWhere - on QueryBuilder { - QueryBuilder isarIdEqualTo( - Id isarId, - ) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.between(lower: isarId, upper: isarId), - ); - }); - } - - QueryBuilder - isarIdNotEqualTo(Id isarId) { - return QueryBuilder.apply(this, (query) { - if (query.whereSort == Sort.asc) { - return query - .addWhereClause( - IdWhereClause.lessThan(upper: isarId, includeUpper: false), - ) - .addWhereClause( - IdWhereClause.greaterThan(lower: isarId, includeLower: false), - ); - } else { - return query - .addWhereClause( - IdWhereClause.greaterThan(lower: isarId, includeLower: false), - ) - .addWhereClause( - IdWhereClause.lessThan(upper: isarId, includeUpper: false), - ); - } - }); - } - - QueryBuilder - isarIdGreaterThan(Id isarId, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.greaterThan(lower: isarId, includeLower: include), - ); - }); - } - - QueryBuilder - isarIdLessThan(Id isarId, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.lessThan(upper: isarId, includeUpper: include), - ); - }); - } - - QueryBuilder isarIdBetween( - Id lowerIsarId, - Id upperIsarId, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.between( - lower: lowerIsarId, - includeLower: includeLower, - upper: upperIsarId, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder idEqualTo( - String id, - ) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IndexWhereClause.equalTo(indexName: r'id', value: [id]), - ); - }); - } - - QueryBuilder idNotEqualTo( - String id, - ) { - return QueryBuilder.apply(this, (query) { - if (query.whereSort == Sort.asc) { - return query - .addWhereClause( - IndexWhereClause.between( - indexName: r'id', - lower: [], - upper: [id], - includeUpper: false, - ), - ) - .addWhereClause( - IndexWhereClause.between( - indexName: r'id', - lower: [id], - includeLower: false, - upper: [], - ), - ); - } else { - return query - .addWhereClause( - IndexWhereClause.between( - indexName: r'id', - lower: [id], - includeLower: false, - upper: [], - ), - ) - .addWhereClause( - IndexWhereClause.between( - indexName: r'id', - lower: [], - upper: [id], - includeUpper: false, - ), - ); - } - }); - } - - QueryBuilder hashEqualTo( - List hash, - ) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IndexWhereClause.equalTo(indexName: r'hash', value: [hash]), - ); - }); - } - - QueryBuilder - hashNotEqualTo(List hash) { - return QueryBuilder.apply(this, (query) { - if (query.whereSort == Sort.asc) { - return query - .addWhereClause( - IndexWhereClause.between( - indexName: r'hash', - lower: [], - upper: [hash], - includeUpper: false, - ), - ) - .addWhereClause( - IndexWhereClause.between( - indexName: r'hash', - lower: [hash], - includeLower: false, - upper: [], - ), - ); - } else { - return query - .addWhereClause( - IndexWhereClause.between( - indexName: r'hash', - lower: [hash], - includeLower: false, - upper: [], - ), - ) - .addWhereClause( - IndexWhereClause.between( - indexName: r'hash', - lower: [], - upper: [hash], - includeUpper: false, - ), - ); - } - }); - } -} - -extension IOSDeviceAssetQueryFilter - on QueryBuilder { - QueryBuilder - hashElementEqualTo(int value) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'hash', value: value), - ); - }); - } - - QueryBuilder - hashElementGreaterThan(int value, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'hash', - value: value, - ), - ); - }); - } - - QueryBuilder - hashElementLessThan(int value, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'hash', - value: value, - ), - ); - }); - } - - QueryBuilder - hashElementBetween( - int lower, - int upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'hash', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder - hashLengthEqualTo(int length) { - return QueryBuilder.apply(this, (query) { - return query.listLength(r'hash', length, true, length, true); - }); - } - - QueryBuilder - hashIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.listLength(r'hash', 0, true, 0, true); - }); - } - - QueryBuilder - hashIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.listLength(r'hash', 0, false, 999999, true); - }); - } - - QueryBuilder - hashLengthLessThan(int length, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.listLength(r'hash', 0, true, length, include); - }); - } - - QueryBuilder - hashLengthGreaterThan(int length, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.listLength(r'hash', length, include, 999999, true); - }); - } - - QueryBuilder - hashLengthBetween( - int lower, - int upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.listLength( - r'hash', - lower, - includeLower, - upper, - includeUpper, - ); - }); - } - - QueryBuilder idEqualTo( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'id', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - idGreaterThan( - String value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'id', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - idLessThan(String value, {bool include = false, bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'id', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder idBetween( - String lower, - String upper, { - bool includeLower = true, - bool includeUpper = true, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'id', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - idStartsWith(String value, {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.startsWith( - property: r'id', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - idEndsWith(String value, {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.endsWith( - property: r'id', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - idContains(String value, {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.contains( - property: r'id', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder idMatches( - String pattern, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.matches( - property: r'id', - wildcard: pattern, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - idIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'id', value: ''), - ); - }); - } - - QueryBuilder - idIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan(property: r'id', value: ''), - ); - }); - } - - QueryBuilder - isarIdEqualTo(Id value) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'isarId', value: value), - ); - }); - } - - QueryBuilder - isarIdGreaterThan(Id value, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'isarId', - value: value, - ), - ); - }); - } - - QueryBuilder - isarIdLessThan(Id value, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'isarId', - value: value, - ), - ); - }); - } - - QueryBuilder - isarIdBetween( - Id lower, - Id upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'isarId', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } -} - -extension IOSDeviceAssetQueryObject - on QueryBuilder {} - -extension IOSDeviceAssetQueryLinks - on QueryBuilder {} - -extension IOSDeviceAssetQuerySortBy - on QueryBuilder { - QueryBuilder sortById() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'id', Sort.asc); - }); - } - - QueryBuilder sortByIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'id', Sort.desc); - }); - } -} - -extension IOSDeviceAssetQuerySortThenBy - on QueryBuilder { - QueryBuilder thenById() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'id', Sort.asc); - }); - } - - QueryBuilder thenByIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'id', Sort.desc); - }); - } - - QueryBuilder thenByIsarId() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isarId', Sort.asc); - }); - } - - QueryBuilder - thenByIsarIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isarId', Sort.desc); - }); - } -} - -extension IOSDeviceAssetQueryWhereDistinct - on QueryBuilder { - QueryBuilder distinctByHash() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'hash'); - }); - } - - QueryBuilder distinctById({ - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'id', caseSensitive: caseSensitive); - }); - } -} - -extension IOSDeviceAssetQueryProperty - on QueryBuilder { - QueryBuilder isarIdProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'isarId'); - }); - } - - QueryBuilder, QQueryOperations> hashProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'hash'); - }); - } - - QueryBuilder idProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'id'); - }); - } -} diff --git a/mobile/lib/entities/store.entity.dart b/mobile/lib/entities/store.entity.dart index 7b59e119d6..17ad88cee9 100644 --- a/mobile/lib/entities/store.entity.dart +++ b/mobile/lib/entities/store.entity.dart @@ -1,38 +1,4 @@ -import 'dart:convert'; -import 'dart:typed_data'; - -import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/domain/services/store.service.dart'; // ignore: non_constant_identifier_names final Store = StoreService.I; - -class SSLClientCertStoreVal { - final Uint8List data; - final String? password; - - const SSLClientCertStoreVal(this.data, this.password); - - Future save() async { - final b64Str = base64Encode(data); - await Store.put(StoreKey.sslClientCertData, b64Str); - if (password != null) { - await Store.put(StoreKey.sslClientPasswd, password!); - } - } - - static SSLClientCertStoreVal? load() { - final b64Str = Store.tryGet(StoreKey.sslClientCertData); - if (b64Str == null) { - return null; - } - final Uint8List certData = base64Decode(b64Str); - final passwd = Store.tryGet(StoreKey.sslClientPasswd); - return SSLClientCertStoreVal(certData, passwd); - } - - static Future delete() async { - await Store.delete(StoreKey.sslClientCertData); - await Store.delete(StoreKey.sslClientPasswd); - } -} diff --git a/mobile/lib/extensions/asset_extensions.dart b/mobile/lib/extensions/asset_extensions.dart index f7f98b3da7..73a8ec4d05 100644 --- a/mobile/lib/extensions/asset_extensions.dart +++ b/mobile/lib/extensions/asset_extensions.dart @@ -1,26 +1,9 @@ import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/domain/models/exif.model.dart'; -import 'package:immich_mobile/entities/asset.entity.dart' as isar hide AssetTypeEnumHelper; import 'package:immich_mobile/extensions/string_extensions.dart'; import 'package:immich_mobile/infrastructure/utils/exif.converter.dart'; -import 'package:immich_mobile/utils/timezone.dart'; import 'package:openapi/api.dart' as api; -extension TZExtension on isar.Asset { - /// Returns the created time of the asset from the exif info (if available) or from - /// the fileCreatedAt field, adjusted to the timezone value from the exif info along with - /// the timezone offset in [Duration] - (DateTime, Duration) getTZAdjustedTimeAndOffset() { - DateTime dt = fileCreatedAt.toLocal(); - - if (exifInfo?.dateTimeOriginal != null) { - return applyTimezoneOffset(dateTime: exifInfo!.dateTimeOriginal!, timeZone: exifInfo?.timeZone); - } - - return (dt, dt.timeZoneOffset); - } -} - extension DTOToAsset on api.AssetResponseDto { RemoteAsset toDto() { return RemoteAsset( diff --git a/mobile/lib/extensions/collection_extensions.dart b/mobile/lib/extensions/collection_extensions.dart index 541db7ccaf..b861eb0570 100644 --- a/mobile/lib/extensions/collection_extensions.dart +++ b/mobile/lib/extensions/collection_extensions.dart @@ -1,9 +1,6 @@ import 'dart:typed_data'; import 'package:collection/collection.dart'; -import 'package:immich_mobile/domain/models/user.model.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/utils/hash.dart'; extension ListExtension on List { List uniqueConsecutive({int Function(E a, E b)? compare, void Function(E a, E b)? onDuplicate}) { @@ -40,31 +37,6 @@ extension IntListExtension on Iterable { } } -extension AssetListExtension on Iterable { - /// Returns the assets that are already available in the Immich server - Iterable remoteOnly({void Function()? errorCallback}) { - final bool onlyRemote = every((e) => e.isRemote); - if (!onlyRemote) { - if (errorCallback != null) errorCallback(); - return where((a) => a.isRemote); - } - return this; - } - - /// Returns the assets that are owned by the user passed to the [owner] param - /// If [owner] is null, an empty list is returned - Iterable ownedOnly(UserDto? owner, {void Function()? errorCallback}) { - if (owner == null) return []; - final isarUserId = fastHash(owner.id); - final bool onlyOwned = every((e) => e.ownerId == isarUserId); - if (!onlyOwned) { - if (errorCallback != null) errorCallback(); - return where((a) => a.ownerId == isarUserId); - } - return this; - } -} - extension SortedByProperty on Iterable { Iterable sortedByField(Comparable Function(T e) key) { return sorted((a, b) => key(a).compareTo(key(b))); diff --git a/mobile/lib/extensions/translate_extensions.dart b/mobile/lib/extensions/translate_extensions.dart index 7677f3cbd8..b01203a90c 100644 --- a/mobile/lib/extensions/translate_extensions.dart +++ b/mobile/lib/extensions/translate_extensions.dart @@ -1,7 +1,7 @@ import 'package:easy_localization/easy_localization.dart'; -import 'package:intl/message_format.dart'; import 'package:flutter/material.dart'; import 'package:immich_mobile/utils/debug_print.dart'; +import 'package:intl/message_format.dart'; extension StringTranslateExtension on String { String t({BuildContext? context, Map? args}) { diff --git a/mobile/lib/infrastructure/entities/device_asset.entity.dart b/mobile/lib/infrastructure/entities/device_asset.entity.dart deleted file mode 100644 index e3e4a0d4f4..0000000000 --- a/mobile/lib/infrastructure/entities/device_asset.entity.dart +++ /dev/null @@ -1,25 +0,0 @@ -import 'dart:typed_data'; - -import 'package:immich_mobile/domain/models/device_asset.model.dart'; -import 'package:immich_mobile/utils/hash.dart'; -import 'package:isar/isar.dart'; - -part 'device_asset.entity.g.dart'; - -@Collection(inheritance: false) -class DeviceAssetEntity { - Id get id => fastHash(assetId); - - @Index(replace: true, unique: true, type: IndexType.hash) - final String assetId; - @Index(unique: false, type: IndexType.hash) - final List hash; - final DateTime modifiedTime; - - const DeviceAssetEntity({required this.assetId, required this.hash, required this.modifiedTime}); - - DeviceAsset toModel() => DeviceAsset(assetId: assetId, hash: Uint8List.fromList(hash), modifiedTime: modifiedTime); - - static DeviceAssetEntity fromDto(DeviceAsset dto) => - DeviceAssetEntity(assetId: dto.assetId, hash: dto.hash, modifiedTime: dto.modifiedTime); -} diff --git a/mobile/lib/infrastructure/entities/device_asset.entity.g.dart b/mobile/lib/infrastructure/entities/device_asset.entity.g.dart deleted file mode 100644 index b6c30aca6f..0000000000 --- a/mobile/lib/infrastructure/entities/device_asset.entity.g.dart +++ /dev/null @@ -1,874 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'device_asset.entity.dart'; - -// ************************************************************************** -// IsarCollectionGenerator -// ************************************************************************** - -// coverage:ignore-file -// ignore_for_file: duplicate_ignore, non_constant_identifier_names, constant_identifier_names, invalid_use_of_protected_member, unnecessary_cast, prefer_const_constructors, lines_longer_than_80_chars, require_trailing_commas, inference_failure_on_function_invocation, unnecessary_parenthesis, unnecessary_raw_strings, unnecessary_null_checks, join_return_with_assignment, prefer_final_locals, avoid_js_rounded_ints, avoid_positional_boolean_parameters, always_specify_types - -extension GetDeviceAssetEntityCollection on Isar { - IsarCollection get deviceAssetEntitys => this.collection(); -} - -const DeviceAssetEntitySchema = CollectionSchema( - name: r'DeviceAssetEntity', - id: 6967030785073446271, - properties: { - r'assetId': PropertySchema(id: 0, name: r'assetId', type: IsarType.string), - r'hash': PropertySchema(id: 1, name: r'hash', type: IsarType.byteList), - r'modifiedTime': PropertySchema( - id: 2, - name: r'modifiedTime', - type: IsarType.dateTime, - ), - }, - - estimateSize: _deviceAssetEntityEstimateSize, - serialize: _deviceAssetEntitySerialize, - deserialize: _deviceAssetEntityDeserialize, - deserializeProp: _deviceAssetEntityDeserializeProp, - idName: r'id', - indexes: { - r'assetId': IndexSchema( - id: 174362542210192109, - name: r'assetId', - unique: true, - replace: true, - properties: [ - IndexPropertySchema( - name: r'assetId', - type: IndexType.hash, - caseSensitive: true, - ), - ], - ), - r'hash': IndexSchema( - id: -7973251393006690288, - name: r'hash', - unique: false, - replace: false, - properties: [ - IndexPropertySchema( - name: r'hash', - type: IndexType.hash, - caseSensitive: false, - ), - ], - ), - }, - links: {}, - embeddedSchemas: {}, - - getId: _deviceAssetEntityGetId, - getLinks: _deviceAssetEntityGetLinks, - attach: _deviceAssetEntityAttach, - version: '3.3.0-dev.3', -); - -int _deviceAssetEntityEstimateSize( - DeviceAssetEntity object, - List offsets, - Map> allOffsets, -) { - var bytesCount = offsets.last; - bytesCount += 3 + object.assetId.length * 3; - bytesCount += 3 + object.hash.length; - return bytesCount; -} - -void _deviceAssetEntitySerialize( - DeviceAssetEntity object, - IsarWriter writer, - List offsets, - Map> allOffsets, -) { - writer.writeString(offsets[0], object.assetId); - writer.writeByteList(offsets[1], object.hash); - writer.writeDateTime(offsets[2], object.modifiedTime); -} - -DeviceAssetEntity _deviceAssetEntityDeserialize( - Id id, - IsarReader reader, - List offsets, - Map> allOffsets, -) { - final object = DeviceAssetEntity( - assetId: reader.readString(offsets[0]), - hash: reader.readByteList(offsets[1]) ?? [], - modifiedTime: reader.readDateTime(offsets[2]), - ); - return object; -} - -P _deviceAssetEntityDeserializeProp

( - IsarReader reader, - int propertyId, - int offset, - Map> allOffsets, -) { - switch (propertyId) { - case 0: - return (reader.readString(offset)) as P; - case 1: - return (reader.readByteList(offset) ?? []) as P; - case 2: - return (reader.readDateTime(offset)) as P; - default: - throw IsarError('Unknown property with id $propertyId'); - } -} - -Id _deviceAssetEntityGetId(DeviceAssetEntity object) { - return object.id; -} - -List> _deviceAssetEntityGetLinks( - DeviceAssetEntity object, -) { - return []; -} - -void _deviceAssetEntityAttach( - IsarCollection col, - Id id, - DeviceAssetEntity object, -) {} - -extension DeviceAssetEntityByIndex on IsarCollection { - Future getByAssetId(String assetId) { - return getByIndex(r'assetId', [assetId]); - } - - DeviceAssetEntity? getByAssetIdSync(String assetId) { - return getByIndexSync(r'assetId', [assetId]); - } - - Future deleteByAssetId(String assetId) { - return deleteByIndex(r'assetId', [assetId]); - } - - bool deleteByAssetIdSync(String assetId) { - return deleteByIndexSync(r'assetId', [assetId]); - } - - Future> getAllByAssetId(List assetIdValues) { - final values = assetIdValues.map((e) => [e]).toList(); - return getAllByIndex(r'assetId', values); - } - - List getAllByAssetIdSync(List assetIdValues) { - final values = assetIdValues.map((e) => [e]).toList(); - return getAllByIndexSync(r'assetId', values); - } - - Future deleteAllByAssetId(List assetIdValues) { - final values = assetIdValues.map((e) => [e]).toList(); - return deleteAllByIndex(r'assetId', values); - } - - int deleteAllByAssetIdSync(List assetIdValues) { - final values = assetIdValues.map((e) => [e]).toList(); - return deleteAllByIndexSync(r'assetId', values); - } - - Future putByAssetId(DeviceAssetEntity object) { - return putByIndex(r'assetId', object); - } - - Id putByAssetIdSync(DeviceAssetEntity object, {bool saveLinks = true}) { - return putByIndexSync(r'assetId', object, saveLinks: saveLinks); - } - - Future> putAllByAssetId(List objects) { - return putAllByIndex(r'assetId', objects); - } - - List putAllByAssetIdSync( - List objects, { - bool saveLinks = true, - }) { - return putAllByIndexSync(r'assetId', objects, saveLinks: saveLinks); - } -} - -extension DeviceAssetEntityQueryWhereSort - on QueryBuilder { - QueryBuilder anyId() { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause(const IdWhereClause.any()); - }); - } -} - -extension DeviceAssetEntityQueryWhere - on QueryBuilder { - QueryBuilder - idEqualTo(Id id) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause(IdWhereClause.between(lower: id, upper: id)); - }); - } - - QueryBuilder - idNotEqualTo(Id id) { - return QueryBuilder.apply(this, (query) { - if (query.whereSort == Sort.asc) { - return query - .addWhereClause( - IdWhereClause.lessThan(upper: id, includeUpper: false), - ) - .addWhereClause( - IdWhereClause.greaterThan(lower: id, includeLower: false), - ); - } else { - return query - .addWhereClause( - IdWhereClause.greaterThan(lower: id, includeLower: false), - ) - .addWhereClause( - IdWhereClause.lessThan(upper: id, includeUpper: false), - ); - } - }); - } - - QueryBuilder - idGreaterThan(Id id, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.greaterThan(lower: id, includeLower: include), - ); - }); - } - - QueryBuilder - idLessThan(Id id, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.lessThan(upper: id, includeUpper: include), - ); - }); - } - - QueryBuilder - idBetween( - Id lowerId, - Id upperId, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.between( - lower: lowerId, - includeLower: includeLower, - upper: upperId, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder - assetIdEqualTo(String assetId) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IndexWhereClause.equalTo(indexName: r'assetId', value: [assetId]), - ); - }); - } - - QueryBuilder - assetIdNotEqualTo(String assetId) { - return QueryBuilder.apply(this, (query) { - if (query.whereSort == Sort.asc) { - return query - .addWhereClause( - IndexWhereClause.between( - indexName: r'assetId', - lower: [], - upper: [assetId], - includeUpper: false, - ), - ) - .addWhereClause( - IndexWhereClause.between( - indexName: r'assetId', - lower: [assetId], - includeLower: false, - upper: [], - ), - ); - } else { - return query - .addWhereClause( - IndexWhereClause.between( - indexName: r'assetId', - lower: [assetId], - includeLower: false, - upper: [], - ), - ) - .addWhereClause( - IndexWhereClause.between( - indexName: r'assetId', - lower: [], - upper: [assetId], - includeUpper: false, - ), - ); - } - }); - } - - QueryBuilder - hashEqualTo(List hash) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IndexWhereClause.equalTo(indexName: r'hash', value: [hash]), - ); - }); - } - - QueryBuilder - hashNotEqualTo(List hash) { - return QueryBuilder.apply(this, (query) { - if (query.whereSort == Sort.asc) { - return query - .addWhereClause( - IndexWhereClause.between( - indexName: r'hash', - lower: [], - upper: [hash], - includeUpper: false, - ), - ) - .addWhereClause( - IndexWhereClause.between( - indexName: r'hash', - lower: [hash], - includeLower: false, - upper: [], - ), - ); - } else { - return query - .addWhereClause( - IndexWhereClause.between( - indexName: r'hash', - lower: [hash], - includeLower: false, - upper: [], - ), - ) - .addWhereClause( - IndexWhereClause.between( - indexName: r'hash', - lower: [], - upper: [hash], - includeUpper: false, - ), - ); - } - }); - } -} - -extension DeviceAssetEntityQueryFilter - on QueryBuilder { - QueryBuilder - assetIdEqualTo(String value, {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'assetId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - assetIdGreaterThan( - String value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'assetId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - assetIdLessThan( - String value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'assetId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - assetIdBetween( - String lower, - String upper, { - bool includeLower = true, - bool includeUpper = true, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'assetId', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - assetIdStartsWith(String value, {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.startsWith( - property: r'assetId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - assetIdEndsWith(String value, {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.endsWith( - property: r'assetId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - assetIdContains(String value, {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.contains( - property: r'assetId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - assetIdMatches(String pattern, {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.matches( - property: r'assetId', - wildcard: pattern, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - assetIdIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'assetId', value: ''), - ); - }); - } - - QueryBuilder - assetIdIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan(property: r'assetId', value: ''), - ); - }); - } - - QueryBuilder - hashElementEqualTo(int value) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'hash', value: value), - ); - }); - } - - QueryBuilder - hashElementGreaterThan(int value, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'hash', - value: value, - ), - ); - }); - } - - QueryBuilder - hashElementLessThan(int value, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'hash', - value: value, - ), - ); - }); - } - - QueryBuilder - hashElementBetween( - int lower, - int upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'hash', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder - hashLengthEqualTo(int length) { - return QueryBuilder.apply(this, (query) { - return query.listLength(r'hash', length, true, length, true); - }); - } - - QueryBuilder - hashIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.listLength(r'hash', 0, true, 0, true); - }); - } - - QueryBuilder - hashIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.listLength(r'hash', 0, false, 999999, true); - }); - } - - QueryBuilder - hashLengthLessThan(int length, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.listLength(r'hash', 0, true, length, include); - }); - } - - QueryBuilder - hashLengthGreaterThan(int length, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.listLength(r'hash', length, include, 999999, true); - }); - } - - QueryBuilder - hashLengthBetween( - int lower, - int upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.listLength( - r'hash', - lower, - includeLower, - upper, - includeUpper, - ); - }); - } - - QueryBuilder - idEqualTo(Id value) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'id', value: value), - ); - }); - } - - QueryBuilder - idGreaterThan(Id value, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'id', - value: value, - ), - ); - }); - } - - QueryBuilder - idLessThan(Id value, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'id', - value: value, - ), - ); - }); - } - - QueryBuilder - idBetween( - Id lower, - Id upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'id', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder - modifiedTimeEqualTo(DateTime value) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'modifiedTime', value: value), - ); - }); - } - - QueryBuilder - modifiedTimeGreaterThan(DateTime value, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'modifiedTime', - value: value, - ), - ); - }); - } - - QueryBuilder - modifiedTimeLessThan(DateTime value, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'modifiedTime', - value: value, - ), - ); - }); - } - - QueryBuilder - modifiedTimeBetween( - DateTime lower, - DateTime upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'modifiedTime', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } -} - -extension DeviceAssetEntityQueryObject - on QueryBuilder {} - -extension DeviceAssetEntityQueryLinks - on QueryBuilder {} - -extension DeviceAssetEntityQuerySortBy - on QueryBuilder { - QueryBuilder - sortByAssetId() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'assetId', Sort.asc); - }); - } - - QueryBuilder - sortByAssetIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'assetId', Sort.desc); - }); - } - - QueryBuilder - sortByModifiedTime() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'modifiedTime', Sort.asc); - }); - } - - QueryBuilder - sortByModifiedTimeDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'modifiedTime', Sort.desc); - }); - } -} - -extension DeviceAssetEntityQuerySortThenBy - on QueryBuilder { - QueryBuilder - thenByAssetId() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'assetId', Sort.asc); - }); - } - - QueryBuilder - thenByAssetIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'assetId', Sort.desc); - }); - } - - QueryBuilder thenById() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'id', Sort.asc); - }); - } - - QueryBuilder - thenByIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'id', Sort.desc); - }); - } - - QueryBuilder - thenByModifiedTime() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'modifiedTime', Sort.asc); - }); - } - - QueryBuilder - thenByModifiedTimeDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'modifiedTime', Sort.desc); - }); - } -} - -extension DeviceAssetEntityQueryWhereDistinct - on QueryBuilder { - QueryBuilder - distinctByAssetId({bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'assetId', caseSensitive: caseSensitive); - }); - } - - QueryBuilder - distinctByHash() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'hash'); - }); - } - - QueryBuilder - distinctByModifiedTime() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'modifiedTime'); - }); - } -} - -extension DeviceAssetEntityQueryProperty - on QueryBuilder { - QueryBuilder idProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'id'); - }); - } - - QueryBuilder assetIdProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'assetId'); - }); - } - - QueryBuilder, QQueryOperations> hashProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'hash'); - }); - } - - QueryBuilder - modifiedTimeProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'modifiedTime'); - }); - } -} diff --git a/mobile/lib/infrastructure/entities/exif.entity.dart b/mobile/lib/infrastructure/entities/exif.entity.dart index 06262f4afc..e009029ea7 100644 --- a/mobile/lib/infrastructure/entities/exif.entity.dart +++ b/mobile/lib/infrastructure/entities/exif.entity.dart @@ -4,96 +4,6 @@ import 'package:immich_mobile/infrastructure/entities/exif.entity.drift.dart'; import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.dart'; import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart'; import 'package:immich_mobile/infrastructure/utils/exif.converter.dart'; -import 'package:isar/isar.dart'; - -part 'exif.entity.g.dart'; - -/// Exif information 1:1 relation with Asset -@Collection(inheritance: false) -class ExifInfo { - final Id? id; - final int? fileSize; - final DateTime? dateTimeOriginal; - final String? timeZone; - final String? make; - final String? model; - final String? lens; - final float? f; - final float? mm; - final short? iso; - final float? exposureSeconds; - final float? lat; - final float? long; - final String? city; - final String? state; - final String? country; - final String? description; - final String? orientation; - - const ExifInfo({ - this.id, - this.fileSize, - this.dateTimeOriginal, - this.timeZone, - this.make, - this.model, - this.lens, - this.f, - this.mm, - this.iso, - this.exposureSeconds, - this.lat, - this.long, - this.city, - this.state, - this.country, - this.description, - this.orientation, - }); - - static ExifInfo fromDto(domain.ExifInfo dto) => ExifInfo( - id: dto.assetId, - fileSize: dto.fileSize, - dateTimeOriginal: dto.dateTimeOriginal, - timeZone: dto.timeZone, - make: dto.make, - model: dto.model, - lens: dto.lens, - f: dto.f, - mm: dto.mm, - iso: dto.iso?.toInt(), - exposureSeconds: dto.exposureSeconds, - lat: dto.latitude, - long: dto.longitude, - city: dto.city, - state: dto.state, - country: dto.country, - description: dto.description, - orientation: dto.orientation, - ); - - domain.ExifInfo toDto() => domain.ExifInfo( - assetId: id, - fileSize: fileSize, - description: description, - orientation: orientation, - timeZone: timeZone, - dateTimeOriginal: dateTimeOriginal, - isFlipped: ExifDtoConverter.isOrientationFlipped(orientation), - latitude: lat, - longitude: long, - city: city, - state: state, - country: country, - make: make, - model: model, - lens: lens, - f: f, - mm: mm, - iso: iso?.toInt(), - exposureSeconds: exposureSeconds, - ); -} @TableIndex.sql('CREATE INDEX IF NOT EXISTS idx_lat_lng ON remote_exif_entity (latitude, longitude)') class RemoteExifEntity extends Table with DriftDefaultsMixin { diff --git a/mobile/lib/infrastructure/entities/exif.entity.g.dart b/mobile/lib/infrastructure/entities/exif.entity.g.dart deleted file mode 100644 index ffbfd0d8f0..0000000000 --- a/mobile/lib/infrastructure/entities/exif.entity.g.dart +++ /dev/null @@ -1,3200 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'exif.entity.dart'; - -// ************************************************************************** -// IsarCollectionGenerator -// ************************************************************************** - -// coverage:ignore-file -// ignore_for_file: duplicate_ignore, non_constant_identifier_names, constant_identifier_names, invalid_use_of_protected_member, unnecessary_cast, prefer_const_constructors, lines_longer_than_80_chars, require_trailing_commas, inference_failure_on_function_invocation, unnecessary_parenthesis, unnecessary_raw_strings, unnecessary_null_checks, join_return_with_assignment, prefer_final_locals, avoid_js_rounded_ints, avoid_positional_boolean_parameters, always_specify_types - -extension GetExifInfoCollection on Isar { - IsarCollection get exifInfos => this.collection(); -} - -const ExifInfoSchema = CollectionSchema( - name: r'ExifInfo', - id: -2409260054350835217, - properties: { - r'city': PropertySchema(id: 0, name: r'city', type: IsarType.string), - r'country': PropertySchema(id: 1, name: r'country', type: IsarType.string), - r'dateTimeOriginal': PropertySchema( - id: 2, - name: r'dateTimeOriginal', - type: IsarType.dateTime, - ), - r'description': PropertySchema( - id: 3, - name: r'description', - type: IsarType.string, - ), - r'exposureSeconds': PropertySchema( - id: 4, - name: r'exposureSeconds', - type: IsarType.float, - ), - r'f': PropertySchema(id: 5, name: r'f', type: IsarType.float), - r'fileSize': PropertySchema(id: 6, name: r'fileSize', type: IsarType.long), - r'iso': PropertySchema(id: 7, name: r'iso', type: IsarType.int), - r'lat': PropertySchema(id: 8, name: r'lat', type: IsarType.float), - r'lens': PropertySchema(id: 9, name: r'lens', type: IsarType.string), - r'long': PropertySchema(id: 10, name: r'long', type: IsarType.float), - r'make': PropertySchema(id: 11, name: r'make', type: IsarType.string), - r'mm': PropertySchema(id: 12, name: r'mm', type: IsarType.float), - r'model': PropertySchema(id: 13, name: r'model', type: IsarType.string), - r'orientation': PropertySchema( - id: 14, - name: r'orientation', - type: IsarType.string, - ), - r'state': PropertySchema(id: 15, name: r'state', type: IsarType.string), - r'timeZone': PropertySchema( - id: 16, - name: r'timeZone', - type: IsarType.string, - ), - }, - - estimateSize: _exifInfoEstimateSize, - serialize: _exifInfoSerialize, - deserialize: _exifInfoDeserialize, - deserializeProp: _exifInfoDeserializeProp, - idName: r'id', - indexes: {}, - links: {}, - embeddedSchemas: {}, - - getId: _exifInfoGetId, - getLinks: _exifInfoGetLinks, - attach: _exifInfoAttach, - version: '3.3.0-dev.3', -); - -int _exifInfoEstimateSize( - ExifInfo object, - List offsets, - Map> allOffsets, -) { - var bytesCount = offsets.last; - { - final value = object.city; - if (value != null) { - bytesCount += 3 + value.length * 3; - } - } - { - final value = object.country; - if (value != null) { - bytesCount += 3 + value.length * 3; - } - } - { - final value = object.description; - if (value != null) { - bytesCount += 3 + value.length * 3; - } - } - { - final value = object.lens; - if (value != null) { - bytesCount += 3 + value.length * 3; - } - } - { - final value = object.make; - if (value != null) { - bytesCount += 3 + value.length * 3; - } - } - { - final value = object.model; - if (value != null) { - bytesCount += 3 + value.length * 3; - } - } - { - final value = object.orientation; - if (value != null) { - bytesCount += 3 + value.length * 3; - } - } - { - final value = object.state; - if (value != null) { - bytesCount += 3 + value.length * 3; - } - } - { - final value = object.timeZone; - if (value != null) { - bytesCount += 3 + value.length * 3; - } - } - return bytesCount; -} - -void _exifInfoSerialize( - ExifInfo object, - IsarWriter writer, - List offsets, - Map> allOffsets, -) { - writer.writeString(offsets[0], object.city); - writer.writeString(offsets[1], object.country); - writer.writeDateTime(offsets[2], object.dateTimeOriginal); - writer.writeString(offsets[3], object.description); - writer.writeFloat(offsets[4], object.exposureSeconds); - writer.writeFloat(offsets[5], object.f); - writer.writeLong(offsets[6], object.fileSize); - writer.writeInt(offsets[7], object.iso); - writer.writeFloat(offsets[8], object.lat); - writer.writeString(offsets[9], object.lens); - writer.writeFloat(offsets[10], object.long); - writer.writeString(offsets[11], object.make); - writer.writeFloat(offsets[12], object.mm); - writer.writeString(offsets[13], object.model); - writer.writeString(offsets[14], object.orientation); - writer.writeString(offsets[15], object.state); - writer.writeString(offsets[16], object.timeZone); -} - -ExifInfo _exifInfoDeserialize( - Id id, - IsarReader reader, - List offsets, - Map> allOffsets, -) { - final object = ExifInfo( - city: reader.readStringOrNull(offsets[0]), - country: reader.readStringOrNull(offsets[1]), - dateTimeOriginal: reader.readDateTimeOrNull(offsets[2]), - description: reader.readStringOrNull(offsets[3]), - exposureSeconds: reader.readFloatOrNull(offsets[4]), - f: reader.readFloatOrNull(offsets[5]), - fileSize: reader.readLongOrNull(offsets[6]), - id: id, - iso: reader.readIntOrNull(offsets[7]), - lat: reader.readFloatOrNull(offsets[8]), - lens: reader.readStringOrNull(offsets[9]), - long: reader.readFloatOrNull(offsets[10]), - make: reader.readStringOrNull(offsets[11]), - mm: reader.readFloatOrNull(offsets[12]), - model: reader.readStringOrNull(offsets[13]), - orientation: reader.readStringOrNull(offsets[14]), - state: reader.readStringOrNull(offsets[15]), - timeZone: reader.readStringOrNull(offsets[16]), - ); - return object; -} - -P _exifInfoDeserializeProp

( - IsarReader reader, - int propertyId, - int offset, - Map> allOffsets, -) { - switch (propertyId) { - case 0: - return (reader.readStringOrNull(offset)) as P; - case 1: - return (reader.readStringOrNull(offset)) as P; - case 2: - return (reader.readDateTimeOrNull(offset)) as P; - case 3: - return (reader.readStringOrNull(offset)) as P; - case 4: - return (reader.readFloatOrNull(offset)) as P; - case 5: - return (reader.readFloatOrNull(offset)) as P; - case 6: - return (reader.readLongOrNull(offset)) as P; - case 7: - return (reader.readIntOrNull(offset)) as P; - case 8: - return (reader.readFloatOrNull(offset)) as P; - case 9: - return (reader.readStringOrNull(offset)) as P; - case 10: - return (reader.readFloatOrNull(offset)) as P; - case 11: - return (reader.readStringOrNull(offset)) as P; - case 12: - return (reader.readFloatOrNull(offset)) as P; - case 13: - return (reader.readStringOrNull(offset)) as P; - case 14: - return (reader.readStringOrNull(offset)) as P; - case 15: - return (reader.readStringOrNull(offset)) as P; - case 16: - return (reader.readStringOrNull(offset)) as P; - default: - throw IsarError('Unknown property with id $propertyId'); - } -} - -Id _exifInfoGetId(ExifInfo object) { - return object.id ?? Isar.autoIncrement; -} - -List> _exifInfoGetLinks(ExifInfo object) { - return []; -} - -void _exifInfoAttach(IsarCollection col, Id id, ExifInfo object) {} - -extension ExifInfoQueryWhereSort on QueryBuilder { - QueryBuilder anyId() { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause(const IdWhereClause.any()); - }); - } -} - -extension ExifInfoQueryWhere on QueryBuilder { - QueryBuilder idEqualTo(Id id) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause(IdWhereClause.between(lower: id, upper: id)); - }); - } - - QueryBuilder idNotEqualTo(Id id) { - return QueryBuilder.apply(this, (query) { - if (query.whereSort == Sort.asc) { - return query - .addWhereClause( - IdWhereClause.lessThan(upper: id, includeUpper: false), - ) - .addWhereClause( - IdWhereClause.greaterThan(lower: id, includeLower: false), - ); - } else { - return query - .addWhereClause( - IdWhereClause.greaterThan(lower: id, includeLower: false), - ) - .addWhereClause( - IdWhereClause.lessThan(upper: id, includeUpper: false), - ); - } - }); - } - - QueryBuilder idGreaterThan( - Id id, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.greaterThan(lower: id, includeLower: include), - ); - }); - } - - QueryBuilder idLessThan( - Id id, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.lessThan(upper: id, includeUpper: include), - ); - }); - } - - QueryBuilder idBetween( - Id lowerId, - Id upperId, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.between( - lower: lowerId, - includeLower: includeLower, - upper: upperId, - includeUpper: includeUpper, - ), - ); - }); - } -} - -extension ExifInfoQueryFilter - on QueryBuilder { - QueryBuilder cityIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNull(property: r'city'), - ); - }); - } - - QueryBuilder cityIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNotNull(property: r'city'), - ); - }); - } - - QueryBuilder cityEqualTo( - String? value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'city', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder cityGreaterThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'city', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder cityLessThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'city', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder cityBetween( - String? lower, - String? upper, { - bool includeLower = true, - bool includeUpper = true, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'city', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder cityStartsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.startsWith( - property: r'city', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder cityEndsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.endsWith( - property: r'city', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder cityContains( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.contains( - property: r'city', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder cityMatches( - String pattern, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.matches( - property: r'city', - wildcard: pattern, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder cityIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'city', value: ''), - ); - }); - } - - QueryBuilder cityIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan(property: r'city', value: ''), - ); - }); - } - - QueryBuilder countryIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNull(property: r'country'), - ); - }); - } - - QueryBuilder countryIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNotNull(property: r'country'), - ); - }); - } - - QueryBuilder countryEqualTo( - String? value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'country', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder countryGreaterThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'country', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder countryLessThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'country', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder countryBetween( - String? lower, - String? upper, { - bool includeLower = true, - bool includeUpper = true, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'country', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder countryStartsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.startsWith( - property: r'country', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder countryEndsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.endsWith( - property: r'country', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder countryContains( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.contains( - property: r'country', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder countryMatches( - String pattern, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.matches( - property: r'country', - wildcard: pattern, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder countryIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'country', value: ''), - ); - }); - } - - QueryBuilder countryIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan(property: r'country', value: ''), - ); - }); - } - - QueryBuilder - dateTimeOriginalIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNull(property: r'dateTimeOriginal'), - ); - }); - } - - QueryBuilder - dateTimeOriginalIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNotNull(property: r'dateTimeOriginal'), - ); - }); - } - - QueryBuilder - dateTimeOriginalEqualTo(DateTime? value) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'dateTimeOriginal', value: value), - ); - }); - } - - QueryBuilder - dateTimeOriginalGreaterThan(DateTime? value, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'dateTimeOriginal', - value: value, - ), - ); - }); - } - - QueryBuilder - dateTimeOriginalLessThan(DateTime? value, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'dateTimeOriginal', - value: value, - ), - ); - }); - } - - QueryBuilder - dateTimeOriginalBetween( - DateTime? lower, - DateTime? upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'dateTimeOriginal', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder descriptionIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNull(property: r'description'), - ); - }); - } - - QueryBuilder - descriptionIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNotNull(property: r'description'), - ); - }); - } - - QueryBuilder descriptionEqualTo( - String? value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'description', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - descriptionGreaterThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'description', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder descriptionLessThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'description', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder descriptionBetween( - String? lower, - String? upper, { - bool includeLower = true, - bool includeUpper = true, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'description', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder descriptionStartsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.startsWith( - property: r'description', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder descriptionEndsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.endsWith( - property: r'description', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder descriptionContains( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.contains( - property: r'description', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder descriptionMatches( - String pattern, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.matches( - property: r'description', - wildcard: pattern, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder descriptionIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'description', value: ''), - ); - }); - } - - QueryBuilder - descriptionIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan(property: r'description', value: ''), - ); - }); - } - - QueryBuilder - exposureSecondsIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNull(property: r'exposureSeconds'), - ); - }); - } - - QueryBuilder - exposureSecondsIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNotNull(property: r'exposureSeconds'), - ); - }); - } - - QueryBuilder - exposureSecondsEqualTo(double? value, {double epsilon = Query.epsilon}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'exposureSeconds', - value: value, - - epsilon: epsilon, - ), - ); - }); - } - - QueryBuilder - exposureSecondsGreaterThan( - double? value, { - bool include = false, - double epsilon = Query.epsilon, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'exposureSeconds', - value: value, - - epsilon: epsilon, - ), - ); - }); - } - - QueryBuilder - exposureSecondsLessThan( - double? value, { - bool include = false, - double epsilon = Query.epsilon, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'exposureSeconds', - value: value, - - epsilon: epsilon, - ), - ); - }); - } - - QueryBuilder - exposureSecondsBetween( - double? lower, - double? upper, { - bool includeLower = true, - bool includeUpper = true, - double epsilon = Query.epsilon, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'exposureSeconds', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - - epsilon: epsilon, - ), - ); - }); - } - - QueryBuilder fIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNull(property: r'f'), - ); - }); - } - - QueryBuilder fIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNotNull(property: r'f'), - ); - }); - } - - QueryBuilder fEqualTo( - double? value, { - double epsilon = Query.epsilon, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'f', value: value, epsilon: epsilon), - ); - }); - } - - QueryBuilder fGreaterThan( - double? value, { - bool include = false, - double epsilon = Query.epsilon, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'f', - value: value, - - epsilon: epsilon, - ), - ); - }); - } - - QueryBuilder fLessThan( - double? value, { - bool include = false, - double epsilon = Query.epsilon, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'f', - value: value, - - epsilon: epsilon, - ), - ); - }); - } - - QueryBuilder fBetween( - double? lower, - double? upper, { - bool includeLower = true, - bool includeUpper = true, - double epsilon = Query.epsilon, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'f', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - - epsilon: epsilon, - ), - ); - }); - } - - QueryBuilder fileSizeIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNull(property: r'fileSize'), - ); - }); - } - - QueryBuilder fileSizeIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNotNull(property: r'fileSize'), - ); - }); - } - - QueryBuilder fileSizeEqualTo( - int? value, - ) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'fileSize', value: value), - ); - }); - } - - QueryBuilder fileSizeGreaterThan( - int? value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'fileSize', - value: value, - ), - ); - }); - } - - QueryBuilder fileSizeLessThan( - int? value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'fileSize', - value: value, - ), - ); - }); - } - - QueryBuilder fileSizeBetween( - int? lower, - int? upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'fileSize', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder idIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNull(property: r'id'), - ); - }); - } - - QueryBuilder idIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNotNull(property: r'id'), - ); - }); - } - - QueryBuilder idEqualTo(Id? value) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'id', value: value), - ); - }); - } - - QueryBuilder idGreaterThan( - Id? value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'id', - value: value, - ), - ); - }); - } - - QueryBuilder idLessThan( - Id? value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'id', - value: value, - ), - ); - }); - } - - QueryBuilder idBetween( - Id? lower, - Id? upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'id', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder isoIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNull(property: r'iso'), - ); - }); - } - - QueryBuilder isoIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNotNull(property: r'iso'), - ); - }); - } - - QueryBuilder isoEqualTo( - int? value, - ) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'iso', value: value), - ); - }); - } - - QueryBuilder isoGreaterThan( - int? value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'iso', - value: value, - ), - ); - }); - } - - QueryBuilder isoLessThan( - int? value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'iso', - value: value, - ), - ); - }); - } - - QueryBuilder isoBetween( - int? lower, - int? upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'iso', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder latIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNull(property: r'lat'), - ); - }); - } - - QueryBuilder latIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNotNull(property: r'lat'), - ); - }); - } - - QueryBuilder latEqualTo( - double? value, { - double epsilon = Query.epsilon, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'lat', - value: value, - - epsilon: epsilon, - ), - ); - }); - } - - QueryBuilder latGreaterThan( - double? value, { - bool include = false, - double epsilon = Query.epsilon, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'lat', - value: value, - - epsilon: epsilon, - ), - ); - }); - } - - QueryBuilder latLessThan( - double? value, { - bool include = false, - double epsilon = Query.epsilon, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'lat', - value: value, - - epsilon: epsilon, - ), - ); - }); - } - - QueryBuilder latBetween( - double? lower, - double? upper, { - bool includeLower = true, - bool includeUpper = true, - double epsilon = Query.epsilon, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'lat', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - - epsilon: epsilon, - ), - ); - }); - } - - QueryBuilder lensIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNull(property: r'lens'), - ); - }); - } - - QueryBuilder lensIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNotNull(property: r'lens'), - ); - }); - } - - QueryBuilder lensEqualTo( - String? value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'lens', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder lensGreaterThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'lens', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder lensLessThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'lens', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder lensBetween( - String? lower, - String? upper, { - bool includeLower = true, - bool includeUpper = true, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'lens', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder lensStartsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.startsWith( - property: r'lens', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder lensEndsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.endsWith( - property: r'lens', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder lensContains( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.contains( - property: r'lens', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder lensMatches( - String pattern, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.matches( - property: r'lens', - wildcard: pattern, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder lensIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'lens', value: ''), - ); - }); - } - - QueryBuilder lensIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan(property: r'lens', value: ''), - ); - }); - } - - QueryBuilder longIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNull(property: r'long'), - ); - }); - } - - QueryBuilder longIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNotNull(property: r'long'), - ); - }); - } - - QueryBuilder longEqualTo( - double? value, { - double epsilon = Query.epsilon, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'long', - value: value, - - epsilon: epsilon, - ), - ); - }); - } - - QueryBuilder longGreaterThan( - double? value, { - bool include = false, - double epsilon = Query.epsilon, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'long', - value: value, - - epsilon: epsilon, - ), - ); - }); - } - - QueryBuilder longLessThan( - double? value, { - bool include = false, - double epsilon = Query.epsilon, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'long', - value: value, - - epsilon: epsilon, - ), - ); - }); - } - - QueryBuilder longBetween( - double? lower, - double? upper, { - bool includeLower = true, - bool includeUpper = true, - double epsilon = Query.epsilon, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'long', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - - epsilon: epsilon, - ), - ); - }); - } - - QueryBuilder makeIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNull(property: r'make'), - ); - }); - } - - QueryBuilder makeIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNotNull(property: r'make'), - ); - }); - } - - QueryBuilder makeEqualTo( - String? value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'make', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder makeGreaterThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'make', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder makeLessThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'make', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder makeBetween( - String? lower, - String? upper, { - bool includeLower = true, - bool includeUpper = true, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'make', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder makeStartsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.startsWith( - property: r'make', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder makeEndsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.endsWith( - property: r'make', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder makeContains( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.contains( - property: r'make', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder makeMatches( - String pattern, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.matches( - property: r'make', - wildcard: pattern, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder makeIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'make', value: ''), - ); - }); - } - - QueryBuilder makeIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan(property: r'make', value: ''), - ); - }); - } - - QueryBuilder mmIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNull(property: r'mm'), - ); - }); - } - - QueryBuilder mmIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNotNull(property: r'mm'), - ); - }); - } - - QueryBuilder mmEqualTo( - double? value, { - double epsilon = Query.epsilon, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'mm', - value: value, - - epsilon: epsilon, - ), - ); - }); - } - - QueryBuilder mmGreaterThan( - double? value, { - bool include = false, - double epsilon = Query.epsilon, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'mm', - value: value, - - epsilon: epsilon, - ), - ); - }); - } - - QueryBuilder mmLessThan( - double? value, { - bool include = false, - double epsilon = Query.epsilon, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'mm', - value: value, - - epsilon: epsilon, - ), - ); - }); - } - - QueryBuilder mmBetween( - double? lower, - double? upper, { - bool includeLower = true, - bool includeUpper = true, - double epsilon = Query.epsilon, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'mm', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - - epsilon: epsilon, - ), - ); - }); - } - - QueryBuilder modelIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNull(property: r'model'), - ); - }); - } - - QueryBuilder modelIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNotNull(property: r'model'), - ); - }); - } - - QueryBuilder modelEqualTo( - String? value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'model', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder modelGreaterThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'model', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder modelLessThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'model', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder modelBetween( - String? lower, - String? upper, { - bool includeLower = true, - bool includeUpper = true, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'model', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder modelStartsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.startsWith( - property: r'model', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder modelEndsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.endsWith( - property: r'model', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder modelContains( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.contains( - property: r'model', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder modelMatches( - String pattern, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.matches( - property: r'model', - wildcard: pattern, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder modelIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'model', value: ''), - ); - }); - } - - QueryBuilder modelIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan(property: r'model', value: ''), - ); - }); - } - - QueryBuilder orientationIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNull(property: r'orientation'), - ); - }); - } - - QueryBuilder - orientationIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNotNull(property: r'orientation'), - ); - }); - } - - QueryBuilder orientationEqualTo( - String? value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'orientation', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - orientationGreaterThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'orientation', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder orientationLessThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'orientation', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder orientationBetween( - String? lower, - String? upper, { - bool includeLower = true, - bool includeUpper = true, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'orientation', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder orientationStartsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.startsWith( - property: r'orientation', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder orientationEndsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.endsWith( - property: r'orientation', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder orientationContains( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.contains( - property: r'orientation', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder orientationMatches( - String pattern, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.matches( - property: r'orientation', - wildcard: pattern, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder orientationIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'orientation', value: ''), - ); - }); - } - - QueryBuilder - orientationIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan(property: r'orientation', value: ''), - ); - }); - } - - QueryBuilder stateIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNull(property: r'state'), - ); - }); - } - - QueryBuilder stateIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNotNull(property: r'state'), - ); - }); - } - - QueryBuilder stateEqualTo( - String? value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'state', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder stateGreaterThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'state', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder stateLessThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'state', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder stateBetween( - String? lower, - String? upper, { - bool includeLower = true, - bool includeUpper = true, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'state', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder stateStartsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.startsWith( - property: r'state', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder stateEndsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.endsWith( - property: r'state', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder stateContains( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.contains( - property: r'state', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder stateMatches( - String pattern, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.matches( - property: r'state', - wildcard: pattern, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder stateIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'state', value: ''), - ); - }); - } - - QueryBuilder stateIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan(property: r'state', value: ''), - ); - }); - } - - QueryBuilder timeZoneIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNull(property: r'timeZone'), - ); - }); - } - - QueryBuilder timeZoneIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNotNull(property: r'timeZone'), - ); - }); - } - - QueryBuilder timeZoneEqualTo( - String? value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'timeZone', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder timeZoneGreaterThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'timeZone', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder timeZoneLessThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'timeZone', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder timeZoneBetween( - String? lower, - String? upper, { - bool includeLower = true, - bool includeUpper = true, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'timeZone', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder timeZoneStartsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.startsWith( - property: r'timeZone', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder timeZoneEndsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.endsWith( - property: r'timeZone', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder timeZoneContains( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.contains( - property: r'timeZone', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder timeZoneMatches( - String pattern, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.matches( - property: r'timeZone', - wildcard: pattern, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder timeZoneIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'timeZone', value: ''), - ); - }); - } - - QueryBuilder timeZoneIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan(property: r'timeZone', value: ''), - ); - }); - } -} - -extension ExifInfoQueryObject - on QueryBuilder {} - -extension ExifInfoQueryLinks - on QueryBuilder {} - -extension ExifInfoQuerySortBy on QueryBuilder { - QueryBuilder sortByCity() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'city', Sort.asc); - }); - } - - QueryBuilder sortByCityDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'city', Sort.desc); - }); - } - - QueryBuilder sortByCountry() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'country', Sort.asc); - }); - } - - QueryBuilder sortByCountryDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'country', Sort.desc); - }); - } - - QueryBuilder sortByDateTimeOriginal() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'dateTimeOriginal', Sort.asc); - }); - } - - QueryBuilder sortByDateTimeOriginalDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'dateTimeOriginal', Sort.desc); - }); - } - - QueryBuilder sortByDescription() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'description', Sort.asc); - }); - } - - QueryBuilder sortByDescriptionDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'description', Sort.desc); - }); - } - - QueryBuilder sortByExposureSeconds() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'exposureSeconds', Sort.asc); - }); - } - - QueryBuilder sortByExposureSecondsDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'exposureSeconds', Sort.desc); - }); - } - - QueryBuilder sortByF() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'f', Sort.asc); - }); - } - - QueryBuilder sortByFDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'f', Sort.desc); - }); - } - - QueryBuilder sortByFileSize() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'fileSize', Sort.asc); - }); - } - - QueryBuilder sortByFileSizeDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'fileSize', Sort.desc); - }); - } - - QueryBuilder sortByIso() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'iso', Sort.asc); - }); - } - - QueryBuilder sortByIsoDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'iso', Sort.desc); - }); - } - - QueryBuilder sortByLat() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'lat', Sort.asc); - }); - } - - QueryBuilder sortByLatDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'lat', Sort.desc); - }); - } - - QueryBuilder sortByLens() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'lens', Sort.asc); - }); - } - - QueryBuilder sortByLensDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'lens', Sort.desc); - }); - } - - QueryBuilder sortByLong() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'long', Sort.asc); - }); - } - - QueryBuilder sortByLongDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'long', Sort.desc); - }); - } - - QueryBuilder sortByMake() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'make', Sort.asc); - }); - } - - QueryBuilder sortByMakeDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'make', Sort.desc); - }); - } - - QueryBuilder sortByMm() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'mm', Sort.asc); - }); - } - - QueryBuilder sortByMmDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'mm', Sort.desc); - }); - } - - QueryBuilder sortByModel() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'model', Sort.asc); - }); - } - - QueryBuilder sortByModelDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'model', Sort.desc); - }); - } - - QueryBuilder sortByOrientation() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'orientation', Sort.asc); - }); - } - - QueryBuilder sortByOrientationDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'orientation', Sort.desc); - }); - } - - QueryBuilder sortByState() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'state', Sort.asc); - }); - } - - QueryBuilder sortByStateDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'state', Sort.desc); - }); - } - - QueryBuilder sortByTimeZone() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'timeZone', Sort.asc); - }); - } - - QueryBuilder sortByTimeZoneDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'timeZone', Sort.desc); - }); - } -} - -extension ExifInfoQuerySortThenBy - on QueryBuilder { - QueryBuilder thenByCity() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'city', Sort.asc); - }); - } - - QueryBuilder thenByCityDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'city', Sort.desc); - }); - } - - QueryBuilder thenByCountry() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'country', Sort.asc); - }); - } - - QueryBuilder thenByCountryDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'country', Sort.desc); - }); - } - - QueryBuilder thenByDateTimeOriginal() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'dateTimeOriginal', Sort.asc); - }); - } - - QueryBuilder thenByDateTimeOriginalDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'dateTimeOriginal', Sort.desc); - }); - } - - QueryBuilder thenByDescription() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'description', Sort.asc); - }); - } - - QueryBuilder thenByDescriptionDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'description', Sort.desc); - }); - } - - QueryBuilder thenByExposureSeconds() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'exposureSeconds', Sort.asc); - }); - } - - QueryBuilder thenByExposureSecondsDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'exposureSeconds', Sort.desc); - }); - } - - QueryBuilder thenByF() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'f', Sort.asc); - }); - } - - QueryBuilder thenByFDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'f', Sort.desc); - }); - } - - QueryBuilder thenByFileSize() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'fileSize', Sort.asc); - }); - } - - QueryBuilder thenByFileSizeDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'fileSize', Sort.desc); - }); - } - - QueryBuilder thenById() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'id', Sort.asc); - }); - } - - QueryBuilder thenByIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'id', Sort.desc); - }); - } - - QueryBuilder thenByIso() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'iso', Sort.asc); - }); - } - - QueryBuilder thenByIsoDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'iso', Sort.desc); - }); - } - - QueryBuilder thenByLat() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'lat', Sort.asc); - }); - } - - QueryBuilder thenByLatDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'lat', Sort.desc); - }); - } - - QueryBuilder thenByLens() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'lens', Sort.asc); - }); - } - - QueryBuilder thenByLensDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'lens', Sort.desc); - }); - } - - QueryBuilder thenByLong() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'long', Sort.asc); - }); - } - - QueryBuilder thenByLongDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'long', Sort.desc); - }); - } - - QueryBuilder thenByMake() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'make', Sort.asc); - }); - } - - QueryBuilder thenByMakeDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'make', Sort.desc); - }); - } - - QueryBuilder thenByMm() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'mm', Sort.asc); - }); - } - - QueryBuilder thenByMmDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'mm', Sort.desc); - }); - } - - QueryBuilder thenByModel() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'model', Sort.asc); - }); - } - - QueryBuilder thenByModelDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'model', Sort.desc); - }); - } - - QueryBuilder thenByOrientation() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'orientation', Sort.asc); - }); - } - - QueryBuilder thenByOrientationDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'orientation', Sort.desc); - }); - } - - QueryBuilder thenByState() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'state', Sort.asc); - }); - } - - QueryBuilder thenByStateDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'state', Sort.desc); - }); - } - - QueryBuilder thenByTimeZone() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'timeZone', Sort.asc); - }); - } - - QueryBuilder thenByTimeZoneDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'timeZone', Sort.desc); - }); - } -} - -extension ExifInfoQueryWhereDistinct - on QueryBuilder { - QueryBuilder distinctByCity({ - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'city', caseSensitive: caseSensitive); - }); - } - - QueryBuilder distinctByCountry({ - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'country', caseSensitive: caseSensitive); - }); - } - - QueryBuilder distinctByDateTimeOriginal() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'dateTimeOriginal'); - }); - } - - QueryBuilder distinctByDescription({ - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'description', caseSensitive: caseSensitive); - }); - } - - QueryBuilder distinctByExposureSeconds() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'exposureSeconds'); - }); - } - - QueryBuilder distinctByF() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'f'); - }); - } - - QueryBuilder distinctByFileSize() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'fileSize'); - }); - } - - QueryBuilder distinctByIso() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'iso'); - }); - } - - QueryBuilder distinctByLat() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'lat'); - }); - } - - QueryBuilder distinctByLens({ - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'lens', caseSensitive: caseSensitive); - }); - } - - QueryBuilder distinctByLong() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'long'); - }); - } - - QueryBuilder distinctByMake({ - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'make', caseSensitive: caseSensitive); - }); - } - - QueryBuilder distinctByMm() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'mm'); - }); - } - - QueryBuilder distinctByModel({ - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'model', caseSensitive: caseSensitive); - }); - } - - QueryBuilder distinctByOrientation({ - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'orientation', caseSensitive: caseSensitive); - }); - } - - QueryBuilder distinctByState({ - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'state', caseSensitive: caseSensitive); - }); - } - - QueryBuilder distinctByTimeZone({ - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'timeZone', caseSensitive: caseSensitive); - }); - } -} - -extension ExifInfoQueryProperty - on QueryBuilder { - QueryBuilder idProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'id'); - }); - } - - QueryBuilder cityProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'city'); - }); - } - - QueryBuilder countryProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'country'); - }); - } - - QueryBuilder - dateTimeOriginalProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'dateTimeOriginal'); - }); - } - - QueryBuilder descriptionProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'description'); - }); - } - - QueryBuilder exposureSecondsProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'exposureSeconds'); - }); - } - - QueryBuilder fProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'f'); - }); - } - - QueryBuilder fileSizeProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'fileSize'); - }); - } - - QueryBuilder isoProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'iso'); - }); - } - - QueryBuilder latProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'lat'); - }); - } - - QueryBuilder lensProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'lens'); - }); - } - - QueryBuilder longProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'long'); - }); - } - - QueryBuilder makeProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'make'); - }); - } - - QueryBuilder mmProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'mm'); - }); - } - - QueryBuilder modelProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'model'); - }); - } - - QueryBuilder orientationProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'orientation'); - }); - } - - QueryBuilder stateProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'state'); - }); - } - - QueryBuilder timeZoneProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'timeZone'); - }); - } -} diff --git a/mobile/lib/infrastructure/entities/store.entity.dart b/mobile/lib/infrastructure/entities/store.entity.dart index d4b3eec84f..2de8eb713e 100644 --- a/mobile/lib/infrastructure/entities/store.entity.dart +++ b/mobile/lib/infrastructure/entities/store.entity.dart @@ -1,18 +1,5 @@ import 'package:drift/drift.dart'; import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart'; -import 'package:isar/isar.dart'; - -part 'store.entity.g.dart'; - -/// Internal class for `Store`, do not use elsewhere. -@Collection(inheritance: false) -class StoreValue { - final Id id; - final int? intValue; - final String? strValue; - - const StoreValue(this.id, {this.intValue, this.strValue}); -} class StoreEntity extends Table with DriftDefaultsMixin { IntColumn get id => integer()(); diff --git a/mobile/lib/infrastructure/entities/store.entity.g.dart b/mobile/lib/infrastructure/entities/store.entity.g.dart deleted file mode 100644 index 626c3084fe..0000000000 --- a/mobile/lib/infrastructure/entities/store.entity.g.dart +++ /dev/null @@ -1,596 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'store.entity.dart'; - -// ************************************************************************** -// IsarCollectionGenerator -// ************************************************************************** - -// coverage:ignore-file -// ignore_for_file: duplicate_ignore, non_constant_identifier_names, constant_identifier_names, invalid_use_of_protected_member, unnecessary_cast, prefer_const_constructors, lines_longer_than_80_chars, require_trailing_commas, inference_failure_on_function_invocation, unnecessary_parenthesis, unnecessary_raw_strings, unnecessary_null_checks, join_return_with_assignment, prefer_final_locals, avoid_js_rounded_ints, avoid_positional_boolean_parameters, always_specify_types - -extension GetStoreValueCollection on Isar { - IsarCollection get storeValues => this.collection(); -} - -const StoreValueSchema = CollectionSchema( - name: r'StoreValue', - id: 902899285492123510, - properties: { - r'intValue': PropertySchema(id: 0, name: r'intValue', type: IsarType.long), - r'strValue': PropertySchema( - id: 1, - name: r'strValue', - type: IsarType.string, - ), - }, - - estimateSize: _storeValueEstimateSize, - serialize: _storeValueSerialize, - deserialize: _storeValueDeserialize, - deserializeProp: _storeValueDeserializeProp, - idName: r'id', - indexes: {}, - links: {}, - embeddedSchemas: {}, - - getId: _storeValueGetId, - getLinks: _storeValueGetLinks, - attach: _storeValueAttach, - version: '3.3.0-dev.3', -); - -int _storeValueEstimateSize( - StoreValue object, - List offsets, - Map> allOffsets, -) { - var bytesCount = offsets.last; - { - final value = object.strValue; - if (value != null) { - bytesCount += 3 + value.length * 3; - } - } - return bytesCount; -} - -void _storeValueSerialize( - StoreValue object, - IsarWriter writer, - List offsets, - Map> allOffsets, -) { - writer.writeLong(offsets[0], object.intValue); - writer.writeString(offsets[1], object.strValue); -} - -StoreValue _storeValueDeserialize( - Id id, - IsarReader reader, - List offsets, - Map> allOffsets, -) { - final object = StoreValue( - id, - intValue: reader.readLongOrNull(offsets[0]), - strValue: reader.readStringOrNull(offsets[1]), - ); - return object; -} - -P _storeValueDeserializeProp

( - IsarReader reader, - int propertyId, - int offset, - Map> allOffsets, -) { - switch (propertyId) { - case 0: - return (reader.readLongOrNull(offset)) as P; - case 1: - return (reader.readStringOrNull(offset)) as P; - default: - throw IsarError('Unknown property with id $propertyId'); - } -} - -Id _storeValueGetId(StoreValue object) { - return object.id; -} - -List> _storeValueGetLinks(StoreValue object) { - return []; -} - -void _storeValueAttach(IsarCollection col, Id id, StoreValue object) {} - -extension StoreValueQueryWhereSort - on QueryBuilder { - QueryBuilder anyId() { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause(const IdWhereClause.any()); - }); - } -} - -extension StoreValueQueryWhere - on QueryBuilder { - QueryBuilder idEqualTo(Id id) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause(IdWhereClause.between(lower: id, upper: id)); - }); - } - - QueryBuilder idNotEqualTo(Id id) { - return QueryBuilder.apply(this, (query) { - if (query.whereSort == Sort.asc) { - return query - .addWhereClause( - IdWhereClause.lessThan(upper: id, includeUpper: false), - ) - .addWhereClause( - IdWhereClause.greaterThan(lower: id, includeLower: false), - ); - } else { - return query - .addWhereClause( - IdWhereClause.greaterThan(lower: id, includeLower: false), - ) - .addWhereClause( - IdWhereClause.lessThan(upper: id, includeUpper: false), - ); - } - }); - } - - QueryBuilder idGreaterThan( - Id id, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.greaterThan(lower: id, includeLower: include), - ); - }); - } - - QueryBuilder idLessThan( - Id id, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.lessThan(upper: id, includeUpper: include), - ); - }); - } - - QueryBuilder idBetween( - Id lowerId, - Id upperId, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.between( - lower: lowerId, - includeLower: includeLower, - upper: upperId, - includeUpper: includeUpper, - ), - ); - }); - } -} - -extension StoreValueQueryFilter - on QueryBuilder { - QueryBuilder idEqualTo( - Id value, - ) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'id', value: value), - ); - }); - } - - QueryBuilder idGreaterThan( - Id value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'id', - value: value, - ), - ); - }); - } - - QueryBuilder idLessThan( - Id value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'id', - value: value, - ), - ); - }); - } - - QueryBuilder idBetween( - Id lower, - Id upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'id', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder intValueIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNull(property: r'intValue'), - ); - }); - } - - QueryBuilder - intValueIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNotNull(property: r'intValue'), - ); - }); - } - - QueryBuilder intValueEqualTo( - int? value, - ) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'intValue', value: value), - ); - }); - } - - QueryBuilder - intValueGreaterThan(int? value, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'intValue', - value: value, - ), - ); - }); - } - - QueryBuilder intValueLessThan( - int? value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'intValue', - value: value, - ), - ); - }); - } - - QueryBuilder intValueBetween( - int? lower, - int? upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'intValue', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder strValueIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNull(property: r'strValue'), - ); - }); - } - - QueryBuilder - strValueIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNotNull(property: r'strValue'), - ); - }); - } - - QueryBuilder strValueEqualTo( - String? value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'strValue', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - strValueGreaterThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'strValue', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder strValueLessThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'strValue', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder strValueBetween( - String? lower, - String? upper, { - bool includeLower = true, - bool includeUpper = true, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'strValue', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - strValueStartsWith(String value, {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.startsWith( - property: r'strValue', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder strValueEndsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.endsWith( - property: r'strValue', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder strValueContains( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.contains( - property: r'strValue', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder strValueMatches( - String pattern, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.matches( - property: r'strValue', - wildcard: pattern, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - strValueIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'strValue', value: ''), - ); - }); - } - - QueryBuilder - strValueIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan(property: r'strValue', value: ''), - ); - }); - } -} - -extension StoreValueQueryObject - on QueryBuilder {} - -extension StoreValueQueryLinks - on QueryBuilder {} - -extension StoreValueQuerySortBy - on QueryBuilder { - QueryBuilder sortByIntValue() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'intValue', Sort.asc); - }); - } - - QueryBuilder sortByIntValueDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'intValue', Sort.desc); - }); - } - - QueryBuilder sortByStrValue() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'strValue', Sort.asc); - }); - } - - QueryBuilder sortByStrValueDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'strValue', Sort.desc); - }); - } -} - -extension StoreValueQuerySortThenBy - on QueryBuilder { - QueryBuilder thenById() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'id', Sort.asc); - }); - } - - QueryBuilder thenByIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'id', Sort.desc); - }); - } - - QueryBuilder thenByIntValue() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'intValue', Sort.asc); - }); - } - - QueryBuilder thenByIntValueDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'intValue', Sort.desc); - }); - } - - QueryBuilder thenByStrValue() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'strValue', Sort.asc); - }); - } - - QueryBuilder thenByStrValueDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'strValue', Sort.desc); - }); - } -} - -extension StoreValueQueryWhereDistinct - on QueryBuilder { - QueryBuilder distinctByIntValue() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'intValue'); - }); - } - - QueryBuilder distinctByStrValue({ - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'strValue', caseSensitive: caseSensitive); - }); - } -} - -extension StoreValueQueryProperty - on QueryBuilder { - QueryBuilder idProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'id'); - }); - } - - QueryBuilder intValueProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'intValue'); - }); - } - - QueryBuilder strValueProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'strValue'); - }); - } -} diff --git a/mobile/lib/infrastructure/entities/user.entity.dart b/mobile/lib/infrastructure/entities/user.entity.dart index 667a9d6a59..8d4371672c 100644 --- a/mobile/lib/infrastructure/entities/user.entity.dart +++ b/mobile/lib/infrastructure/entities/user.entity.dart @@ -1,79 +1,6 @@ import 'package:drift/drift.dart' hide Index; import 'package:immich_mobile/domain/models/user.model.dart'; import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart'; -import 'package:immich_mobile/utils/hash.dart'; -import 'package:isar/isar.dart'; - -part 'user.entity.g.dart'; - -@Collection(inheritance: false) -class User { - Id get isarId => fastHash(id); - @Index(unique: true, replace: false, type: IndexType.hash) - final String id; - final DateTime updatedAt; - final String email; - final String name; - final bool isPartnerSharedBy; - final bool isPartnerSharedWith; - final bool isAdmin; - final String profileImagePath; - @Enumerated(EnumType.ordinal) - final AvatarColor avatarColor; - final bool memoryEnabled; - final bool inTimeline; - final int quotaUsageInBytes; - final int quotaSizeInBytes; - - const User({ - required this.id, - required this.updatedAt, - required this.email, - required this.name, - required this.isAdmin, - this.isPartnerSharedBy = false, - this.isPartnerSharedWith = false, - this.profileImagePath = '', - this.avatarColor = AvatarColor.primary, - this.memoryEnabled = true, - this.inTimeline = false, - this.quotaUsageInBytes = 0, - this.quotaSizeInBytes = 0, - }); - - static User fromDto(UserDto dto) => User( - id: dto.id, - updatedAt: dto.updatedAt ?? DateTime(2025), - email: dto.email, - name: dto.name, - isAdmin: dto.isAdmin, - isPartnerSharedBy: dto.isPartnerSharedBy, - isPartnerSharedWith: dto.isPartnerSharedWith, - profileImagePath: dto.hasProfileImage ? "HAS_PROFILE_IMAGE" : "", - avatarColor: dto.avatarColor, - memoryEnabled: dto.memoryEnabled, - inTimeline: dto.inTimeline, - quotaUsageInBytes: dto.quotaUsageInBytes, - quotaSizeInBytes: dto.quotaSizeInBytes, - ); - - UserDto toDto() => UserDto( - id: id, - email: email, - name: name, - isAdmin: isAdmin, - updatedAt: updatedAt, - avatarColor: avatarColor, - memoryEnabled: memoryEnabled, - inTimeline: inTimeline, - isPartnerSharedBy: isPartnerSharedBy, - isPartnerSharedWith: isPartnerSharedWith, - hasProfileImage: profileImagePath.isNotEmpty, - profileChangedAt: updatedAt, - quotaUsageInBytes: quotaUsageInBytes, - quotaSizeInBytes: quotaSizeInBytes, - ); -} class UserEntity extends Table with DriftDefaultsMixin { const UserEntity(); diff --git a/mobile/lib/infrastructure/entities/user.entity.g.dart b/mobile/lib/infrastructure/entities/user.entity.g.dart deleted file mode 100644 index 7e0af41b77..0000000000 --- a/mobile/lib/infrastructure/entities/user.entity.g.dart +++ /dev/null @@ -1,1854 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'user.entity.dart'; - -// ************************************************************************** -// IsarCollectionGenerator -// ************************************************************************** - -// coverage:ignore-file -// ignore_for_file: duplicate_ignore, non_constant_identifier_names, constant_identifier_names, invalid_use_of_protected_member, unnecessary_cast, prefer_const_constructors, lines_longer_than_80_chars, require_trailing_commas, inference_failure_on_function_invocation, unnecessary_parenthesis, unnecessary_raw_strings, unnecessary_null_checks, join_return_with_assignment, prefer_final_locals, avoid_js_rounded_ints, avoid_positional_boolean_parameters, always_specify_types - -extension GetUserCollection on Isar { - IsarCollection get users => this.collection(); -} - -const UserSchema = CollectionSchema( - name: r'User', - id: -7838171048429979076, - properties: { - r'avatarColor': PropertySchema( - id: 0, - name: r'avatarColor', - type: IsarType.byte, - enumMap: _UseravatarColorEnumValueMap, - ), - r'email': PropertySchema(id: 1, name: r'email', type: IsarType.string), - r'id': PropertySchema(id: 2, name: r'id', type: IsarType.string), - r'inTimeline': PropertySchema( - id: 3, - name: r'inTimeline', - type: IsarType.bool, - ), - r'isAdmin': PropertySchema(id: 4, name: r'isAdmin', type: IsarType.bool), - r'isPartnerSharedBy': PropertySchema( - id: 5, - name: r'isPartnerSharedBy', - type: IsarType.bool, - ), - r'isPartnerSharedWith': PropertySchema( - id: 6, - name: r'isPartnerSharedWith', - type: IsarType.bool, - ), - r'memoryEnabled': PropertySchema( - id: 7, - name: r'memoryEnabled', - type: IsarType.bool, - ), - r'name': PropertySchema(id: 8, name: r'name', type: IsarType.string), - r'profileImagePath': PropertySchema( - id: 9, - name: r'profileImagePath', - type: IsarType.string, - ), - r'quotaSizeInBytes': PropertySchema( - id: 10, - name: r'quotaSizeInBytes', - type: IsarType.long, - ), - r'quotaUsageInBytes': PropertySchema( - id: 11, - name: r'quotaUsageInBytes', - type: IsarType.long, - ), - r'updatedAt': PropertySchema( - id: 12, - name: r'updatedAt', - type: IsarType.dateTime, - ), - }, - - estimateSize: _userEstimateSize, - serialize: _userSerialize, - deserialize: _userDeserialize, - deserializeProp: _userDeserializeProp, - idName: r'isarId', - indexes: { - r'id': IndexSchema( - id: -3268401673993471357, - name: r'id', - unique: true, - replace: false, - properties: [ - IndexPropertySchema( - name: r'id', - type: IndexType.hash, - caseSensitive: true, - ), - ], - ), - }, - links: {}, - embeddedSchemas: {}, - - getId: _userGetId, - getLinks: _userGetLinks, - attach: _userAttach, - version: '3.3.0-dev.3', -); - -int _userEstimateSize( - User object, - List offsets, - Map> allOffsets, -) { - var bytesCount = offsets.last; - bytesCount += 3 + object.email.length * 3; - bytesCount += 3 + object.id.length * 3; - bytesCount += 3 + object.name.length * 3; - bytesCount += 3 + object.profileImagePath.length * 3; - return bytesCount; -} - -void _userSerialize( - User object, - IsarWriter writer, - List offsets, - Map> allOffsets, -) { - writer.writeByte(offsets[0], object.avatarColor.index); - writer.writeString(offsets[1], object.email); - writer.writeString(offsets[2], object.id); - writer.writeBool(offsets[3], object.inTimeline); - writer.writeBool(offsets[4], object.isAdmin); - writer.writeBool(offsets[5], object.isPartnerSharedBy); - writer.writeBool(offsets[6], object.isPartnerSharedWith); - writer.writeBool(offsets[7], object.memoryEnabled); - writer.writeString(offsets[8], object.name); - writer.writeString(offsets[9], object.profileImagePath); - writer.writeLong(offsets[10], object.quotaSizeInBytes); - writer.writeLong(offsets[11], object.quotaUsageInBytes); - writer.writeDateTime(offsets[12], object.updatedAt); -} - -User _userDeserialize( - Id id, - IsarReader reader, - List offsets, - Map> allOffsets, -) { - final object = User( - avatarColor: - _UseravatarColorValueEnumMap[reader.readByteOrNull(offsets[0])] ?? - AvatarColor.primary, - email: reader.readString(offsets[1]), - id: reader.readString(offsets[2]), - inTimeline: reader.readBoolOrNull(offsets[3]) ?? false, - isAdmin: reader.readBool(offsets[4]), - isPartnerSharedBy: reader.readBoolOrNull(offsets[5]) ?? false, - isPartnerSharedWith: reader.readBoolOrNull(offsets[6]) ?? false, - memoryEnabled: reader.readBoolOrNull(offsets[7]) ?? true, - name: reader.readString(offsets[8]), - profileImagePath: reader.readStringOrNull(offsets[9]) ?? '', - quotaSizeInBytes: reader.readLongOrNull(offsets[10]) ?? 0, - quotaUsageInBytes: reader.readLongOrNull(offsets[11]) ?? 0, - updatedAt: reader.readDateTime(offsets[12]), - ); - return object; -} - -P _userDeserializeProp

( - IsarReader reader, - int propertyId, - int offset, - Map> allOffsets, -) { - switch (propertyId) { - case 0: - return (_UseravatarColorValueEnumMap[reader.readByteOrNull(offset)] ?? - AvatarColor.primary) - as P; - case 1: - return (reader.readString(offset)) as P; - case 2: - return (reader.readString(offset)) as P; - case 3: - return (reader.readBoolOrNull(offset) ?? false) as P; - case 4: - return (reader.readBool(offset)) as P; - case 5: - return (reader.readBoolOrNull(offset) ?? false) as P; - case 6: - return (reader.readBoolOrNull(offset) ?? false) as P; - case 7: - return (reader.readBoolOrNull(offset) ?? true) as P; - case 8: - return (reader.readString(offset)) as P; - case 9: - return (reader.readStringOrNull(offset) ?? '') as P; - case 10: - return (reader.readLongOrNull(offset) ?? 0) as P; - case 11: - return (reader.readLongOrNull(offset) ?? 0) as P; - case 12: - return (reader.readDateTime(offset)) as P; - default: - throw IsarError('Unknown property with id $propertyId'); - } -} - -const _UseravatarColorEnumValueMap = { - 'primary': 0, - 'pink': 1, - 'red': 2, - 'yellow': 3, - 'blue': 4, - 'green': 5, - 'purple': 6, - 'orange': 7, - 'gray': 8, - 'amber': 9, -}; -const _UseravatarColorValueEnumMap = { - 0: AvatarColor.primary, - 1: AvatarColor.pink, - 2: AvatarColor.red, - 3: AvatarColor.yellow, - 4: AvatarColor.blue, - 5: AvatarColor.green, - 6: AvatarColor.purple, - 7: AvatarColor.orange, - 8: AvatarColor.gray, - 9: AvatarColor.amber, -}; - -Id _userGetId(User object) { - return object.isarId; -} - -List> _userGetLinks(User object) { - return []; -} - -void _userAttach(IsarCollection col, Id id, User object) {} - -extension UserByIndex on IsarCollection { - Future getById(String id) { - return getByIndex(r'id', [id]); - } - - User? getByIdSync(String id) { - return getByIndexSync(r'id', [id]); - } - - Future deleteById(String id) { - return deleteByIndex(r'id', [id]); - } - - bool deleteByIdSync(String id) { - return deleteByIndexSync(r'id', [id]); - } - - Future> getAllById(List idValues) { - final values = idValues.map((e) => [e]).toList(); - return getAllByIndex(r'id', values); - } - - List getAllByIdSync(List idValues) { - final values = idValues.map((e) => [e]).toList(); - return getAllByIndexSync(r'id', values); - } - - Future deleteAllById(List idValues) { - final values = idValues.map((e) => [e]).toList(); - return deleteAllByIndex(r'id', values); - } - - int deleteAllByIdSync(List idValues) { - final values = idValues.map((e) => [e]).toList(); - return deleteAllByIndexSync(r'id', values); - } - - Future putById(User object) { - return putByIndex(r'id', object); - } - - Id putByIdSync(User object, {bool saveLinks = true}) { - return putByIndexSync(r'id', object, saveLinks: saveLinks); - } - - Future> putAllById(List objects) { - return putAllByIndex(r'id', objects); - } - - List putAllByIdSync(List objects, {bool saveLinks = true}) { - return putAllByIndexSync(r'id', objects, saveLinks: saveLinks); - } -} - -extension UserQueryWhereSort on QueryBuilder { - QueryBuilder anyIsarId() { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause(const IdWhereClause.any()); - }); - } -} - -extension UserQueryWhere on QueryBuilder { - QueryBuilder isarIdEqualTo(Id isarId) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.between(lower: isarId, upper: isarId), - ); - }); - } - - QueryBuilder isarIdNotEqualTo(Id isarId) { - return QueryBuilder.apply(this, (query) { - if (query.whereSort == Sort.asc) { - return query - .addWhereClause( - IdWhereClause.lessThan(upper: isarId, includeUpper: false), - ) - .addWhereClause( - IdWhereClause.greaterThan(lower: isarId, includeLower: false), - ); - } else { - return query - .addWhereClause( - IdWhereClause.greaterThan(lower: isarId, includeLower: false), - ) - .addWhereClause( - IdWhereClause.lessThan(upper: isarId, includeUpper: false), - ); - } - }); - } - - QueryBuilder isarIdGreaterThan( - Id isarId, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.greaterThan(lower: isarId, includeLower: include), - ); - }); - } - - QueryBuilder isarIdLessThan( - Id isarId, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.lessThan(upper: isarId, includeUpper: include), - ); - }); - } - - QueryBuilder isarIdBetween( - Id lowerIsarId, - Id upperIsarId, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.between( - lower: lowerIsarId, - includeLower: includeLower, - upper: upperIsarId, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder idEqualTo(String id) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IndexWhereClause.equalTo(indexName: r'id', value: [id]), - ); - }); - } - - QueryBuilder idNotEqualTo(String id) { - return QueryBuilder.apply(this, (query) { - if (query.whereSort == Sort.asc) { - return query - .addWhereClause( - IndexWhereClause.between( - indexName: r'id', - lower: [], - upper: [id], - includeUpper: false, - ), - ) - .addWhereClause( - IndexWhereClause.between( - indexName: r'id', - lower: [id], - includeLower: false, - upper: [], - ), - ); - } else { - return query - .addWhereClause( - IndexWhereClause.between( - indexName: r'id', - lower: [id], - includeLower: false, - upper: [], - ), - ) - .addWhereClause( - IndexWhereClause.between( - indexName: r'id', - lower: [], - upper: [id], - includeUpper: false, - ), - ); - } - }); - } -} - -extension UserQueryFilter on QueryBuilder { - QueryBuilder avatarColorEqualTo( - AvatarColor value, - ) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'avatarColor', value: value), - ); - }); - } - - QueryBuilder avatarColorGreaterThan( - AvatarColor value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'avatarColor', - value: value, - ), - ); - }); - } - - QueryBuilder avatarColorLessThan( - AvatarColor value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'avatarColor', - value: value, - ), - ); - }); - } - - QueryBuilder avatarColorBetween( - AvatarColor lower, - AvatarColor upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'avatarColor', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder emailEqualTo( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'email', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder emailGreaterThan( - String value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'email', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder emailLessThan( - String value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'email', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder emailBetween( - String lower, - String upper, { - bool includeLower = true, - bool includeUpper = true, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'email', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder emailStartsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.startsWith( - property: r'email', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder emailEndsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.endsWith( - property: r'email', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder emailContains( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.contains( - property: r'email', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder emailMatches( - String pattern, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.matches( - property: r'email', - wildcard: pattern, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder emailIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'email', value: ''), - ); - }); - } - - QueryBuilder emailIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan(property: r'email', value: ''), - ); - }); - } - - QueryBuilder idEqualTo( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'id', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder idGreaterThan( - String value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'id', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder idLessThan( - String value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'id', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder idBetween( - String lower, - String upper, { - bool includeLower = true, - bool includeUpper = true, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'id', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder idStartsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.startsWith( - property: r'id', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder idEndsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.endsWith( - property: r'id', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder idContains( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.contains( - property: r'id', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder idMatches( - String pattern, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.matches( - property: r'id', - wildcard: pattern, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder idIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'id', value: ''), - ); - }); - } - - QueryBuilder idIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan(property: r'id', value: ''), - ); - }); - } - - QueryBuilder inTimelineEqualTo( - bool value, - ) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'inTimeline', value: value), - ); - }); - } - - QueryBuilder isAdminEqualTo(bool value) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'isAdmin', value: value), - ); - }); - } - - QueryBuilder isPartnerSharedByEqualTo( - bool value, - ) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'isPartnerSharedBy', value: value), - ); - }); - } - - QueryBuilder isPartnerSharedWithEqualTo( - bool value, - ) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'isPartnerSharedWith', value: value), - ); - }); - } - - QueryBuilder isarIdEqualTo(Id value) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'isarId', value: value), - ); - }); - } - - QueryBuilder isarIdGreaterThan( - Id value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'isarId', - value: value, - ), - ); - }); - } - - QueryBuilder isarIdLessThan( - Id value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'isarId', - value: value, - ), - ); - }); - } - - QueryBuilder isarIdBetween( - Id lower, - Id upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'isarId', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder memoryEnabledEqualTo( - bool value, - ) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'memoryEnabled', value: value), - ); - }); - } - - QueryBuilder nameEqualTo( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'name', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder nameGreaterThan( - String value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'name', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder nameLessThan( - String value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'name', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder nameBetween( - String lower, - String upper, { - bool includeLower = true, - bool includeUpper = true, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'name', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder nameStartsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.startsWith( - property: r'name', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder nameEndsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.endsWith( - property: r'name', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder nameContains( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.contains( - property: r'name', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder nameMatches( - String pattern, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.matches( - property: r'name', - wildcard: pattern, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder nameIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'name', value: ''), - ); - }); - } - - QueryBuilder nameIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan(property: r'name', value: ''), - ); - }); - } - - QueryBuilder profileImagePathEqualTo( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'profileImagePath', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder profileImagePathGreaterThan( - String value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'profileImagePath', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder profileImagePathLessThan( - String value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'profileImagePath', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder profileImagePathBetween( - String lower, - String upper, { - bool includeLower = true, - bool includeUpper = true, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'profileImagePath', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder profileImagePathStartsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.startsWith( - property: r'profileImagePath', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder profileImagePathEndsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.endsWith( - property: r'profileImagePath', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder profileImagePathContains( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.contains( - property: r'profileImagePath', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder profileImagePathMatches( - String pattern, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.matches( - property: r'profileImagePath', - wildcard: pattern, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder profileImagePathIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'profileImagePath', value: ''), - ); - }); - } - - QueryBuilder profileImagePathIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan(property: r'profileImagePath', value: ''), - ); - }); - } - - QueryBuilder quotaSizeInBytesEqualTo( - int value, - ) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'quotaSizeInBytes', value: value), - ); - }); - } - - QueryBuilder quotaSizeInBytesGreaterThan( - int value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'quotaSizeInBytes', - value: value, - ), - ); - }); - } - - QueryBuilder quotaSizeInBytesLessThan( - int value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'quotaSizeInBytes', - value: value, - ), - ); - }); - } - - QueryBuilder quotaSizeInBytesBetween( - int lower, - int upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'quotaSizeInBytes', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder quotaUsageInBytesEqualTo( - int value, - ) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'quotaUsageInBytes', value: value), - ); - }); - } - - QueryBuilder quotaUsageInBytesGreaterThan( - int value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'quotaUsageInBytes', - value: value, - ), - ); - }); - } - - QueryBuilder quotaUsageInBytesLessThan( - int value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'quotaUsageInBytes', - value: value, - ), - ); - }); - } - - QueryBuilder quotaUsageInBytesBetween( - int lower, - int upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'quotaUsageInBytes', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder updatedAtEqualTo( - DateTime value, - ) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'updatedAt', value: value), - ); - }); - } - - QueryBuilder updatedAtGreaterThan( - DateTime value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'updatedAt', - value: value, - ), - ); - }); - } - - QueryBuilder updatedAtLessThan( - DateTime value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'updatedAt', - value: value, - ), - ); - }); - } - - QueryBuilder updatedAtBetween( - DateTime lower, - DateTime upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'updatedAt', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } -} - -extension UserQueryObject on QueryBuilder {} - -extension UserQueryLinks on QueryBuilder {} - -extension UserQuerySortBy on QueryBuilder { - QueryBuilder sortByAvatarColor() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'avatarColor', Sort.asc); - }); - } - - QueryBuilder sortByAvatarColorDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'avatarColor', Sort.desc); - }); - } - - QueryBuilder sortByEmail() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'email', Sort.asc); - }); - } - - QueryBuilder sortByEmailDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'email', Sort.desc); - }); - } - - QueryBuilder sortById() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'id', Sort.asc); - }); - } - - QueryBuilder sortByIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'id', Sort.desc); - }); - } - - QueryBuilder sortByInTimeline() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'inTimeline', Sort.asc); - }); - } - - QueryBuilder sortByInTimelineDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'inTimeline', Sort.desc); - }); - } - - QueryBuilder sortByIsAdmin() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isAdmin', Sort.asc); - }); - } - - QueryBuilder sortByIsAdminDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isAdmin', Sort.desc); - }); - } - - QueryBuilder sortByIsPartnerSharedBy() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isPartnerSharedBy', Sort.asc); - }); - } - - QueryBuilder sortByIsPartnerSharedByDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isPartnerSharedBy', Sort.desc); - }); - } - - QueryBuilder sortByIsPartnerSharedWith() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isPartnerSharedWith', Sort.asc); - }); - } - - QueryBuilder sortByIsPartnerSharedWithDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isPartnerSharedWith', Sort.desc); - }); - } - - QueryBuilder sortByMemoryEnabled() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'memoryEnabled', Sort.asc); - }); - } - - QueryBuilder sortByMemoryEnabledDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'memoryEnabled', Sort.desc); - }); - } - - QueryBuilder sortByName() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'name', Sort.asc); - }); - } - - QueryBuilder sortByNameDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'name', Sort.desc); - }); - } - - QueryBuilder sortByProfileImagePath() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'profileImagePath', Sort.asc); - }); - } - - QueryBuilder sortByProfileImagePathDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'profileImagePath', Sort.desc); - }); - } - - QueryBuilder sortByQuotaSizeInBytes() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'quotaSizeInBytes', Sort.asc); - }); - } - - QueryBuilder sortByQuotaSizeInBytesDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'quotaSizeInBytes', Sort.desc); - }); - } - - QueryBuilder sortByQuotaUsageInBytes() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'quotaUsageInBytes', Sort.asc); - }); - } - - QueryBuilder sortByQuotaUsageInBytesDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'quotaUsageInBytes', Sort.desc); - }); - } - - QueryBuilder sortByUpdatedAt() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'updatedAt', Sort.asc); - }); - } - - QueryBuilder sortByUpdatedAtDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'updatedAt', Sort.desc); - }); - } -} - -extension UserQuerySortThenBy on QueryBuilder { - QueryBuilder thenByAvatarColor() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'avatarColor', Sort.asc); - }); - } - - QueryBuilder thenByAvatarColorDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'avatarColor', Sort.desc); - }); - } - - QueryBuilder thenByEmail() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'email', Sort.asc); - }); - } - - QueryBuilder thenByEmailDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'email', Sort.desc); - }); - } - - QueryBuilder thenById() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'id', Sort.asc); - }); - } - - QueryBuilder thenByIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'id', Sort.desc); - }); - } - - QueryBuilder thenByInTimeline() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'inTimeline', Sort.asc); - }); - } - - QueryBuilder thenByInTimelineDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'inTimeline', Sort.desc); - }); - } - - QueryBuilder thenByIsAdmin() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isAdmin', Sort.asc); - }); - } - - QueryBuilder thenByIsAdminDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isAdmin', Sort.desc); - }); - } - - QueryBuilder thenByIsPartnerSharedBy() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isPartnerSharedBy', Sort.asc); - }); - } - - QueryBuilder thenByIsPartnerSharedByDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isPartnerSharedBy', Sort.desc); - }); - } - - QueryBuilder thenByIsPartnerSharedWith() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isPartnerSharedWith', Sort.asc); - }); - } - - QueryBuilder thenByIsPartnerSharedWithDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isPartnerSharedWith', Sort.desc); - }); - } - - QueryBuilder thenByIsarId() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isarId', Sort.asc); - }); - } - - QueryBuilder thenByIsarIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isarId', Sort.desc); - }); - } - - QueryBuilder thenByMemoryEnabled() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'memoryEnabled', Sort.asc); - }); - } - - QueryBuilder thenByMemoryEnabledDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'memoryEnabled', Sort.desc); - }); - } - - QueryBuilder thenByName() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'name', Sort.asc); - }); - } - - QueryBuilder thenByNameDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'name', Sort.desc); - }); - } - - QueryBuilder thenByProfileImagePath() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'profileImagePath', Sort.asc); - }); - } - - QueryBuilder thenByProfileImagePathDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'profileImagePath', Sort.desc); - }); - } - - QueryBuilder thenByQuotaSizeInBytes() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'quotaSizeInBytes', Sort.asc); - }); - } - - QueryBuilder thenByQuotaSizeInBytesDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'quotaSizeInBytes', Sort.desc); - }); - } - - QueryBuilder thenByQuotaUsageInBytes() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'quotaUsageInBytes', Sort.asc); - }); - } - - QueryBuilder thenByQuotaUsageInBytesDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'quotaUsageInBytes', Sort.desc); - }); - } - - QueryBuilder thenByUpdatedAt() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'updatedAt', Sort.asc); - }); - } - - QueryBuilder thenByUpdatedAtDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'updatedAt', Sort.desc); - }); - } -} - -extension UserQueryWhereDistinct on QueryBuilder { - QueryBuilder distinctByAvatarColor() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'avatarColor'); - }); - } - - QueryBuilder distinctByEmail({ - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'email', caseSensitive: caseSensitive); - }); - } - - QueryBuilder distinctById({ - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'id', caseSensitive: caseSensitive); - }); - } - - QueryBuilder distinctByInTimeline() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'inTimeline'); - }); - } - - QueryBuilder distinctByIsAdmin() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'isAdmin'); - }); - } - - QueryBuilder distinctByIsPartnerSharedBy() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'isPartnerSharedBy'); - }); - } - - QueryBuilder distinctByIsPartnerSharedWith() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'isPartnerSharedWith'); - }); - } - - QueryBuilder distinctByMemoryEnabled() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'memoryEnabled'); - }); - } - - QueryBuilder distinctByName({ - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'name', caseSensitive: caseSensitive); - }); - } - - QueryBuilder distinctByProfileImagePath({ - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy( - r'profileImagePath', - caseSensitive: caseSensitive, - ); - }); - } - - QueryBuilder distinctByQuotaSizeInBytes() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'quotaSizeInBytes'); - }); - } - - QueryBuilder distinctByQuotaUsageInBytes() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'quotaUsageInBytes'); - }); - } - - QueryBuilder distinctByUpdatedAt() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'updatedAt'); - }); - } -} - -extension UserQueryProperty on QueryBuilder { - QueryBuilder isarIdProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'isarId'); - }); - } - - QueryBuilder avatarColorProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'avatarColor'); - }); - } - - QueryBuilder emailProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'email'); - }); - } - - QueryBuilder idProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'id'); - }); - } - - QueryBuilder inTimelineProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'inTimeline'); - }); - } - - QueryBuilder isAdminProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'isAdmin'); - }); - } - - QueryBuilder isPartnerSharedByProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'isPartnerSharedBy'); - }); - } - - QueryBuilder isPartnerSharedWithProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'isPartnerSharedWith'); - }); - } - - QueryBuilder memoryEnabledProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'memoryEnabled'); - }); - } - - QueryBuilder nameProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'name'); - }); - } - - QueryBuilder profileImagePathProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'profileImagePath'); - }); - } - - QueryBuilder quotaSizeInBytesProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'quotaSizeInBytes'); - }); - } - - QueryBuilder quotaUsageInBytesProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'quotaUsageInBytes'); - }); - } - - QueryBuilder updatedAtProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'updatedAt'); - }); - } -} diff --git a/mobile/lib/infrastructure/repositories/db.repository.dart b/mobile/lib/infrastructure/repositories/db.repository.dart index d41891e2ea..eca8810b91 100644 --- a/mobile/lib/infrastructure/repositories/db.repository.dart +++ b/mobile/lib/infrastructure/repositories/db.repository.dart @@ -3,7 +3,6 @@ import 'dart:async'; import 'package:drift/drift.dart'; import 'package:drift_flutter/drift_flutter.dart'; import 'package:flutter/foundation.dart'; -import 'package:immich_mobile/domain/interfaces/db.interface.dart'; import 'package:immich_mobile/infrastructure/entities/asset_edit.entity.dart'; import 'package:immich_mobile/infrastructure/entities/asset_face.entity.dart'; import 'package:immich_mobile/infrastructure/entities/auth_user.entity.dart'; @@ -27,22 +26,6 @@ import 'package:immich_mobile/infrastructure/entities/user.entity.dart'; import 'package:immich_mobile/infrastructure/entities/user_metadata.entity.dart'; import 'package:immich_mobile/infrastructure/repositories/db.repository.drift.dart'; import 'package:immich_mobile/infrastructure/repositories/db.repository.steps.dart'; -import 'package:isar/isar.dart' hide Index; - -// #zoneTxn is the symbol used by Isar to mark a transaction within the current zone -// ref: isar/isar_common.dart -const Symbol _kzoneTxn = #zoneTxn; - -class IsarDatabaseRepository implements IDatabaseRepository { - final Isar _db; - const IsarDatabaseRepository(Isar db) : _db = db; - - // Isar do not support nested transactions. This is a workaround to prevent us from making nested transactions - // Reuse the current transaction if it is already active, else start a new transaction - @override - Future transaction(Future Function() callback) => - Zone.current[_kzoneTxn] == null ? _db.writeTxn(callback) : callback(); -} @DriftDatabase( tables: [ @@ -70,7 +53,7 @@ class IsarDatabaseRepository implements IDatabaseRepository { ], include: {'package:immich_mobile/infrastructure/entities/merged_asset.drift'}, ) -class Drift extends $Drift implements IDatabaseRepository { +class Drift extends $Drift { Drift([QueryExecutor? executor]) : super(executor ?? driftDatabase(name: 'immich', native: const DriftNativeOptions(shareAcrossIsolates: true))); @@ -261,10 +244,9 @@ class Drift extends $Drift implements IDatabaseRepository { ); } -class DriftDatabaseRepository implements IDatabaseRepository { +class DriftDatabaseRepository { final Drift _db; const DriftDatabaseRepository(this._db); - @override Future transaction(Future Function() callback) => _db.transaction(callback); } diff --git a/mobile/lib/infrastructure/repositories/device_asset.repository.dart b/mobile/lib/infrastructure/repositories/device_asset.repository.dart deleted file mode 100644 index 73ee148ab3..0000000000 --- a/mobile/lib/infrastructure/repositories/device_asset.repository.dart +++ /dev/null @@ -1,31 +0,0 @@ -import 'package:immich_mobile/domain/models/device_asset.model.dart'; -import 'package:immich_mobile/infrastructure/entities/device_asset.entity.dart'; -import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; -import 'package:isar/isar.dart'; - -class IsarDeviceAssetRepository extends IsarDatabaseRepository { - final Isar _db; - - const IsarDeviceAssetRepository(this._db) : super(_db); - - Future deleteIds(List ids) { - return transaction(() async { - await _db.deviceAssetEntitys.deleteAllByAssetId(ids.toList()); - }); - } - - Future> getByIds(List localIds) { - return _db.deviceAssetEntitys - .where() - .anyOf(localIds, (query, id) => query.assetIdEqualTo(id)) - .findAll() - .then((value) => value.map((e) => e.toModel()).toList()); - } - - Future updateAll(List assetHash) { - return transaction(() async { - await _db.deviceAssetEntitys.putAll(assetHash.map(DeviceAssetEntity.fromDto).toList()); - return true; - }); - } -} diff --git a/mobile/lib/infrastructure/repositories/exif.repository.dart b/mobile/lib/infrastructure/repositories/exif.repository.dart deleted file mode 100644 index 0ede30680e..0000000000 --- a/mobile/lib/infrastructure/repositories/exif.repository.dart +++ /dev/null @@ -1,40 +0,0 @@ -import 'package:immich_mobile/domain/models/exif.model.dart'; -import 'package:immich_mobile/infrastructure/entities/exif.entity.dart' as entity; -import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; -import 'package:isar/isar.dart'; - -class IsarExifRepository extends IsarDatabaseRepository { - final Isar _db; - - const IsarExifRepository(this._db) : super(_db); - - Future delete(int assetId) async { - await transaction(() async { - await _db.exifInfos.delete(assetId); - }); - } - - Future deleteAll() async { - await transaction(() async { - await _db.exifInfos.clear(); - }); - } - - Future get(int assetId) async { - return (await _db.exifInfos.get(assetId))?.toDto(); - } - - Future update(ExifInfo exifInfo) { - return transaction(() async { - await _db.exifInfos.put(entity.ExifInfo.fromDto(exifInfo)); - return exifInfo; - }); - } - - Future> updateAll(List exifInfos) { - return transaction(() async { - await _db.exifInfos.putAll(exifInfos.map(entity.ExifInfo.fromDto).toList()); - return exifInfos; - }); - } -} diff --git a/mobile/lib/infrastructure/repositories/logger_db.repository.dart b/mobile/lib/infrastructure/repositories/logger_db.repository.dart index e494782fa6..d11174356d 100644 --- a/mobile/lib/infrastructure/repositories/logger_db.repository.dart +++ b/mobile/lib/infrastructure/repositories/logger_db.repository.dart @@ -1,11 +1,10 @@ import 'package:drift/drift.dart'; import 'package:drift_flutter/drift_flutter.dart'; -import 'package:immich_mobile/domain/interfaces/db.interface.dart'; import 'package:immich_mobile/infrastructure/entities/log.entity.dart'; import 'package:immich_mobile/infrastructure/repositories/logger_db.repository.drift.dart'; @DriftDatabase(tables: [LogMessageEntity]) -class DriftLogger extends $DriftLogger implements IDatabaseRepository { +class DriftLogger extends $DriftLogger { DriftLogger([QueryExecutor? executor]) : super( executor ?? driftDatabase(name: 'immich_logs', native: const DriftNativeOptions(shareAcrossIsolates: true)), diff --git a/mobile/lib/infrastructure/repositories/remote_asset.repository.dart b/mobile/lib/infrastructure/repositories/remote_asset.repository.dart index 00c0b81850..6d19d17931 100644 --- a/mobile/lib/infrastructure/repositories/remote_asset.repository.dart +++ b/mobile/lib/infrastructure/repositories/remote_asset.repository.dart @@ -4,7 +4,7 @@ import 'package:immich_mobile/domain/models/asset_edit.model.dart'; import 'package:immich_mobile/domain/models/exif.model.dart'; import 'package:immich_mobile/domain/models/stack.model.dart'; import 'package:immich_mobile/infrastructure/entities/asset_edit.entity.dart'; -import 'package:immich_mobile/infrastructure/entities/exif.entity.dart' hide ExifInfo; +import 'package:immich_mobile/infrastructure/entities/exif.entity.dart'; import 'package:immich_mobile/infrastructure/entities/exif.entity.drift.dart'; import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.dart'; import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.drift.dart'; diff --git a/mobile/lib/infrastructure/repositories/store.repository.dart b/mobile/lib/infrastructure/repositories/store.repository.dart index d4e34a02f5..9680aa0425 100644 --- a/mobile/lib/infrastructure/repositories/store.repository.dart +++ b/mobile/lib/infrastructure/repositories/store.repository.dart @@ -1,150 +1,42 @@ import 'package:drift/drift.dart'; import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/domain/models/user.model.dart'; -import 'package:immich_mobile/infrastructure/entities/store.entity.dart'; import 'package:immich_mobile/infrastructure/entities/store.entity.drift.dart'; import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/user.repository.dart'; -import 'package:isar/isar.dart'; -// Temporary interface until Isar is removed to make the service work with both Isar and Sqlite -abstract class IStoreRepository { - Future deleteAll(); - Stream>> watchAll(); - Future delete(StoreKey key); - Future upsert(StoreKey key, T value); - Future tryGet(StoreKey key); - Stream watch(StoreKey key); - Future>> getAll(); -} - -class IsarStoreRepository extends IsarDatabaseRepository implements IStoreRepository { - final Isar _db; - final validStoreKeys = StoreKey.values.map((e) => e.id).toSet(); - - IsarStoreRepository(super.db) : _db = db; - - @override - Future deleteAll() async { - return await transaction(() async { - await _db.storeValues.clear(); - return true; - }); - } - - @override - Stream>> watchAll() { - return _db.storeValues - .filter() - .anyOf(validStoreKeys, (query, id) => query.idEqualTo(id)) - .watch(fireImmediately: true) - .asyncMap((entities) => Future.wait(entities.map((entity) => _toUpdateEvent(entity)))); - } - - @override - Future delete(StoreKey key) async { - return await transaction(() async => await _db.storeValues.delete(key.id)); - } - - @override - Future upsert(StoreKey key, T value) async { - return await transaction(() async { - await _db.storeValues.put(await _fromValue(key, value)); - return true; - }); - } - - @override - Future tryGet(StoreKey key) async { - final entity = (await _db.storeValues.get(key.id)); - if (entity == null) { - return null; - } - return await _toValue(key, entity); - } - - @override - Stream watch(StoreKey key) async* { - yield* _db.storeValues - .watchObject(key.id, fireImmediately: true) - .asyncMap((e) async => e == null ? null : await _toValue(key, e)); - } - - Future> _toUpdateEvent(StoreValue entity) async { - final key = StoreKey.values.firstWhere((e) => e.id == entity.id) as StoreKey; - final value = await _toValue(key, entity); - return StoreDto(key, value); - } - - Future _toValue(StoreKey key, StoreValue entity) async => - switch (key.type) { - const (int) => entity.intValue, - const (String) => entity.strValue, - const (bool) => entity.intValue == 1, - const (DateTime) => entity.intValue == null ? null : DateTime.fromMillisecondsSinceEpoch(entity.intValue!), - const (UserDto) => - entity.strValue == null ? null : await IsarUserRepository(_db).getByUserId(entity.strValue!), - _ => null, - } - as T?; - - Future _fromValue(StoreKey key, T value) async { - final (int? intValue, String? strValue) = switch (key.type) { - const (int) => (value as int, null), - const (String) => (null, value as String), - const (bool) => ((value as bool) ? 1 : 0, null), - const (DateTime) => ((value as DateTime).millisecondsSinceEpoch, null), - const (UserDto) => (null, (await IsarUserRepository(_db).update(value as UserDto)).id), - _ => throw UnsupportedError("Unsupported primitive type: ${key.type} for key: ${key.name}"), - }; - return StoreValue(key.id, intValue: intValue, strValue: strValue); - } - - @override - Future>> getAll() async { - final entities = await _db.storeValues.filter().anyOf(validStoreKeys, (query, id) => query.idEqualTo(id)).findAll(); - return Future.wait(entities.map((e) => _toUpdateEvent(e)).toList()); - } -} - -class DriftStoreRepository extends DriftDatabaseRepository implements IStoreRepository { +class DriftStoreRepository extends DriftDatabaseRepository { final Drift _db; final validStoreKeys = StoreKey.values.map((e) => e.id).toSet(); DriftStoreRepository(super.db) : _db = db; - @override Future deleteAll() async { await _db.storeEntity.deleteAll(); return true; } - @override Future>> getAll() async { final query = _db.storeEntity.select()..where((entity) => entity.id.isIn(validStoreKeys)); return query.asyncMap((entity) => _toUpdateEvent(entity)).get(); } - @override Stream>> watchAll() { final query = _db.storeEntity.select()..where((entity) => entity.id.isIn(validStoreKeys)); return query.asyncMap((entity) => _toUpdateEvent(entity)).watch(); } - @override Future delete(StoreKey key) async { await _db.storeEntity.deleteWhere((entity) => entity.id.equals(key.id)); return; } - @override Future upsert(StoreKey key, T value) async { await _db.storeEntity.insertOnConflictUpdate(await _fromValue(key, value)); return true; } - @override Future tryGet(StoreKey key) async { final entity = await _db.managers.storeEntity.filter((entity) => entity.id.equals(key.id)).getSingleOrNull(); if (entity == null) { @@ -153,7 +45,6 @@ class DriftStoreRepository extends DriftDatabaseRepository implements IStoreRepo return await _toValue(key, entity); } - @override Stream watch(StoreKey key) async* { final query = _db.storeEntity.select()..where((entity) => entity.id.equals(key.id)); diff --git a/mobile/lib/infrastructure/repositories/user.repository.dart b/mobile/lib/infrastructure/repositories/user.repository.dart index d4eb1ceed6..ce7cb124db 100644 --- a/mobile/lib/infrastructure/repositories/user.repository.dart +++ b/mobile/lib/infrastructure/repositories/user.repository.dart @@ -1,72 +1,9 @@ import 'package:drift/drift.dart'; -import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/domain/models/user.model.dart'; import 'package:immich_mobile/domain/models/user_metadata.model.dart'; import 'package:immich_mobile/infrastructure/entities/auth_user.entity.drift.dart'; -import 'package:immich_mobile/infrastructure/entities/user.entity.dart' as entity; import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/user_metadata.repository.dart'; -import 'package:isar/isar.dart'; - -class IsarUserRepository extends IsarDatabaseRepository { - final Isar _db; - const IsarUserRepository(super.db) : _db = db; - - Future delete(List ids) async { - await transaction(() async { - await _db.users.deleteAllById(ids); - }); - } - - Future deleteAll() async { - await transaction(() async { - await _db.users.clear(); - }); - } - - Future> getAll({SortUserBy? sortBy}) async { - return (await _db.users - .where() - .optional( - sortBy != null, - (query) => switch (sortBy!) { - SortUserBy.id => query.sortById(), - }, - ) - .findAll()) - .map((u) => u.toDto()) - .toList(); - } - - Future getByUserId(String id) async { - return (await _db.users.getById(id))?.toDto(); - } - - Future> getByUserIds(List ids) async { - return (await _db.users.getAllById(ids)).map((u) => u?.toDto()).toList(); - } - - Future insert(UserDto user) async { - await transaction(() async { - await _db.users.put(entity.User.fromDto(user)); - }); - return true; - } - - Future update(UserDto user) async { - await transaction(() async { - await _db.users.put(entity.User.fromDto(user)); - }); - return user; - } - - Future updateAll(List users) async { - await transaction(() async { - await _db.users.putAll(users.map(entity.User.fromDto).toList()); - }); - return true; - } -} class DriftAuthUserRepository extends DriftDatabaseRepository { final Drift _db; @@ -117,6 +54,7 @@ extension on AuthUserEntityData { id: id, email: email, name: name, + updatedAt: profileChangedAt, profileChangedAt: profileChangedAt, hasProfileImage: hasProfileImage, avatarColor: avatarColor, diff --git a/mobile/lib/main.dart b/mobile/lib/main.dart index 7e7c709eeb..4a284b9bda 100644 --- a/mobile/lib/main.dart +++ b/mobile/lib/main.dart @@ -14,7 +14,6 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/constants/constants.dart'; import 'package:immich_mobile/constants/locales.dart'; import 'package:immich_mobile/domain/services/background_worker.service.dart'; -import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/generated/codegen_loader.g.dart'; @@ -24,7 +23,6 @@ import 'package:immich_mobile/pages/common/splash_screen.page.dart'; import 'package:immich_mobile/platform/background_worker_lock_api.g.dart'; import 'package:immich_mobile/providers/app_life_cycle.provider.dart'; import 'package:immich_mobile/providers/asset_viewer/share_intent_upload.provider.dart'; -import 'package:immich_mobile/providers/db.provider.dart'; import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; import 'package:immich_mobile/providers/infrastructure/platform.provider.dart'; import 'package:immich_mobile/providers/locale_provider.dart'; @@ -32,9 +30,7 @@ import 'package:immich_mobile/providers/routes.provider.dart'; import 'package:immich_mobile/providers/theme.provider.dart'; import 'package:immich_mobile/routing/app_navigation_observer.dart'; import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/services/background.service.dart'; import 'package:immich_mobile/services/deep_link.service.dart'; -import 'package:immich_mobile/services/local_notification.service.dart'; import 'package:immich_mobile/theme/dynamic_theme.dart'; import 'package:immich_mobile/theme/theme_data.dart'; import 'package:immich_mobile/utils/bootstrap.dart'; @@ -53,23 +49,13 @@ void main() async { ImmichWidgetsBinding(); unawaited(BackgroundWorkerLockService(BackgroundWorkerLockApi()).lock()); await EasyLocalization.ensureInitialized(); - final (isar, drift, logDb) = await Bootstrap.initDB(); - await Bootstrap.initDomain(isar, drift, logDb); + final (drift, _) = await Bootstrap.initDomain(); await initApp(); // Warm-up isolate pool for worker manager await workerManagerPatch.init(dynamicSpawning: true, isolatesCount: max(Platform.numberOfProcessors - 1, 5)); - await migrateDatabaseIfNeeded(isar, drift); + await migrateDatabaseIfNeeded(); - runApp( - ProviderScope( - overrides: [ - dbProvider.overrideWithValue(isar), - isarProvider.overrideWithValue(isar), - driftProvider.overrideWith(driftOverride(drift)), - ], - child: const MainWidget(), - ), - ); + runApp(ProviderScope(overrides: [driftProvider.overrideWith(driftOverride(drift))], child: const MainWidget())); } catch (error, stack) { runApp(BootstrapErrorWidget(error: error.toString(), stack: stack.toString())); } @@ -176,7 +162,6 @@ class ImmichAppState extends ConsumerState with WidgetsBindingObserve } } SystemChrome.setSystemUIOverlayStyle(overlayStyle); - await ref.read(localNotificationService).setup(); } Future _deepLinkBuilder(PlatformDeepLink deepLink) async { @@ -215,20 +200,14 @@ class ImmichAppState extends ConsumerState with WidgetsBindingObserve initApp().then((_) => dPrint(() => "App Init Completed")); WidgetsBinding.instance.addPostFrameCallback((_) { // needs to be delayed so that EasyLocalization is working - if (Store.isBetaTimelineEnabled) { - ref.read(backgroundServiceProvider).disableService(); - ref.read(backgroundWorkerFgServiceProvider).enable(); - if (Platform.isAndroid) { - ref - .read(backgroundWorkerFgServiceProvider) - .saveNotificationMessage( - StaticTranslations.instance.uploading_media, - StaticTranslations.instance.backup_background_service_default_notification, - ); - } - } else { - ref.read(backgroundWorkerFgServiceProvider).disable(); - ref.read(backgroundServiceProvider).resumeServiceIfEnabled(); + ref.read(backgroundWorkerFgServiceProvider).enable(); + if (Platform.isAndroid) { + ref + .read(backgroundWorkerFgServiceProvider) + .saveNotificationMessage( + StaticTranslations.instance.uploading_media, + StaticTranslations.instance.backup_background_service_default_notification, + ); } }); diff --git a/mobile/lib/models/albums/album_add_asset_response.model.dart b/mobile/lib/models/albums/album_add_asset_response.model.dart deleted file mode 100644 index 38dd989af5..0000000000 --- a/mobile/lib/models/albums/album_add_asset_response.model.dart +++ /dev/null @@ -1,38 +0,0 @@ -// ignore_for_file: public_member_api_docs, sort_constructors_first -import 'dart:convert'; - -import 'package:collection/collection.dart'; - -class AlbumAddAssetsResponse { - List alreadyInAlbum; - int successfullyAdded; - - AlbumAddAssetsResponse({required this.alreadyInAlbum, required this.successfullyAdded}); - - AlbumAddAssetsResponse copyWith({List? alreadyInAlbum, int? successfullyAdded}) { - return AlbumAddAssetsResponse( - alreadyInAlbum: alreadyInAlbum ?? this.alreadyInAlbum, - successfullyAdded: successfullyAdded ?? this.successfullyAdded, - ); - } - - Map toMap() { - return {'alreadyInAlbum': alreadyInAlbum, 'successfullyAdded': successfullyAdded}; - } - - String toJson() => json.encode(toMap()); - - @override - String toString() => 'AddAssetsResponse(alreadyInAlbum: $alreadyInAlbum, successfullyAdded: $successfullyAdded)'; - - @override - bool operator ==(covariant AlbumAddAssetsResponse other) { - if (identical(this, other)) return true; - final listEquals = const DeepCollectionEquality().equals; - - return listEquals(other.alreadyInAlbum, alreadyInAlbum) && other.successfullyAdded == successfullyAdded; - } - - @override - int get hashCode => alreadyInAlbum.hashCode ^ successfullyAdded.hashCode; -} diff --git a/mobile/lib/models/albums/album_viewer_page_state.model.dart b/mobile/lib/models/albums/album_viewer_page_state.model.dart deleted file mode 100644 index 70427899ae..0000000000 --- a/mobile/lib/models/albums/album_viewer_page_state.model.dart +++ /dev/null @@ -1,60 +0,0 @@ -import 'dart:convert'; - -class AlbumViewerPageState { - final bool isEditAlbum; - final String editTitleText; - final String editDescriptionText; - - const AlbumViewerPageState({ - required this.isEditAlbum, - required this.editTitleText, - required this.editDescriptionText, - }); - - AlbumViewerPageState copyWith({bool? isEditAlbum, String? editTitleText, String? editDescriptionText}) { - return AlbumViewerPageState( - isEditAlbum: isEditAlbum ?? this.isEditAlbum, - editTitleText: editTitleText ?? this.editTitleText, - editDescriptionText: editDescriptionText ?? this.editDescriptionText, - ); - } - - Map toMap() { - final result = {}; - - result.addAll({'isEditAlbum': isEditAlbum}); - result.addAll({'editTitleText': editTitleText}); - result.addAll({'editDescriptionText': editDescriptionText}); - - return result; - } - - factory AlbumViewerPageState.fromMap(Map map) { - return AlbumViewerPageState( - isEditAlbum: map['isEditAlbum'] ?? false, - editTitleText: map['editTitleText'] ?? '', - editDescriptionText: map['editDescriptionText'] ?? '', - ); - } - - String toJson() => json.encode(toMap()); - - factory AlbumViewerPageState.fromJson(String source) => AlbumViewerPageState.fromMap(json.decode(source)); - - @override - String toString() => - 'AlbumViewerPageState(isEditAlbum: $isEditAlbum, editTitleText: $editTitleText, editDescriptionText: $editDescriptionText)'; - - @override - bool operator ==(Object other) { - if (identical(this, other)) return true; - - return other is AlbumViewerPageState && - other.isEditAlbum == isEditAlbum && - other.editTitleText == editTitleText && - other.editDescriptionText == editDescriptionText; - } - - @override - int get hashCode => isEditAlbum.hashCode ^ editTitleText.hashCode ^ editDescriptionText.hashCode; -} diff --git a/mobile/lib/models/albums/asset_selection_page_result.model.dart b/mobile/lib/models/albums/asset_selection_page_result.model.dart deleted file mode 100644 index cc750f397f..0000000000 --- a/mobile/lib/models/albums/asset_selection_page_result.model.dart +++ /dev/null @@ -1,18 +0,0 @@ -import 'package:collection/collection.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; - -class AssetSelectionPageResult { - final Set selectedAssets; - - const AssetSelectionPageResult({required this.selectedAssets}); - @override - bool operator ==(Object other) { - if (identical(this, other)) return true; - final setEquals = const DeepCollectionEquality().equals; - - return other is AssetSelectionPageResult && setEquals(other.selectedAssets, selectedAssets); - } - - @override - int get hashCode => selectedAssets.hashCode; -} diff --git a/mobile/lib/models/asset_selection_state.dart b/mobile/lib/models/asset_selection_state.dart deleted file mode 100644 index aded3064ce..0000000000 --- a/mobile/lib/models/asset_selection_state.dart +++ /dev/null @@ -1,47 +0,0 @@ -import 'package:immich_mobile/entities/asset.entity.dart'; - -class AssetSelectionState { - final bool hasRemote; - final bool hasLocal; - final bool hasMerged; - final int selectedCount; - - const AssetSelectionState({ - this.hasRemote = false, - this.hasLocal = false, - this.hasMerged = false, - this.selectedCount = 0, - }); - - AssetSelectionState copyWith({bool? hasRemote, bool? hasLocal, bool? hasMerged, int? selectedCount}) { - return AssetSelectionState( - hasRemote: hasRemote ?? this.hasRemote, - hasLocal: hasLocal ?? this.hasLocal, - hasMerged: hasMerged ?? this.hasMerged, - selectedCount: selectedCount ?? this.selectedCount, - ); - } - - AssetSelectionState.fromSelection(Set selection) - : hasLocal = selection.any((e) => e.storage == AssetState.local), - hasMerged = selection.any((e) => e.storage == AssetState.merged), - hasRemote = selection.any((e) => e.storage == AssetState.remote), - selectedCount = selection.length; - - @override - String toString() => - 'SelectionAssetState(hasRemote: $hasRemote, hasLocal: $hasLocal, hasMerged: $hasMerged, selectedCount: $selectedCount)'; - - @override - bool operator ==(covariant AssetSelectionState other) { - if (identical(this, other)) return true; - - return other.hasRemote == hasRemote && - other.hasLocal == hasLocal && - other.hasMerged == hasMerged && - other.selectedCount == selectedCount; - } - - @override - int get hashCode => hasRemote.hashCode ^ hasLocal.hashCode ^ hasMerged.hashCode ^ selectedCount.hashCode; -} diff --git a/mobile/lib/models/backup/available_album.model.dart b/mobile/lib/models/backup/available_album.model.dart deleted file mode 100644 index 502d0b66be..0000000000 --- a/mobile/lib/models/backup/available_album.model.dart +++ /dev/null @@ -1,35 +0,0 @@ -import 'package:immich_mobile/entities/album.entity.dart'; - -class AvailableAlbum { - final Album album; - final int assetCount; - final DateTime? lastBackup; - const AvailableAlbum({required this.album, required this.assetCount, this.lastBackup}); - - AvailableAlbum copyWith({Album? album, int? assetCount, DateTime? lastBackup}) { - return AvailableAlbum( - album: album ?? this.album, - assetCount: assetCount ?? this.assetCount, - lastBackup: lastBackup ?? this.lastBackup, - ); - } - - String get name => album.name; - - String get id => album.localId!; - - bool get isAll => album.isAll; - - @override - String toString() => 'AvailableAlbum(albumEntity: $album, lastBackup: $lastBackup)'; - - @override - bool operator ==(Object other) { - if (identical(this, other)) return true; - - return other is AvailableAlbum && other.album == album; - } - - @override - int get hashCode => album.hashCode; -} diff --git a/mobile/lib/models/backup/backup_candidate.model.dart b/mobile/lib/models/backup/backup_candidate.model.dart deleted file mode 100644 index 01c257dc05..0000000000 --- a/mobile/lib/models/backup/backup_candidate.model.dart +++ /dev/null @@ -1,19 +0,0 @@ -import 'package:immich_mobile/entities/asset.entity.dart'; - -class BackupCandidate { - BackupCandidate({required this.asset, required this.albumNames}); - - Asset asset; - List albumNames; - - @override - int get hashCode => asset.hashCode; - - @override - bool operator ==(Object other) { - if (other is! BackupCandidate) { - return false; - } - return asset == other.asset; - } -} diff --git a/mobile/lib/models/backup/backup_state.model.dart b/mobile/lib/models/backup/backup_state.model.dart deleted file mode 100644 index 51a17de4fc..0000000000 --- a/mobile/lib/models/backup/backup_state.model.dart +++ /dev/null @@ -1,173 +0,0 @@ -// ignore_for_file: public_member_api_docs, sort_constructors_first - -import 'package:collection/collection.dart'; -import 'package:immich_mobile/models/backup/backup_candidate.model.dart'; - -import 'package:immich_mobile/models/backup/available_album.model.dart'; -import 'package:immich_mobile/models/backup/current_upload_asset.model.dart'; -import 'package:immich_mobile/models/server_info/server_disk_info.model.dart'; - -enum BackUpProgressEnum { idle, inProgress, manualInProgress, inBackground, done } - -class BackUpState { - // enum - final BackUpProgressEnum backupProgress; - final List allAssetsInDatabase; - final double progressInPercentage; - final String progressInFileSize; - final double progressInFileSpeed; - final List progressInFileSpeeds; - final DateTime progressInFileSpeedUpdateTime; - final int progressInFileSpeedUpdateSentBytes; - final double iCloudDownloadProgress; - final ServerDiskInfo serverInfo; - final bool autoBackup; - final bool backgroundBackup; - final bool backupRequireWifi; - final bool backupRequireCharging; - final int backupTriggerDelay; - - /// All available albums on the device - final List availableAlbums; - final Set selectedBackupAlbums; - final Set excludedBackupAlbums; - - /// Assets that are not overlapping in selected backup albums and excluded backup albums - final Set allUniqueAssets; - - /// All assets from the selected albums that have been backup - final Set selectedAlbumsBackupAssetsIds; - - // Current Backup Asset - final CurrentUploadAsset currentUploadAsset; - - const BackUpState({ - required this.backupProgress, - required this.allAssetsInDatabase, - required this.progressInPercentage, - required this.progressInFileSize, - required this.progressInFileSpeed, - required this.progressInFileSpeeds, - required this.progressInFileSpeedUpdateTime, - required this.progressInFileSpeedUpdateSentBytes, - required this.iCloudDownloadProgress, - required this.serverInfo, - required this.autoBackup, - required this.backgroundBackup, - required this.backupRequireWifi, - required this.backupRequireCharging, - required this.backupTriggerDelay, - required this.availableAlbums, - required this.selectedBackupAlbums, - required this.excludedBackupAlbums, - required this.allUniqueAssets, - required this.selectedAlbumsBackupAssetsIds, - required this.currentUploadAsset, - }); - - BackUpState copyWith({ - BackUpProgressEnum? backupProgress, - List? allAssetsInDatabase, - double? progressInPercentage, - String? progressInFileSize, - double? progressInFileSpeed, - List? progressInFileSpeeds, - DateTime? progressInFileSpeedUpdateTime, - int? progressInFileSpeedUpdateSentBytes, - double? iCloudDownloadProgress, - ServerDiskInfo? serverInfo, - bool? autoBackup, - bool? backgroundBackup, - bool? backupRequireWifi, - bool? backupRequireCharging, - int? backupTriggerDelay, - List? availableAlbums, - Set? selectedBackupAlbums, - Set? excludedBackupAlbums, - Set? allUniqueAssets, - Set? selectedAlbumsBackupAssetsIds, - CurrentUploadAsset? currentUploadAsset, - }) { - return BackUpState( - backupProgress: backupProgress ?? this.backupProgress, - allAssetsInDatabase: allAssetsInDatabase ?? this.allAssetsInDatabase, - progressInPercentage: progressInPercentage ?? this.progressInPercentage, - progressInFileSize: progressInFileSize ?? this.progressInFileSize, - progressInFileSpeed: progressInFileSpeed ?? this.progressInFileSpeed, - progressInFileSpeeds: progressInFileSpeeds ?? this.progressInFileSpeeds, - progressInFileSpeedUpdateTime: progressInFileSpeedUpdateTime ?? this.progressInFileSpeedUpdateTime, - progressInFileSpeedUpdateSentBytes: progressInFileSpeedUpdateSentBytes ?? this.progressInFileSpeedUpdateSentBytes, - iCloudDownloadProgress: iCloudDownloadProgress ?? this.iCloudDownloadProgress, - serverInfo: serverInfo ?? this.serverInfo, - autoBackup: autoBackup ?? this.autoBackup, - backgroundBackup: backgroundBackup ?? this.backgroundBackup, - backupRequireWifi: backupRequireWifi ?? this.backupRequireWifi, - backupRequireCharging: backupRequireCharging ?? this.backupRequireCharging, - backupTriggerDelay: backupTriggerDelay ?? this.backupTriggerDelay, - availableAlbums: availableAlbums ?? this.availableAlbums, - selectedBackupAlbums: selectedBackupAlbums ?? this.selectedBackupAlbums, - excludedBackupAlbums: excludedBackupAlbums ?? this.excludedBackupAlbums, - allUniqueAssets: allUniqueAssets ?? this.allUniqueAssets, - selectedAlbumsBackupAssetsIds: selectedAlbumsBackupAssetsIds ?? this.selectedAlbumsBackupAssetsIds, - currentUploadAsset: currentUploadAsset ?? this.currentUploadAsset, - ); - } - - @override - String toString() { - return 'BackUpState(backupProgress: $backupProgress, allAssetsInDatabase: $allAssetsInDatabase, progressInPercentage: $progressInPercentage, progressInFileSize: $progressInFileSize, progressInFileSpeed: $progressInFileSpeed, progressInFileSpeeds: $progressInFileSpeeds, progressInFileSpeedUpdateTime: $progressInFileSpeedUpdateTime, progressInFileSpeedUpdateSentBytes: $progressInFileSpeedUpdateSentBytes, iCloudDownloadProgress: $iCloudDownloadProgress, serverInfo: $serverInfo, autoBackup: $autoBackup, backgroundBackup: $backgroundBackup, backupRequireWifi: $backupRequireWifi, backupRequireCharging: $backupRequireCharging, backupTriggerDelay: $backupTriggerDelay, availableAlbums: $availableAlbums, selectedBackupAlbums: $selectedBackupAlbums, excludedBackupAlbums: $excludedBackupAlbums, allUniqueAssets: $allUniqueAssets, selectedAlbumsBackupAssetsIds: $selectedAlbumsBackupAssetsIds, currentUploadAsset: $currentUploadAsset)'; - } - - @override - bool operator ==(covariant BackUpState other) { - if (identical(this, other)) return true; - final collectionEquals = const DeepCollectionEquality().equals; - - return other.backupProgress == backupProgress && - collectionEquals(other.allAssetsInDatabase, allAssetsInDatabase) && - other.progressInPercentage == progressInPercentage && - other.progressInFileSize == progressInFileSize && - other.progressInFileSpeed == progressInFileSpeed && - collectionEquals(other.progressInFileSpeeds, progressInFileSpeeds) && - other.progressInFileSpeedUpdateTime == progressInFileSpeedUpdateTime && - other.progressInFileSpeedUpdateSentBytes == progressInFileSpeedUpdateSentBytes && - other.iCloudDownloadProgress == iCloudDownloadProgress && - other.serverInfo == serverInfo && - other.autoBackup == autoBackup && - other.backgroundBackup == backgroundBackup && - other.backupRequireWifi == backupRequireWifi && - other.backupRequireCharging == backupRequireCharging && - other.backupTriggerDelay == backupTriggerDelay && - collectionEquals(other.availableAlbums, availableAlbums) && - collectionEquals(other.selectedBackupAlbums, selectedBackupAlbums) && - collectionEquals(other.excludedBackupAlbums, excludedBackupAlbums) && - collectionEquals(other.allUniqueAssets, allUniqueAssets) && - collectionEquals(other.selectedAlbumsBackupAssetsIds, selectedAlbumsBackupAssetsIds) && - other.currentUploadAsset == currentUploadAsset; - } - - @override - int get hashCode { - return backupProgress.hashCode ^ - allAssetsInDatabase.hashCode ^ - progressInPercentage.hashCode ^ - progressInFileSize.hashCode ^ - progressInFileSpeed.hashCode ^ - progressInFileSpeeds.hashCode ^ - progressInFileSpeedUpdateTime.hashCode ^ - progressInFileSpeedUpdateSentBytes.hashCode ^ - iCloudDownloadProgress.hashCode ^ - serverInfo.hashCode ^ - autoBackup.hashCode ^ - backgroundBackup.hashCode ^ - backupRequireWifi.hashCode ^ - backupRequireCharging.hashCode ^ - backupTriggerDelay.hashCode ^ - availableAlbums.hashCode ^ - selectedBackupAlbums.hashCode ^ - excludedBackupAlbums.hashCode ^ - allUniqueAssets.hashCode ^ - selectedAlbumsBackupAssetsIds.hashCode ^ - currentUploadAsset.hashCode; - } -} diff --git a/mobile/lib/models/backup/current_upload_asset.model.dart b/mobile/lib/models/backup/current_upload_asset.model.dart deleted file mode 100644 index 2214897357..0000000000 --- a/mobile/lib/models/backup/current_upload_asset.model.dart +++ /dev/null @@ -1,95 +0,0 @@ -// ignore_for_file: public_member_api_docs, sort_constructors_first -import 'dart:convert'; - -class CurrentUploadAsset { - final String id; - final DateTime fileCreatedAt; - final String fileName; - final String fileType; - final int? fileSize; - final bool? iCloudAsset; - - const CurrentUploadAsset({ - required this.id, - required this.fileCreatedAt, - required this.fileName, - required this.fileType, - this.fileSize, - this.iCloudAsset, - }); - - @pragma('vm:prefer-inline') - bool get isIcloudAsset => iCloudAsset != null && iCloudAsset!; - - CurrentUploadAsset copyWith({ - String? id, - DateTime? fileCreatedAt, - String? fileName, - String? fileType, - int? fileSize, - bool? iCloudAsset, - }) { - return CurrentUploadAsset( - id: id ?? this.id, - fileCreatedAt: fileCreatedAt ?? this.fileCreatedAt, - fileName: fileName ?? this.fileName, - fileType: fileType ?? this.fileType, - fileSize: fileSize ?? this.fileSize, - iCloudAsset: iCloudAsset ?? this.iCloudAsset, - ); - } - - Map toMap() { - return { - 'id': id, - 'fileCreatedAt': fileCreatedAt.millisecondsSinceEpoch, - 'fileName': fileName, - 'fileType': fileType, - 'fileSize': fileSize, - 'iCloudAsset': iCloudAsset, - }; - } - - factory CurrentUploadAsset.fromMap(Map map) { - return CurrentUploadAsset( - id: map['id'] as String, - fileCreatedAt: DateTime.fromMillisecondsSinceEpoch(map['fileCreatedAt'] as int), - fileName: map['fileName'] as String, - fileType: map['fileType'] as String, - fileSize: map['fileSize'] as int, - iCloudAsset: map['iCloudAsset'] != null ? map['iCloudAsset'] as bool : null, - ); - } - - String toJson() => json.encode(toMap()); - - factory CurrentUploadAsset.fromJson(String source) => - CurrentUploadAsset.fromMap(json.decode(source) as Map); - - @override - String toString() { - return 'CurrentUploadAsset(id: $id, fileCreatedAt: $fileCreatedAt, fileName: $fileName, fileType: $fileType, fileSize: $fileSize, iCloudAsset: $iCloudAsset)'; - } - - @override - bool operator ==(covariant CurrentUploadAsset other) { - if (identical(this, other)) return true; - - return other.id == id && - other.fileCreatedAt == fileCreatedAt && - other.fileName == fileName && - other.fileType == fileType && - other.fileSize == fileSize && - other.iCloudAsset == iCloudAsset; - } - - @override - int get hashCode { - return id.hashCode ^ - fileCreatedAt.hashCode ^ - fileName.hashCode ^ - fileType.hashCode ^ - fileSize.hashCode ^ - iCloudAsset.hashCode; - } -} diff --git a/mobile/lib/models/backup/error_upload_asset.model.dart b/mobile/lib/models/backup/error_upload_asset.model.dart deleted file mode 100644 index 38f241e748..0000000000 --- a/mobile/lib/models/backup/error_upload_asset.model.dart +++ /dev/null @@ -1,65 +0,0 @@ -import 'package:immich_mobile/entities/asset.entity.dart'; - -class ErrorUploadAsset { - final String id; - final DateTime fileCreatedAt; - final String fileName; - final String fileType; - final Asset asset; - final String errorMessage; - - const ErrorUploadAsset({ - required this.id, - required this.fileCreatedAt, - required this.fileName, - required this.fileType, - required this.asset, - required this.errorMessage, - }); - - ErrorUploadAsset copyWith({ - String? id, - DateTime? fileCreatedAt, - String? fileName, - String? fileType, - Asset? asset, - String? errorMessage, - }) { - return ErrorUploadAsset( - id: id ?? this.id, - fileCreatedAt: fileCreatedAt ?? this.fileCreatedAt, - fileName: fileName ?? this.fileName, - fileType: fileType ?? this.fileType, - asset: asset ?? this.asset, - errorMessage: errorMessage ?? this.errorMessage, - ); - } - - @override - String toString() { - return 'ErrorUploadAsset(id: $id, fileCreatedAt: $fileCreatedAt, fileName: $fileName, fileType: $fileType, asset: $asset, errorMessage: $errorMessage)'; - } - - @override - bool operator ==(Object other) { - if (identical(this, other)) return true; - - return other is ErrorUploadAsset && - other.id == id && - other.fileCreatedAt == fileCreatedAt && - other.fileName == fileName && - other.fileType == fileType && - other.asset == asset && - other.errorMessage == errorMessage; - } - - @override - int get hashCode { - return id.hashCode ^ - fileCreatedAt.hashCode ^ - fileName.hashCode ^ - fileType.hashCode ^ - asset.hashCode ^ - errorMessage.hashCode; - } -} diff --git a/mobile/lib/models/backup/manual_upload_state.model.dart b/mobile/lib/models/backup/manual_upload_state.model.dart deleted file mode 100644 index 120327c611..0000000000 --- a/mobile/lib/models/backup/manual_upload_state.model.dart +++ /dev/null @@ -1,102 +0,0 @@ -import 'package:collection/collection.dart'; - -import 'package:immich_mobile/models/backup/current_upload_asset.model.dart'; - -class ManualUploadState { - // Current Backup Asset - final CurrentUploadAsset currentUploadAsset; - final int currentAssetIndex; - - final bool showDetailedNotification; - - /// Manual Upload Stats - final int totalAssetsToUpload; - final int successfulUploads; - final double progressInPercentage; - final String progressInFileSize; - final double progressInFileSpeed; - final List progressInFileSpeeds; - final DateTime progressInFileSpeedUpdateTime; - final int progressInFileSpeedUpdateSentBytes; - - const ManualUploadState({ - required this.progressInPercentage, - required this.progressInFileSize, - required this.progressInFileSpeed, - required this.progressInFileSpeeds, - required this.progressInFileSpeedUpdateTime, - required this.progressInFileSpeedUpdateSentBytes, - required this.currentUploadAsset, - required this.totalAssetsToUpload, - required this.currentAssetIndex, - required this.successfulUploads, - required this.showDetailedNotification, - }); - - ManualUploadState copyWith({ - double? progressInPercentage, - String? progressInFileSize, - double? progressInFileSpeed, - List? progressInFileSpeeds, - DateTime? progressInFileSpeedUpdateTime, - int? progressInFileSpeedUpdateSentBytes, - CurrentUploadAsset? currentUploadAsset, - int? totalAssetsToUpload, - int? successfulUploads, - int? currentAssetIndex, - bool? showDetailedNotification, - }) { - return ManualUploadState( - progressInPercentage: progressInPercentage ?? this.progressInPercentage, - progressInFileSize: progressInFileSize ?? this.progressInFileSize, - progressInFileSpeed: progressInFileSpeed ?? this.progressInFileSpeed, - progressInFileSpeeds: progressInFileSpeeds ?? this.progressInFileSpeeds, - progressInFileSpeedUpdateTime: progressInFileSpeedUpdateTime ?? this.progressInFileSpeedUpdateTime, - progressInFileSpeedUpdateSentBytes: progressInFileSpeedUpdateSentBytes ?? this.progressInFileSpeedUpdateSentBytes, - currentUploadAsset: currentUploadAsset ?? this.currentUploadAsset, - totalAssetsToUpload: totalAssetsToUpload ?? this.totalAssetsToUpload, - currentAssetIndex: currentAssetIndex ?? this.currentAssetIndex, - successfulUploads: successfulUploads ?? this.successfulUploads, - showDetailedNotification: showDetailedNotification ?? this.showDetailedNotification, - ); - } - - @override - String toString() { - return 'ManualUploadState(progressInPercentage: $progressInPercentage, progressInFileSize: $progressInFileSize, progressInFileSpeed: $progressInFileSpeed, progressInFileSpeeds: $progressInFileSpeeds, progressInFileSpeedUpdateTime: $progressInFileSpeedUpdateTime, progressInFileSpeedUpdateSentBytes: $progressInFileSpeedUpdateSentBytes, currentUploadAsset: $currentUploadAsset, totalAssetsToUpload: $totalAssetsToUpload, successfulUploads: $successfulUploads, currentAssetIndex: $currentAssetIndex, showDetailedNotification: $showDetailedNotification)'; - } - - @override - bool operator ==(Object other) { - if (identical(this, other)) return true; - final collectionEquals = const DeepCollectionEquality().equals; - - return other is ManualUploadState && - other.progressInPercentage == progressInPercentage && - other.progressInFileSize == progressInFileSize && - other.progressInFileSpeed == progressInFileSpeed && - collectionEquals(other.progressInFileSpeeds, progressInFileSpeeds) && - other.progressInFileSpeedUpdateTime == progressInFileSpeedUpdateTime && - other.progressInFileSpeedUpdateSentBytes == progressInFileSpeedUpdateSentBytes && - other.currentUploadAsset == currentUploadAsset && - other.totalAssetsToUpload == totalAssetsToUpload && - other.currentAssetIndex == currentAssetIndex && - other.successfulUploads == successfulUploads && - other.showDetailedNotification == showDetailedNotification; - } - - @override - int get hashCode { - return progressInPercentage.hashCode ^ - progressInFileSize.hashCode ^ - progressInFileSpeed.hashCode ^ - progressInFileSpeeds.hashCode ^ - progressInFileSpeedUpdateTime.hashCode ^ - progressInFileSpeedUpdateSentBytes.hashCode ^ - currentUploadAsset.hashCode ^ - totalAssetsToUpload.hashCode ^ - currentAssetIndex.hashCode ^ - successfulUploads.hashCode ^ - showDetailedNotification.hashCode; - } -} diff --git a/mobile/lib/models/backup/success_upload_asset.model.dart b/mobile/lib/models/backup/success_upload_asset.model.dart deleted file mode 100644 index da1e104ba3..0000000000 --- a/mobile/lib/models/backup/success_upload_asset.model.dart +++ /dev/null @@ -1,31 +0,0 @@ -import 'package:immich_mobile/models/backup/backup_candidate.model.dart'; - -class SuccessUploadAsset { - final BackupCandidate candidate; - final String remoteAssetId; - final bool isDuplicate; - - const SuccessUploadAsset({required this.candidate, required this.remoteAssetId, required this.isDuplicate}); - - SuccessUploadAsset copyWith({BackupCandidate? candidate, String? remoteAssetId, bool? isDuplicate}) { - return SuccessUploadAsset( - candidate: candidate ?? this.candidate, - remoteAssetId: remoteAssetId ?? this.remoteAssetId, - isDuplicate: isDuplicate ?? this.isDuplicate, - ); - } - - @override - String toString() => - 'SuccessUploadAsset(asset: $candidate, remoteAssetId: $remoteAssetId, isDuplicate: $isDuplicate)'; - - @override - bool operator ==(covariant SuccessUploadAsset other) { - if (identical(this, other)) return true; - - return other.candidate == candidate && other.remoteAssetId == remoteAssetId && other.isDuplicate == isDuplicate; - } - - @override - int get hashCode => candidate.hashCode ^ remoteAssetId.hashCode ^ isDuplicate.hashCode; -} diff --git a/mobile/lib/models/memories/memory.model.dart b/mobile/lib/models/memories/memory.model.dart deleted file mode 100644 index 8a9db5d51b..0000000000 --- a/mobile/lib/models/memories/memory.model.dart +++ /dev/null @@ -1,29 +0,0 @@ -// ignore_for_file: public_member_api_docs, sort_constructors_first - -import 'package:collection/collection.dart'; - -import 'package:immich_mobile/entities/asset.entity.dart'; - -class Memory { - final String title; - final List assets; - const Memory({required this.title, required this.assets}); - - Memory copyWith({String? title, List? assets}) { - return Memory(title: title ?? this.title, assets: assets ?? this.assets); - } - - @override - String toString() => 'Memory(title: $title, assets: $assets)'; - - @override - bool operator ==(Object other) { - if (identical(this, other)) return true; - final listEquals = const DeepCollectionEquality().equals; - - return other is Memory && other.title == title && listEquals(other.assets, assets); - } - - @override - int get hashCode => title.hashCode ^ assets.hashCode; -} diff --git a/mobile/lib/models/search/search_filter.model.dart b/mobile/lib/models/search/search_filter.model.dart index 1b730e0c68..16f3be4655 100644 --- a/mobile/lib/models/search/search_filter.model.dart +++ b/mobile/lib/models/search/search_filter.model.dart @@ -1,8 +1,8 @@ // ignore_for_file: public_member_api_docs, sort_constructors_first import 'dart:convert'; +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/domain/models/person.model.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; class SearchLocationFilter { String? country; diff --git a/mobile/lib/models/search/search_result.model.dart b/mobile/lib/models/search/search_result.model.dart deleted file mode 100644 index 02553869bf..0000000000 --- a/mobile/lib/models/search/search_result.model.dart +++ /dev/null @@ -1,28 +0,0 @@ -import 'package:collection/collection.dart'; - -import 'package:immich_mobile/entities/asset.entity.dart'; - -class SearchResult { - final List assets; - final int? nextPage; - - const SearchResult({required this.assets, this.nextPage}); - - SearchResult copyWith({List? assets, int? nextPage}) { - return SearchResult(assets: assets ?? this.assets, nextPage: nextPage ?? this.nextPage); - } - - @override - String toString() => 'SearchResult(assets: $assets, nextPage: $nextPage)'; - - @override - bool operator ==(covariant SearchResult other) { - if (identical(this, other)) return true; - final listEquals = const DeepCollectionEquality().equals; - - return listEquals(other.assets, assets) && other.nextPage == nextPage; - } - - @override - int get hashCode => assets.hashCode ^ nextPage.hashCode; -} diff --git a/mobile/lib/models/search/search_result_page_state.model.dart b/mobile/lib/models/search/search_result_page_state.model.dart deleted file mode 100644 index 7c8a27b50c..0000000000 --- a/mobile/lib/models/search/search_result_page_state.model.dart +++ /dev/null @@ -1,57 +0,0 @@ -import 'package:collection/collection.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; - -class SearchResultPageState { - final bool isLoading; - final bool isSuccess; - final bool isError; - final bool isSmart; - final List searchResult; - - const SearchResultPageState({ - required this.isLoading, - required this.isSuccess, - required this.isError, - required this.isSmart, - required this.searchResult, - }); - - SearchResultPageState copyWith({ - bool? isLoading, - bool? isSuccess, - bool? isError, - bool? isSmart, - List? searchResult, - }) { - return SearchResultPageState( - isLoading: isLoading ?? this.isLoading, - isSuccess: isSuccess ?? this.isSuccess, - isError: isError ?? this.isError, - isSmart: isSmart ?? this.isSmart, - searchResult: searchResult ?? this.searchResult, - ); - } - - @override - String toString() { - return 'SearchresultPageState(isLoading: $isLoading, isSuccess: $isSuccess, isError: $isError, isSmart: $isSmart, searchResult: $searchResult)'; - } - - @override - bool operator ==(Object other) { - if (identical(this, other)) return true; - final listEquals = const DeepCollectionEquality().equals; - - return other is SearchResultPageState && - other.isLoading == isLoading && - other.isSuccess == isSuccess && - other.isError == isError && - other.isSmart == isSmart && - listEquals(other.searchResult, searchResult); - } - - @override - int get hashCode { - return isLoading.hashCode ^ isSuccess.hashCode ^ isError.hashCode ^ isSmart.hashCode ^ searchResult.hashCode; - } -} diff --git a/mobile/lib/pages/album/album_additional_shared_user_selection.page.dart b/mobile/lib/pages/album/album_additional_shared_user_selection.page.dart deleted file mode 100644 index f40ac9ccae..0000000000 --- a/mobile/lib/pages/album/album_additional_shared_user_selection.page.dart +++ /dev/null @@ -1,115 +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/domain/models/user.model.dart'; -import 'package:immich_mobile/entities/album.entity.dart'; -import 'package:immich_mobile/extensions/asyncvalue_extensions.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/providers/album/suggested_shared_users.provider.dart'; -import 'package:immich_mobile/widgets/common/user_circle_avatar.dart'; - -@RoutePage() -class AlbumAdditionalSharedUserSelectionPage extends HookConsumerWidget { - final Album album; - - const AlbumAdditionalSharedUserSelectionPage({super.key, required this.album}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final AsyncValue> suggestedShareUsers = ref.watch(otherUsersProvider); - final sharedUsersList = useState>({}); - - addNewUsersHandler() { - context.maybePop(sharedUsersList.value.map((e) => e.id).toList()); - } - - buildTileIcon(UserDto user) { - if (sharedUsersList.value.contains(user)) { - return CircleAvatar(backgroundColor: context.primaryColor, child: const Icon(Icons.check_rounded, size: 25)); - } else { - return UserCircleAvatar(user: user); - } - } - - buildUserList(List users) { - List usersChip = []; - - for (var user in sharedUsersList.value) { - usersChip.add( - Padding( - padding: const EdgeInsets.symmetric(horizontal: 8.0), - child: Chip( - backgroundColor: context.primaryColor.withValues(alpha: 0.15), - label: Text(user.name, style: const TextStyle(fontSize: 12, fontWeight: FontWeight.bold)), - ), - ), - ); - } - return ListView( - children: [ - Wrap(children: [...usersChip]), - Padding( - padding: const EdgeInsets.all(16.0), - child: Text( - 'suggestions'.tr(), - style: const TextStyle(fontSize: 14, color: Colors.grey, fontWeight: FontWeight.bold), - ), - ), - ListView.builder( - primary: false, - shrinkWrap: true, - itemBuilder: ((context, index) { - return ListTile( - leading: buildTileIcon(users[index]), - dense: true, - title: Text(users[index].name, style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold)), - subtitle: Text(users[index].email, style: const TextStyle(fontSize: 12)), - onTap: () { - if (sharedUsersList.value.contains(users[index])) { - sharedUsersList.value = sharedUsersList.value - .where((selectedUser) => selectedUser.id != users[index].id) - .toSet(); - } else { - sharedUsersList.value = {...sharedUsersList.value, users[index]}; - } - }, - ); - }), - itemCount: users.length, - ), - ], - ); - } - - return Scaffold( - appBar: AppBar( - title: const Text('invite_to_album').tr(), - elevation: 0, - centerTitle: false, - leading: IconButton( - icon: const Icon(Icons.close_rounded), - onPressed: () { - context.maybePop(null); - }, - ), - actions: [ - TextButton( - onPressed: sharedUsersList.value.isEmpty ? null : addNewUsersHandler, - child: const Text("add", style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold)).tr(), - ), - ], - ), - body: suggestedShareUsers.widgetWhen( - onData: (users) { - for (var sharedUsers in album.sharedUsers) { - users.removeWhere((u) => u.id == sharedUsers.id || u.id == album.ownerId); - } - - return buildUserList(users); - }, - ), - ); - } -} diff --git a/mobile/lib/pages/album/album_asset_selection.page.dart b/mobile/lib/pages/album/album_asset_selection.page.dart deleted file mode 100644 index ccc4c44d43..0000000000 --- a/mobile/lib/pages/album/album_asset_selection.page.dart +++ /dev/null @@ -1,74 +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/entities/asset.entity.dart'; -import 'package:immich_mobile/extensions/asyncvalue_extensions.dart'; -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/providers/timeline.provider.dart'; -import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart'; -import 'package:immich_mobile/widgets/asset_grid/immich_asset_grid.dart'; - -@RoutePage() -class AlbumAssetSelectionPage extends HookConsumerWidget { - const AlbumAssetSelectionPage({super.key, required this.existingAssets, this.canDeselect = false}); - - final Set existingAssets; - final bool canDeselect; - - @override - Widget build(BuildContext context, WidgetRef ref) { - final assetSelectionRenderList = ref.watch(assetSelectionTimelineProvider); - final selected = useState>(existingAssets); - final selectionEnabledHook = useState(true); - - Widget buildBody(RenderList renderList) { - return ImmichAssetGrid( - renderList: renderList, - listener: (active, assets) { - selectionEnabledHook.value = active; - selected.value = assets; - }, - selectionActive: true, - preselectedAssets: existingAssets, - canDeselect: canDeselect, - showMultiSelectIndicator: false, - ); - } - - return Scaffold( - appBar: AppBar( - elevation: 0, - leading: IconButton( - icon: const Icon(Icons.close_rounded), - onPressed: () { - AutoRouter.of(context).popForced(null); - }, - ), - title: selected.value.isEmpty - ? const Text('add_photos', style: TextStyle(fontSize: 18)).tr() - : const Text( - 'share_assets_selected', - style: TextStyle(fontSize: 18), - ).tr(namedArgs: {'count': selected.value.length.toString()}), - centerTitle: false, - actions: [ - if (selected.value.isNotEmpty || canDeselect) - TextButton( - onPressed: () { - var payload = AssetSelectionPageResult(selectedAssets: selected.value); - AutoRouter.of(context).popForced(payload); - }, - child: Text( - canDeselect ? "done" : "add", - style: TextStyle(fontWeight: FontWeight.bold, color: context.primaryColor), - ).tr(), - ), - ], - ), - body: assetSelectionRenderList.widgetWhen(onData: (data) => buildBody(data)), - ); - } -} diff --git a/mobile/lib/pages/album/album_control_button.dart b/mobile/lib/pages/album/album_control_button.dart deleted file mode 100644 index 578eb839a0..0000000000 --- a/mobile/lib/pages/album/album_control_button.dart +++ /dev/null @@ -1,37 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/widgets/album/album_action_filled_button.dart'; - -class AlbumControlButton extends ConsumerWidget { - final void Function()? onAddPhotosPressed; - final void Function()? onAddUsersPressed; - - const AlbumControlButton({super.key, this.onAddPhotosPressed, this.onAddUsersPressed}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - return SizedBox( - height: 36, - child: ListView( - scrollDirection: Axis.horizontal, - children: [ - if (onAddPhotosPressed != null) - AlbumActionFilledButton( - key: const ValueKey('add_photos_button'), - iconData: Icons.add_photo_alternate_outlined, - onPressed: onAddPhotosPressed, - labelText: "add_photos".tr(), - ), - if (onAddUsersPressed != null) - AlbumActionFilledButton( - key: const ValueKey('add_users_button'), - iconData: Icons.person_add_alt_rounded, - onPressed: onAddUsersPressed, - labelText: "album_viewer_page_share_add_users".tr(), - ), - ], - ), - ); - } -} diff --git a/mobile/lib/pages/album/album_date_range.dart b/mobile/lib/pages/album/album_date_range.dart deleted file mode 100644 index dbfd9214f1..0000000000 --- a/mobile/lib/pages/album/album_date_range.dart +++ /dev/null @@ -1,53 +0,0 @@ -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/album/current_album.provider.dart'; - -class AlbumDateRange extends ConsumerWidget { - const AlbumDateRange({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final data = ref.watch( - currentAlbumProvider.select((album) { - if (album == null || album.assets.isEmpty) { - return null; - } - - final startDate = album.startDate; - final endDate = album.endDate; - if (startDate == null || endDate == null) { - return null; - } - return (startDate, endDate, album.shared); - }), - ); - - if (data == null) { - return const SizedBox(); - } - final (startDate, endDate, shared) = data; - - return Padding( - padding: const EdgeInsets.only(left: 16.0), - child: Text( - _getDateRangeText(startDate, endDate), - style: context.textTheme.labelLarge?.copyWith(color: context.colorScheme.onSurfaceVariant), - ), - ); - } - - @pragma('vm:prefer-inline') - String _getDateRangeText(DateTime startDate, DateTime endDate) { - if (startDate.day == endDate.day && startDate.month == endDate.month && startDate.year == endDate.year) { - return DateFormat.yMMMd().format(startDate); - } - - final String startDateText = (startDate.year == endDate.year ? DateFormat.MMMd() : DateFormat.yMMMd()).format( - startDate, - ); - final String endDateText = DateFormat.yMMMd().format(endDate); - return "$startDateText - $endDateText"; - } -} diff --git a/mobile/lib/pages/album/album_description.dart b/mobile/lib/pages/album/album_description.dart deleted file mode 100644 index 383367e8b7..0000000000 --- a/mobile/lib/pages/album/album_description.dart +++ /dev/null @@ -1,42 +0,0 @@ -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/album/current_album.provider.dart'; -import 'package:immich_mobile/widgets/album/album_viewer_editable_description.dart'; -import 'package:immich_mobile/providers/auth.provider.dart'; - -class AlbumDescription extends ConsumerWidget { - const AlbumDescription({super.key, required this.descriptionFocusNode}); - - final FocusNode descriptionFocusNode; - - @override - Widget build(BuildContext context, WidgetRef ref) { - final userId = ref.watch(authProvider).userId; - final (isOwner, isRemote, albumDescription) = ref.watch( - currentAlbumProvider.select((album) { - if (album == null) { - return const (false, false, ''); - } - - return (album.ownerId == userId, album.isRemote, album.description); - }), - ); - - if (isOwner && isRemote) { - return Padding( - padding: const EdgeInsets.only(left: 8, right: 8), - child: AlbumViewerEditableDescription( - albumDescription: albumDescription ?? 'add_a_description'.tr(), - descriptionFocusNode: descriptionFocusNode, - ), - ); - } - - return Padding( - padding: const EdgeInsets.only(left: 16, right: 8), - child: Text(albumDescription ?? 'add_a_description'.tr(), style: context.textTheme.bodyLarge), - ); - } -} diff --git a/mobile/lib/pages/album/album_options.page.dart b/mobile/lib/pages/album/album_options.page.dart deleted file mode 100644 index ca65a92a79..0000000000 --- a/mobile/lib/pages/album/album_options.page.dart +++ /dev/null @@ -1,192 +0,0 @@ -import 'dart:async'; - -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:fluttertoast/fluttertoast.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/domain/models/user.model.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/extensions/theme_extensions.dart'; -import 'package:immich_mobile/infrastructure/entities/user.entity.dart' as entity; -import 'package:immich_mobile/providers/album/album.provider.dart'; -import 'package:immich_mobile/providers/album/current_album.provider.dart'; -import 'package:immich_mobile/providers/auth.provider.dart'; -import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/utils/immich_loading_overlay.dart'; -import 'package:immich_mobile/widgets/common/immich_toast.dart'; -import 'package:immich_mobile/widgets/common/user_circle_avatar.dart'; - -@RoutePage() -class AlbumOptionsPage extends HookConsumerWidget { - const AlbumOptionsPage({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final album = ref.watch(currentAlbumProvider); - if (album == null) { - return const SizedBox(); - } - - final sharedUsers = useState(album.sharedUsers.map((u) => u.toDto()).toList()); - final owner = album.owner.value; - final userId = ref.watch(authProvider).userId; - final activityEnabled = useState(album.activityEnabled); - final isProcessing = useProcessingOverlay(); - final isOwner = owner?.id == userId; - - void showErrorMessage() { - context.pop(); - ImmichToast.show( - context: context, - msg: "shared_album_section_people_action_error".tr(), - toastType: ToastType.error, - gravity: ToastGravity.BOTTOM, - ); - } - - void leaveAlbum() async { - isProcessing.value = true; - - try { - final isSuccess = await ref.read(albumProvider.notifier).leaveAlbum(album); - - if (isSuccess) { - unawaited(context.navigateTo(const TabControllerRoute(children: [AlbumsRoute()]))); - } else { - showErrorMessage(); - } - } catch (_) { - showErrorMessage(); - } - - isProcessing.value = false; - } - - void removeUserFromAlbum(UserDto user) async { - isProcessing.value = true; - - try { - await ref.read(albumProvider.notifier).removeUser(album, user); - album.sharedUsers.remove(entity.User.fromDto(user)); - sharedUsers.value = album.sharedUsers.map((u) => u.toDto()).toList(); - } catch (error) { - showErrorMessage(); - } - - context.pop(); - isProcessing.value = false; - } - - void handleUserClick(UserDto user) { - var actions = []; - - if (user.id == userId) { - actions = [ - ListTile( - leading: const Icon(Icons.exit_to_app_rounded), - title: const Text("shared_album_section_people_action_leave").tr(), - onTap: leaveAlbum, - ), - ]; - } - - if (isOwner) { - actions = [ - ListTile( - leading: const Icon(Icons.person_remove_rounded), - title: const Text("shared_album_section_people_action_remove_user").tr(), - onTap: () => removeUserFromAlbum(user), - ), - ]; - } - - showModalBottomSheet( - backgroundColor: context.colorScheme.surfaceContainer, - isScrollControlled: false, - context: context, - builder: (context) { - return SafeArea( - child: Padding( - padding: const EdgeInsets.only(top: 24.0), - child: Column(mainAxisSize: MainAxisSize.min, children: [...actions]), - ), - ); - }, - ); - } - - buildOwnerInfo() { - return ListTile( - leading: owner != null ? UserCircleAvatar(user: owner.toDto()) : const SizedBox(), - title: Text(album.owner.value?.name ?? "", style: const TextStyle(fontWeight: FontWeight.w500)), - subtitle: Text(album.owner.value?.email ?? "", style: TextStyle(color: context.colorScheme.onSurfaceSecondary)), - trailing: Text("owner", style: context.textTheme.labelLarge).tr(), - ); - } - - buildSharedUsersList() { - return ListView.builder( - primary: false, - shrinkWrap: true, - itemCount: sharedUsers.value.length, - itemBuilder: (context, index) { - final user = sharedUsers.value[index]; - return ListTile( - leading: UserCircleAvatar(user: user), - title: Text(user.name, style: const TextStyle(fontWeight: FontWeight.w500)), - subtitle: Text(user.email, style: TextStyle(color: context.colorScheme.onSurfaceSecondary)), - trailing: userId == user.id || isOwner ? const Icon(Icons.more_horiz_rounded) : const SizedBox(), - onTap: userId == user.id || isOwner ? () => handleUserClick(user) : null, - ); - }, - ); - } - - buildSectionTitle(String text) { - return Padding( - padding: const EdgeInsets.all(16.0), - child: Text(text, style: context.textTheme.bodySmall), - ); - } - - return Scaffold( - appBar: AppBar( - leading: IconButton( - icon: const Icon(Icons.arrow_back_ios_new_rounded), - onPressed: () => context.maybePop(null), - ), - centerTitle: true, - title: Text("options".tr()), - ), - body: ListView( - children: [ - if (isOwner && album.shared) - SwitchListTile.adaptive( - value: activityEnabled.value, - onChanged: (bool value) async { - activityEnabled.value = value; - if (await ref.read(albumProvider.notifier).setActivitystatus(album, value)) { - album.activityEnabled = value; - } - }, - activeThumbColor: activityEnabled.value ? context.primaryColor : context.themeData.disabledColor, - dense: true, - title: Text( - "comments_and_likes", - style: context.textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w500), - ).tr(), - subtitle: Text( - "let_others_respond", - style: context.textTheme.labelLarge?.copyWith(color: context.colorScheme.onSurfaceSecondary), - ).tr(), - ), - buildSectionTitle("shared_album_section_people_title".tr()), - buildOwnerInfo(), - buildSharedUsersList(), - ], - ), - ); - } -} diff --git a/mobile/lib/pages/album/album_shared_user_icons.dart b/mobile/lib/pages/album/album_shared_user_icons.dart deleted file mode 100644 index 7cf6f387ae..0000000000 --- a/mobile/lib/pages/album/album_shared_user_icons.dart +++ /dev/null @@ -1,52 +0,0 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/domain/models/user.model.dart'; -import 'package:immich_mobile/providers/album/current_album.provider.dart'; -import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/widgets/common/user_circle_avatar.dart'; - -class AlbumSharedUserIcons extends HookConsumerWidget { - const AlbumSharedUserIcons({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final sharedUsers = useRef>(const []); - sharedUsers.value = ref.watch( - currentAlbumProvider.select((album) { - if (album == null) { - return const []; - } - - if (album.sharedUsers.length == sharedUsers.value.length) { - return sharedUsers.value; - } - - return album.sharedUsers.map((u) => u.toDto()).toList(growable: false); - }), - ); - - if (sharedUsers.value.isEmpty) { - return const SizedBox(); - } - - return GestureDetector( - onTap: () => context.pushRoute(const AlbumOptionsRoute()), - child: SizedBox( - height: 50, - child: ListView.builder( - padding: const EdgeInsets.only(left: 16, bottom: 8), - scrollDirection: Axis.horizontal, - itemBuilder: ((context, index) { - return Padding( - padding: const EdgeInsets.only(right: 8.0), - child: UserCircleAvatar(user: sharedUsers.value[index], size: 36), - ); - }), - itemCount: sharedUsers.value.length, - ), - ), - ); - } -} diff --git a/mobile/lib/pages/album/album_shared_user_selection.page.dart b/mobile/lib/pages/album/album_shared_user_selection.page.dart deleted file mode 100644 index ec084b1859..0000000000 --- a/mobile/lib/pages/album/album_shared_user_selection.page.dart +++ /dev/null @@ -1,140 +0,0 @@ -import 'dart:async'; - -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/domain/models/user.model.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/extensions/asyncvalue_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/suggested_shared_users.provider.dart'; -import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/widgets/common/user_circle_avatar.dart'; - -@RoutePage() -class AlbumSharedUserSelectionPage extends HookConsumerWidget { - const AlbumSharedUserSelectionPage({super.key, required this.assets}); - - final Set assets; - - @override - Widget build(BuildContext context, WidgetRef ref) { - final sharedUsersList = useState>({}); - final suggestedShareUsers = ref.watch(otherUsersProvider); - - createSharedAlbum() async { - var newAlbum = await ref.watch(albumProvider.notifier).createAlbum(ref.watch(albumTitleProvider), assets); - - if (newAlbum != null) { - ref.watch(albumTitleProvider.notifier).clearAlbumTitle(); - unawaited(context.maybePop(true)); - unawaited(context.navigateTo(const TabControllerRoute(children: [AlbumsRoute()]))); - } - - ScaffoldMessenger( - child: SnackBar( - content: Text( - 'select_user_for_sharing_page_err_album', - style: context.textTheme.bodyLarge?.copyWith(color: context.primaryColor), - ).tr(), - ), - ); - } - - buildTileIcon(UserDto user) { - if (sharedUsersList.value.contains(user)) { - return CircleAvatar(backgroundColor: context.primaryColor, child: const Icon(Icons.check_rounded, size: 25)); - } else { - return UserCircleAvatar(user: user); - } - } - - buildUserList(List users) { - List usersChip = []; - - for (var user in sharedUsersList.value) { - usersChip.add( - Padding( - padding: const EdgeInsets.symmetric(horizontal: 8.0), - child: Chip( - backgroundColor: context.primaryColor.withValues(alpha: 0.15), - label: Text( - user.email, - style: const TextStyle(fontSize: 12, color: Colors.black87, fontWeight: FontWeight.bold), - ), - ), - ), - ); - } - return ListView( - children: [ - Wrap(children: [...usersChip]), - Padding( - padding: const EdgeInsets.all(16.0), - child: const Text( - 'suggestions', - style: TextStyle(fontSize: 14, color: Colors.grey, fontWeight: FontWeight.bold), - ).tr(), - ), - ListView.builder( - primary: false, - shrinkWrap: true, - itemBuilder: ((context, index) { - return ListTile( - leading: buildTileIcon(users[index]), - title: Text(users[index].email, style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold)), - onTap: () { - if (sharedUsersList.value.contains(users[index])) { - sharedUsersList.value = sharedUsersList.value - .where((selectedUser) => selectedUser.id != users[index].id) - .toSet(); - } else { - sharedUsersList.value = {...sharedUsersList.value, users[index]}; - } - }, - ); - }), - itemCount: users.length, - ), - ], - ); - } - - return Scaffold( - appBar: AppBar( - title: Text('invite_to_album', style: TextStyle(color: context.primaryColor)).tr(), - elevation: 0, - centerTitle: false, - leading: IconButton( - icon: const Icon(Icons.close_rounded), - onPressed: () { - unawaited(context.maybePop()); - }, - ), - actions: [ - TextButton( - style: TextButton.styleFrom(foregroundColor: context.primaryColor), - onPressed: sharedUsersList.value.isEmpty ? null : createSharedAlbum, - child: const Text( - "create_album", - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.bold, - // color: context.primaryColor, - ), - ).tr(), - ), - ], - ), - body: suggestedShareUsers.widgetWhen( - onData: (users) { - return buildUserList(users); - }, - ), - ); - } -} diff --git a/mobile/lib/pages/album/album_title.dart b/mobile/lib/pages/album/album_title.dart deleted file mode 100644 index 6c7fc3faaa..0000000000 --- a/mobile/lib/pages/album/album_title.dart +++ /dev/null @@ -1,38 +0,0 @@ -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/album/current_album.provider.dart'; -import 'package:immich_mobile/widgets/album/album_viewer_editable_title.dart'; -import 'package:immich_mobile/providers/auth.provider.dart'; - -class AlbumTitle extends ConsumerWidget { - const AlbumTitle({super.key, required this.titleFocusNode}); - - final FocusNode titleFocusNode; - - @override - Widget build(BuildContext context, WidgetRef ref) { - final userId = ref.watch(authProvider).userId; - final (isOwner, isRemote, albumName) = ref.watch( - currentAlbumProvider.select((album) { - if (album == null) { - return const (false, false, ''); - } - - return (album.ownerId == userId, album.isRemote, album.name); - }), - ); - - if (isOwner && isRemote) { - return Padding( - padding: const EdgeInsets.only(left: 8, right: 8), - child: AlbumViewerEditableTitle(albumName: albumName, titleFocusNode: titleFocusNode), - ); - } - - return Padding( - padding: const EdgeInsets.only(left: 16, right: 8), - child: Text(albumName, style: context.textTheme.headlineLarge?.copyWith(fontWeight: FontWeight.w700)), - ); - } -} diff --git a/mobile/lib/pages/album/album_viewer.dart b/mobile/lib/pages/album/album_viewer.dart deleted file mode 100644 index 97853fb96a..0000000000 --- a/mobile/lib/pages/album/album_viewer.dart +++ /dev/null @@ -1,165 +0,0 @@ -import 'dart:async'; - -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:fluttertoast/fluttertoast.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -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/pages/album/album_control_button.dart'; -import 'package:immich_mobile/pages/album/album_date_range.dart'; -import 'package:immich_mobile/pages/album/album_description.dart'; -import 'package:immich_mobile/pages/album/album_shared_user_icons.dart'; -import 'package:immich_mobile/pages/album/album_title.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/asset_viewer/current_asset.provider.dart'; -import 'package:immich_mobile/providers/timeline.provider.dart'; -import 'package:immich_mobile/utils/immich_loading_overlay.dart'; -import 'package:immich_mobile/providers/multiselect.provider.dart'; -import 'package:immich_mobile/providers/auth.provider.dart'; -import 'package:immich_mobile/widgets/album/album_viewer_appbar.dart'; -import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/widgets/asset_grid/multiselect_grid.dart'; -import 'package:immich_mobile/widgets/common/immich_toast.dart'; - -class AlbumViewer extends HookConsumerWidget { - const AlbumViewer({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final album = ref.watch(currentAlbumProvider); - if (album == null) { - return const SizedBox(); - } - - final titleFocusNode = useFocusNode(); - final descriptionFocusNode = useFocusNode(); - final userId = ref.watch(authProvider).userId; - final isMultiselecting = ref.watch(multiselectProvider); - final isProcessing = useProcessingOverlay(); - final isOwner = ref.watch( - currentAlbumProvider.select((album) { - return album?.ownerId == userId; - }), - ); - - Future onRemoveFromAlbumPressed(Iterable assets) async { - final bool isSuccess = await ref.read(albumProvider.notifier).removeAsset(album, assets); - - if (!isSuccess) { - ImmichToast.show( - context: context, - msg: "album_viewer_appbar_share_err_remove".tr(), - toastType: ToastType.error, - gravity: ToastGravity.BOTTOM, - ); - } - return isSuccess; - } - - /// Find out if the assets in album exist on the device - /// If they exist, add to selected asset state to show they are already selected. - void onAddPhotosPressed() async { - AssetSelectionPageResult? returnPayload = await context.pushRoute( - AlbumAssetSelectionRoute(existingAssets: album.assets, canDeselect: false), - ); - - if (returnPayload != null && returnPayload.selectedAssets.isNotEmpty) { - // Check if there is new assets add - isProcessing.value = true; - - await ref.watch(albumProvider.notifier).addAssets(album, returnPayload.selectedAssets); - - isProcessing.value = false; - } - } - - void onAddUsersPressed() async { - List? sharedUserIds = await context.pushRoute?>( - AlbumAdditionalSharedUserSelectionRoute(album: album), - ); - - if (sharedUserIds != null) { - isProcessing.value = true; - - await ref.watch(albumProvider.notifier).addUsers(album, sharedUserIds); - - isProcessing.value = false; - } - } - - onActivitiesPressed() { - if (album.remoteId != null) { - ref.read(currentAssetProvider.notifier).set(null); - context.pushRoute(const ActivitiesRoute()); - } - } - - return Stack( - children: [ - MultiselectGrid( - key: const ValueKey("albumViewerMultiselectGrid"), - renderListProvider: albumTimelineProvider(album.id), - topWidget: Container( - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - colors: [ - context.primaryColor.withValues(alpha: 0.06), - context.primaryColor.withValues(alpha: 0.04), - Colors.indigo.withValues(alpha: 0.02), - Colors.transparent, - ], - stops: const [0.0, 0.3, 0.7, 1.0], - ), - ), - child: Column( - mainAxisAlignment: MainAxisAlignment.end, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const SizedBox(height: 32), - const AlbumDateRange(), - AlbumTitle(key: const ValueKey("albumTitle"), titleFocusNode: titleFocusNode), - AlbumDescription(key: const ValueKey("albumDescription"), descriptionFocusNode: descriptionFocusNode), - const AlbumSharedUserIcons(), - if (album.isRemote) - Padding( - padding: const EdgeInsets.only(left: 16.0), - child: AlbumControlButton( - key: const ValueKey("albumControlButton"), - onAddPhotosPressed: onAddPhotosPressed, - onAddUsersPressed: isOwner ? onAddUsersPressed : null, - ), - ), - const SizedBox(height: 8), - ], - ), - ), - onRemoveFromAlbum: onRemoveFromAlbumPressed, - editEnabled: album.ownerId == userId, - ), - AnimatedPositioned( - key: const ValueKey("albumViewerAppbarPositioned"), - duration: const Duration(milliseconds: 300), - top: isMultiselecting ? -(kToolbarHeight + context.padding.top) : 0, - left: 0, - right: 0, - child: AlbumViewerAppbar( - key: const ValueKey("albumViewerAppbar"), - titleFocusNode: titleFocusNode, - descriptionFocusNode: descriptionFocusNode, - userId: userId, - onAddPhotos: onAddPhotosPressed, - onAddUsers: onAddUsersPressed, - onActivities: onActivitiesPressed, - ), - ), - ], - ); - } -} diff --git a/mobile/lib/pages/album/album_viewer.page.dart b/mobile/lib/pages/album/album_viewer.page.dart deleted file mode 100644 index c99dacd9b7..0000000000 --- a/mobile/lib/pages/album/album_viewer.page.dart +++ /dev/null @@ -1,29 +0,0 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/pages/album/album_viewer.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/timeline.provider.dart'; - -@RoutePage() -class AlbumViewerPage extends HookConsumerWidget { - final int albumId; - - const AlbumViewerPage({super.key, required this.albumId}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - // Listen provider to prevent autoDispose when navigating to other routes from within the viewer page - ref.listen(currentAlbumProvider, (_, __) {}); - - // This call helps rendering the asset selection instantly - ref.listen(assetSelectionTimelineProvider, (_, __) {}); - - ref.listen(albumWatcher(albumId), (_, albumFuture) { - albumFuture.whenData((value) => ref.read(currentAlbumProvider.notifier).set(value)); - }); - - return const Scaffold(body: AlbumViewer()); - } -} diff --git a/mobile/lib/pages/albums/albums.page.dart b/mobile/lib/pages/albums/albums.page.dart deleted file mode 100644 index 5f155c2f0d..0000000000 --- a/mobile/lib/pages/albums/albums.page.dart +++ /dev/null @@ -1,359 +0,0 @@ -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/extensions/translate_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'; -import 'package:immich_mobile/widgets/common/search_field.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(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: const 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: const BorderRadius.all(Radius.circular(24)), - gradient: LinearGradient( - colors: [ - context.colorScheme.primary.withValues(alpha: 0.075), - context.colorScheme.primary.withValues(alpha: 0.09), - context.colorScheme.primary.withValues(alpha: 0.075), - ], - begin: Alignment.topLeft, - end: Alignment.bottomRight, - transform: const GradientRotation(0.5 * pi), - ), - ), - child: SearchField( - autofocus: false, - contentPadding: const EdgeInsets.all(16), - hintText: 'search_albums'.tr(), - prefixIcon: const Icon(Icons.search_rounded), - suffixIcon: searchController.text.isNotEmpty - ? IconButton(icon: const Icon(Icons.clear_rounded), onPressed: clearSearch) - : null, - 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 != null - ? Text( - '${'items_count'.t(context: context, args: {'count': sorted[index].assetCount})} • ${sorted[index].ownerId != userId ? 'shared_by_user'.t(context: context, args: {'user': sorted[index].ownerName!}) : 'owned'.t(context: context)}', - 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, - ), - ); - }, - ), - ), - ], - ), - ), - resizeToAvoidBottomInset: false, - ); - } -} - -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: const BorderRadius.all(Radius.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: const WidgetStatePropertyAll(1), - shape: WidgetStateProperty.all( - const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(24))), - ), - padding: const 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( - const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.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), - ), - ), - ], - ), - ); - }, - ); - } -} diff --git a/mobile/lib/pages/backup/album_preview.page.dart b/mobile/lib/pages/backup/album_preview.page.dart deleted file mode 100644 index def31afcd4..0000000000 --- a/mobile/lib/pages/backup/album_preview.page.dart +++ /dev/null @@ -1,64 +0,0 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/album.entity.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/extensions/theme_extensions.dart'; -import 'package:immich_mobile/repositories/album_media.repository.dart'; -import 'package:immich_mobile/widgets/common/immich_thumbnail.dart'; - -@RoutePage() -class AlbumPreviewPage extends HookConsumerWidget { - final Album album; - const AlbumPreviewPage({super.key, required this.album}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final assets = useState>([]); - - getAssetsInAlbum() async { - assets.value = await ref.read(albumMediaRepositoryProvider).getAssets(album.localId!); - } - - useEffect(() { - getAssetsInAlbum(); - return null; - }, []); - - return Scaffold( - appBar: AppBar( - elevation: 0, - title: Column( - children: [ - Text(album.name, style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold)), - Padding( - padding: const EdgeInsets.only(top: 4.0), - child: Text( - "ID ${album.id}", - style: TextStyle( - fontSize: 10, - color: context.colorScheme.onSurfaceSecondary, - fontWeight: FontWeight.bold, - ), - ), - ), - ], - ), - leading: IconButton(onPressed: () => context.maybePop(), icon: const Icon(Icons.arrow_back_ios_new_rounded)), - ), - body: GridView.builder( - gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 5, - crossAxisSpacing: 2, - mainAxisSpacing: 2, - ), - itemCount: assets.value.length, - itemBuilder: (context, index) { - return ImmichThumbnail(asset: assets.value[index], width: 100, height: 100); - }, - ), - ); - } -} diff --git a/mobile/lib/pages/backup/backup_album_selection.page.dart b/mobile/lib/pages/backup/backup_album_selection.page.dart deleted file mode 100644 index d222211577..0000000000 --- a/mobile/lib/pages/backup/backup_album_selection.page.dart +++ /dev/null @@ -1,225 +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/providers/album/album.provider.dart'; -import 'package:immich_mobile/providers/backup/backup.provider.dart'; -import 'package:immich_mobile/services/app_settings.service.dart'; -import 'package:immich_mobile/utils/hooks/app_settings_update_hook.dart'; -import 'package:immich_mobile/widgets/backup/album_info_card.dart'; -import 'package:immich_mobile/widgets/backup/album_info_list_tile.dart'; -import 'package:immich_mobile/widgets/settings/settings_switch_list_tile.dart'; - -@RoutePage() -class BackupAlbumSelectionPage extends HookConsumerWidget { - const BackupAlbumSelectionPage({super.key}); - @override - Widget build(BuildContext context, WidgetRef ref) { - final selectedBackupAlbums = ref.watch(backupProvider).selectedBackupAlbums; - final excludedBackupAlbums = ref.watch(backupProvider).excludedBackupAlbums; - final enableSyncUploadAlbum = useAppSettingsState(AppSettingsEnum.syncAlbums); - final isDarkTheme = context.isDarkTheme; - final albums = ref.watch(backupProvider).availableAlbums; - - useEffect(() { - ref.watch(backupProvider.notifier).getBackupInfo(); - return null; - }, []); - - buildAlbumSelectionList() { - if (albums.isEmpty) { - return const SliverToBoxAdapter(child: Center(child: CircularProgressIndicator())); - } - - return SliverPadding( - padding: const EdgeInsets.symmetric(vertical: 12.0), - sliver: SliverList( - delegate: SliverChildBuilderDelegate(((context, index) { - return AlbumInfoListTile(album: albums[index]); - }), childCount: albums.length), - ), - ); - } - - buildAlbumSelectionGrid() { - if (albums.isEmpty) { - return const SliverToBoxAdapter(child: Center(child: CircularProgressIndicator())); - } - - return SliverPadding( - padding: const EdgeInsets.all(12.0), - sliver: SliverGrid.builder( - gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( - maxCrossAxisExtent: 300, - mainAxisSpacing: 12, - crossAxisSpacing: 12, - ), - itemCount: albums.length, - itemBuilder: ((context, index) { - return AlbumInfoCard(album: albums[index]); - }), - ), - ); - } - - buildSelectedAlbumNameChip() { - return selectedBackupAlbums.map((album) { - void removeSelection() => ref.read(backupProvider.notifier).removeAlbumForBackup(album); - - return Padding( - padding: const EdgeInsets.only(right: 8.0), - child: GestureDetector( - onTap: removeSelection, - child: Chip( - label: Text( - album.name, - style: TextStyle( - fontSize: 12, - color: isDarkTheme ? Colors.black : Colors.white, - fontWeight: FontWeight.bold, - ), - ), - backgroundColor: context.primaryColor, - deleteIconColor: isDarkTheme ? Colors.black : Colors.white, - deleteIcon: const Icon(Icons.cancel_rounded, size: 15), - onDeleted: removeSelection, - ), - ), - ); - }).toSet(); - } - - buildExcludedAlbumNameChip() { - return excludedBackupAlbums.map((album) { - void removeSelection() { - ref.watch(backupProvider.notifier).removeExcludedAlbumForBackup(album); - } - - return GestureDetector( - onTap: removeSelection, - child: Padding( - padding: const EdgeInsets.only(right: 8.0), - child: Chip( - label: Text( - album.name, - style: TextStyle(fontSize: 12, color: context.scaffoldBackgroundColor, fontWeight: FontWeight.bold), - ), - backgroundColor: Colors.red[300], - deleteIconColor: context.scaffoldBackgroundColor, - deleteIcon: const Icon(Icons.cancel_rounded, size: 15), - onDeleted: removeSelection, - ), - ), - ); - }).toSet(); - } - - handleSyncAlbumToggle(bool isEnable) async { - if (isEnable) { - await ref.read(albumProvider.notifier).refreshRemoteAlbums(); - for (final album in selectedBackupAlbums) { - await ref.read(albumProvider.notifier).createSyncAlbum(album.name); - } - } - } - - return Scaffold( - appBar: AppBar( - leading: IconButton(onPressed: () => context.maybePop(), icon: const Icon(Icons.arrow_back_ios_rounded)), - title: const Text("backup_album_selection_page_select_albums").tr(), - elevation: 0, - ), - body: CustomScrollView( - physics: const ClampingScrollPhysics(), - slivers: [ - SliverToBoxAdapter( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 16.0), - child: Text("backup_album_selection_page_selection_info", style: context.textTheme.titleSmall).tr(), - ), - - // Selected Album Chips - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0), - child: Wrap(children: [...buildSelectedAlbumNameChip(), ...buildExcludedAlbumNameChip()]), - ), - - SettingsSwitchListTile( - valueNotifier: enableSyncUploadAlbum, - title: "sync_albums".tr(), - subtitle: "sync_upload_album_setting_subtitle".tr(), - contentPadding: const EdgeInsets.symmetric(horizontal: 16), - titleStyle: context.textTheme.bodyLarge?.copyWith(fontWeight: FontWeight.bold), - subtitleStyle: context.textTheme.labelLarge?.copyWith(color: context.colorScheme.primary), - onChanged: handleSyncAlbumToggle, - ), - - ListTile( - title: Text( - "backup_album_selection_page_albums_device".tr( - namedArgs: {'count': ref.watch(backupProvider).availableAlbums.length.toString()}, - ), - style: context.textTheme.titleSmall, - ), - subtitle: Padding( - padding: const EdgeInsets.symmetric(vertical: 8.0), - child: Text( - "backup_album_selection_page_albums_tap", - style: context.textTheme.labelLarge?.copyWith(color: context.primaryColor), - ).tr(), - ), - trailing: IconButton( - splashRadius: 16, - icon: Icon(Icons.info, size: 20, color: context.primaryColor), - onPressed: () { - // show the dialog - showDialog( - context: context, - builder: (BuildContext context) { - return AlertDialog( - shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(10))), - elevation: 5, - title: Text( - 'backup_album_selection_page_selection_info', - style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: context.primaryColor), - ).tr(), - content: SingleChildScrollView( - child: ListBody( - children: [ - const Text( - 'backup_album_selection_page_assets_scatter', - style: TextStyle(fontSize: 14), - ).tr(), - ], - ), - ), - ); - }, - ); - }, - ), - ), - - // buildSearchBar(), - ], - ), - ), - SliverLayoutBuilder( - builder: (context, constraints) { - if (constraints.crossAxisExtent > 600) { - return buildAlbumSelectionGrid(); - } else { - return buildAlbumSelectionList(); - } - }, - ), - ], - ), - ); - } -} diff --git a/mobile/lib/pages/backup/backup_controller.page.dart b/mobile/lib/pages/backup/backup_controller.page.dart deleted file mode 100644 index 1e008be1bb..0000000000 --- a/mobile/lib/pages/backup/backup_controller.page.dart +++ /dev/null @@ -1,286 +0,0 @@ -import 'dart:io'; -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/backup/backup_state.model.dart'; -import 'package:immich_mobile/providers/album/album.provider.dart'; -import 'package:immich_mobile/providers/backup/backup.provider.dart'; -import 'package:immich_mobile/providers/backup/error_backup_list.provider.dart'; -import 'package:immich_mobile/providers/backup/ios_background_settings.provider.dart'; -import 'package:immich_mobile/providers/backup/manual_upload.provider.dart'; -import 'package:immich_mobile/providers/websocket.provider.dart'; -import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/widgets/backup/backup_info_card.dart'; -import 'package:immich_mobile/widgets/backup/current_backup_asset_info_box.dart'; -import 'package:wakelock_plus/wakelock_plus.dart'; - -@RoutePage() -class BackupControllerPage extends HookConsumerWidget { - const BackupControllerPage({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - BackUpState backupState = ref.watch(backupProvider); - final hasAnyAlbum = backupState.selectedBackupAlbums.isNotEmpty; - final didGetBackupInfo = useState(false); - - bool hasExclusiveAccess = backupState.backupProgress != BackUpProgressEnum.inBackground; - bool shouldBackup = - backupState.allUniqueAssets.length - backupState.selectedAlbumsBackupAssetsIds.length == 0 || - !hasExclusiveAccess - ? false - : true; - - useEffect(() { - // Update the background settings information just to make sure we - // have the latest, since the platform channel will not update - // automatically - if (Platform.isIOS) { - ref.watch(iOSBackgroundSettingsProvider.notifier).refresh(); - } - - ref.watch(websocketProvider.notifier).stopListenToEvent('on_upload_success'); - - return () { - WakelockPlus.disable(); - }; - }, []); - - useEffect(() { - if (backupState.backupProgress == BackUpProgressEnum.idle && !didGetBackupInfo.value) { - ref.watch(backupProvider.notifier).getBackupInfo(); - didGetBackupInfo.value = true; - } - return null; - }, [backupState.backupProgress]); - - useEffect(() { - if (backupState.backupProgress == BackUpProgressEnum.inProgress) { - WakelockPlus.enable(); - } else { - WakelockPlus.disable(); - } - - return null; - }, [backupState.backupProgress]); - - Widget buildSelectedAlbumName() { - var text = "backup_controller_page_backup_selected".tr(); - var albums = ref.watch(backupProvider).selectedBackupAlbums; - - if (albums.isNotEmpty) { - for (var album in albums) { - if (album.name == "Recent" || album.name == "Recents") { - text += "${album.name} (${'all'.tr()}), "; - } else { - text += "${album.name}, "; - } - } - - return Padding( - padding: const EdgeInsets.only(top: 8.0), - child: Text( - text.trim().substring(0, text.length - 2), - style: context.textTheme.labelLarge?.copyWith(color: context.primaryColor), - ), - ); - } else { - return Padding( - padding: const EdgeInsets.only(top: 8.0), - child: Text( - "backup_controller_page_none_selected".tr(), - style: context.textTheme.labelLarge?.copyWith(color: context.primaryColor), - ), - ); - } - } - - Widget buildExcludedAlbumName() { - var text = "backup_controller_page_excluded".tr(); - var albums = ref.watch(backupProvider).excludedBackupAlbums; - - if (albums.isNotEmpty) { - for (var album in albums) { - text += "${album.name}, "; - } - - return Padding( - padding: const EdgeInsets.only(top: 8.0), - child: Text( - text.trim().substring(0, text.length - 2), - style: context.textTheme.labelLarge?.copyWith(color: Colors.red[300]), - ), - ); - } else { - return const SizedBox(); - } - } - - buildFolderSelectionTile() { - return Padding( - padding: const EdgeInsets.only(top: 8.0), - child: Card( - shape: RoundedRectangleBorder( - borderRadius: const BorderRadius.all(Radius.circular(20)), - side: BorderSide(color: context.colorScheme.outlineVariant, width: 1), - ), - elevation: 0, - borderOnForeground: false, - child: ListTile( - minVerticalPadding: 18, - title: Text("backup_controller_page_albums", style: context.textTheme.titleMedium).tr(), - subtitle: Padding( - padding: const EdgeInsets.only(top: 8.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - "backup_controller_page_to_backup", - style: context.textTheme.bodyMedium?.copyWith(color: context.colorScheme.onSurfaceSecondary), - ).tr(), - buildSelectedAlbumName(), - buildExcludedAlbumName(), - ], - ), - ), - trailing: ElevatedButton( - onPressed: () async { - await context.pushRoute(const BackupAlbumSelectionRoute()); - // waited until returning from selection - await ref.read(backupProvider.notifier).backupAlbumSelectionDone(); - // waited until backup albums are stored in DB - await ref.read(albumProvider.notifier).refreshDeviceAlbums(); - }, - child: const Text("select", style: TextStyle(fontWeight: FontWeight.bold)).tr(), - ), - ), - ), - ); - } - - void startBackup() { - ref.watch(errorBackupListProvider.notifier).empty(); - if (ref.watch(backupProvider).backupProgress != BackUpProgressEnum.inBackground) { - ref.watch(backupProvider.notifier).startBackupProcess(); - } - } - - Widget buildBackupButton() { - return Padding( - padding: const EdgeInsets.only(top: 24), - child: Container( - child: - backupState.backupProgress == BackUpProgressEnum.inProgress || - backupState.backupProgress == BackUpProgressEnum.manualInProgress - ? ElevatedButton( - style: ElevatedButton.styleFrom( - foregroundColor: Colors.grey[50], - backgroundColor: Colors.red[300], - // padding: const EdgeInsets.all(14), - ), - onPressed: () { - if (backupState.backupProgress == BackUpProgressEnum.manualInProgress) { - ref.read(manualUploadProvider.notifier).cancelBackup(); - } else { - ref.read(backupProvider.notifier).cancelBackup(); - } - }, - child: const Text("cancel", style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold)).tr(), - ) - : ElevatedButton( - onPressed: shouldBackup ? startBackup : null, - child: const Text( - "backup_controller_page_start_backup", - style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold), - ).tr(), - ), - ), - ); - } - - buildBackgroundBackupInfo() { - return ListTile( - leading: const Icon(Icons.info_outline_rounded), - title: Text('background_backup_running_error'.tr()), - ); - } - - buildLoadingIndicator() { - return const Padding( - padding: EdgeInsets.only(top: 42.0), - child: Center(child: CircularProgressIndicator()), - ); - } - - return Scaffold( - appBar: AppBar( - elevation: 0, - title: const Text("backup_controller_page_backup").tr(), - leading: IconButton( - onPressed: () { - ref.watch(websocketProvider.notifier).listenUploadEvent(); - context.maybePop(true); - }, - splashRadius: 24, - icon: const Icon(Icons.arrow_back_ios_rounded), - ), - actions: [ - Padding( - padding: const EdgeInsets.only(right: 8.0), - child: IconButton( - onPressed: () => context.pushRoute(const BackupOptionsRoute()), - splashRadius: 24, - icon: const Icon(Icons.settings_outlined), - ), - ), - ], - ), - body: Stack( - children: [ - Padding( - padding: const EdgeInsets.only(left: 16.0, right: 16, bottom: 32), - child: ListView( - // crossAxisAlignment: CrossAxisAlignment.start, - children: hasAnyAlbum - ? [ - buildFolderSelectionTile(), - BackupInfoCard( - title: "total".tr(), - subtitle: "backup_controller_page_total_sub".tr(), - info: ref.watch(backupProvider).availableAlbums.isEmpty - ? "..." - : "${backupState.allUniqueAssets.length}", - ), - BackupInfoCard( - title: "backup_controller_page_backup".tr(), - subtitle: "backup_controller_page_backup_sub".tr(), - info: ref.watch(backupProvider).availableAlbums.isEmpty - ? "..." - : "${backupState.selectedAlbumsBackupAssetsIds.length}", - ), - BackupInfoCard( - title: "backup_controller_page_remainder".tr(), - subtitle: "backup_controller_page_remainder_sub".tr(), - info: ref.watch(backupProvider).availableAlbums.isEmpty - ? "..." - : "${max(0, backupState.allUniqueAssets.length - backupState.selectedAlbumsBackupAssetsIds.length)}", - ), - const Divider(), - const CurrentUploadingAssetInfoBox(), - if (!hasExclusiveAccess) buildBackgroundBackupInfo(), - buildBackupButton(), - ] - : [buildFolderSelectionTile(), if (!didGetBackupInfo.value) buildLoadingIndicator()], - ), - ), - ], - ), - ); - } -} diff --git a/mobile/lib/pages/backup/backup_options.page.dart b/mobile/lib/pages/backup/backup_options.page.dart deleted file mode 100644 index 846a32a742..0000000000 --- a/mobile/lib/pages/backup/backup_options.page.dart +++ /dev/null @@ -1,24 +0,0 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:immich_mobile/widgets/settings/backup_settings/backup_settings.dart'; - -@RoutePage() -class BackupOptionsPage extends StatelessWidget { - const BackupOptionsPage({super.key}); - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - elevation: 0, - title: const Text("backup_options_page_title").tr(), - leading: IconButton( - onPressed: () => context.maybePop(true), - splashRadius: 24, - icon: const Icon(Icons.arrow_back_ios_rounded), - ), - ), - body: const BackupSettings(), - ); - } -} diff --git a/mobile/lib/pages/backup/failed_backup_status.page.dart b/mobile/lib/pages/backup/failed_backup_status.page.dart deleted file mode 100644 index a97a133b89..0000000000 --- a/mobile/lib/pages/backup/failed_backup_status.page.dart +++ /dev/null @@ -1,116 +0,0 @@ -import 'package:auto_route/auto_route.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/presentation/widgets/images/local_image_provider.dart'; -import 'package:immich_mobile/providers/backup/error_backup_list.provider.dart'; -import 'package:intl/intl.dart'; -import 'package:immich_mobile/domain/models/asset/base_asset.model.dart' as base_asset; - -@RoutePage() -class FailedBackupStatusPage extends HookConsumerWidget { - const FailedBackupStatusPage({super.key}); - @override - Widget build(BuildContext context, WidgetRef ref) { - final errorBackupList = ref.watch(errorBackupListProvider); - - return Scaffold( - appBar: AppBar( - elevation: 0, - title: Text( - "Failed Backup (${errorBackupList.length})", - style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold), - ), - leading: IconButton( - onPressed: () { - context.maybePop(true); - }, - splashRadius: 24, - icon: const Icon(Icons.arrow_back_ios_rounded), - ), - ), - body: ListView.builder( - shrinkWrap: true, - itemCount: errorBackupList.length, - itemBuilder: ((context, index) { - var errorAsset = errorBackupList.elementAt(index); - - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 4), - child: Card( - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.all( - Radius.circular(15), // if you need this - ), - side: BorderSide(color: Colors.black12, width: 1), - ), - elevation: 0, - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - ConstrainedBox( - constraints: const BoxConstraints(minWidth: 100, minHeight: 100, maxWidth: 100, maxHeight: 150), - child: ClipRRect( - borderRadius: const BorderRadius.only( - bottomLeft: Radius.circular(15), - topLeft: Radius.circular(15), - ), - clipBehavior: Clip.hardEdge, - child: Image( - fit: BoxFit.cover, - image: LocalThumbProvider(id: errorAsset.asset.localId!, assetType: base_asset.AssetType.video), - ), - ), - ), - Expanded( - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - DateFormat.yMMMMd().format( - DateTime.parse(errorAsset.fileCreatedAt.toString()).toLocal(), - ), - style: TextStyle( - fontWeight: FontWeight.w600, - color: context.isDarkTheme ? Colors.white70 : Colors.grey[800], - ), - ), - Icon(Icons.error, color: Colors.red.withAlpha(200), size: 18), - ], - ), - Padding( - padding: const EdgeInsets.symmetric(vertical: 8.0), - child: Text( - errorAsset.fileName, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: TextStyle(fontWeight: FontWeight.bold, color: context.primaryColor), - ), - ), - Text( - errorAsset.errorMessage, - style: TextStyle( - fontWeight: FontWeight.w500, - color: context.isDarkTheme ? Colors.white70 : Colors.grey[800], - ), - ), - ], - ), - ), - ), - ], - ), - ), - ); - }), - ), - ); - } -} diff --git a/mobile/lib/pages/common/activities.page.dart b/mobile/lib/pages/common/activities.page.dart deleted file mode 100644 index 9d1123dbca..0000000000 --- a/mobile/lib/pages/common/activities.page.dart +++ /dev/null @@ -1,97 +0,0 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:collection/collection.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart' hide Store; -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/models/activities/activity.model.dart'; -import 'package:immich_mobile/providers/activity.provider.dart'; -import 'package:immich_mobile/providers/album/current_album.provider.dart'; -import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart'; -import 'package:immich_mobile/providers/user.provider.dart'; -import 'package:immich_mobile/widgets/activities/activity_text_field.dart'; -import 'package:immich_mobile/widgets/activities/activity_tile.dart'; -import 'package:immich_mobile/widgets/activities/dismissible_activity.dart'; - -@RoutePage() -class ActivitiesPage extends HookConsumerWidget { - const ActivitiesPage({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - // Album has to be set in the provider before reaching this page - final album = ref.watch(currentAlbumProvider)!; - final asset = ref.watch(currentAssetProvider); - final user = ref.watch(currentUserProvider); - - final activityNotifier = ref.read(albumActivityProvider(album.remoteId!, asset?.remoteId).notifier); - final activities = ref.watch(albumActivityProvider(album.remoteId!, asset?.remoteId)); - - final listViewScrollController = useScrollController(); - - Future onAddComment(String comment) async { - await activityNotifier.addComment(comment); - // Scroll to the end of the list to show the newly added activity - await listViewScrollController.animateTo( - listViewScrollController.position.maxScrollExtent + 200, - duration: const Duration(milliseconds: 600), - curve: Curves.fastOutSlowIn, - ); - } - - return Scaffold( - appBar: AppBar(title: asset == null ? Text(album.name) : null), - body: activities.widgetWhen( - onData: (data) { - final liked = data.firstWhereOrNull( - (a) => a.type == ActivityType.like && a.user.id == user?.id && a.assetId == asset?.remoteId, - ); - - return SafeArea( - child: Stack( - children: [ - ListView.builder( - controller: listViewScrollController, - // +1 to display an additional over-scroll space after the last element - itemCount: data.length + 1, - itemBuilder: (context, index) { - // Additional vertical gap after the last element - if (index == data.length) { - return const SizedBox(height: 80); - } - - final activity = data[index]; - final canDelete = activity.user.id == user?.id || album.ownerId == user?.id; - - return Padding( - padding: const EdgeInsets.all(5), - child: DismissibleActivity( - activity.id, - ActivityTile(activity), - onDismiss: canDelete - ? (activityId) async => await activityNotifier.removeActivity(activity.id) - : null, - ), - ); - }, - ), - Align( - alignment: Alignment.bottomCenter, - child: Container( - color: context.scaffoldBackgroundColor, - child: ActivityTextField( - isEnabled: album.activityEnabled, - likeId: liked?.id, - onSubmit: onAddComment, - ), - ), - ), - ], - ), - ); - }, - ), - ); - } -} diff --git a/mobile/lib/pages/common/change_experience.page.dart b/mobile/lib/pages/common/change_experience.page.dart deleted file mode 100644 index 2cc3dede1e..0000000000 --- a/mobile/lib/pages/common/change_experience.page.dart +++ /dev/null @@ -1,168 +0,0 @@ -import 'dart:async'; - -import 'package:auto_route/auto_route.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/domain/models/store.model.dart'; -import 'package:immich_mobile/entities/store.entity.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/infrastructure/repositories/store.repository.dart'; -import 'package:immich_mobile/providers/album/album.provider.dart'; -import 'package:immich_mobile/providers/asset.provider.dart'; -import 'package:immich_mobile/providers/background_sync.provider.dart'; -import 'package:immich_mobile/providers/backup/backup.provider.dart'; -import 'package:immich_mobile/providers/backup/manual_upload.provider.dart'; -import 'package:immich_mobile/providers/gallery_permission.provider.dart'; -import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; -import 'package:immich_mobile/providers/infrastructure/platform.provider.dart'; -import 'package:immich_mobile/providers/infrastructure/readonly_mode.provider.dart'; -import 'package:immich_mobile/providers/websocket.provider.dart'; -import 'package:immich_mobile/services/app_settings.service.dart'; -import 'package:immich_mobile/services/background.service.dart'; -import 'package:immich_mobile/utils/migration.dart'; -import 'package:logging/logging.dart'; -import 'package:permission_handler/permission_handler.dart'; - -@RoutePage() -class ChangeExperiencePage extends ConsumerStatefulWidget { - final bool switchingToBeta; - - const ChangeExperiencePage({super.key, required this.switchingToBeta}); - - @override - ConsumerState createState() => _ChangeExperiencePageState(); -} - -class _ChangeExperiencePageState extends ConsumerState { - AsyncValue hasMigrated = const AsyncValue.loading(); - - @override - void initState() { - super.initState(); - WidgetsBinding.instance.addPostFrameCallback((_) => _handleMigration()); - } - - Future _handleMigration() async { - try { - await _performMigrationLogic().timeout( - const Duration(minutes: 3), - onTimeout: () async { - await IsarStoreRepository(ref.read(isarProvider)).upsert(StoreKey.betaTimeline, widget.switchingToBeta); - await DriftStoreRepository(ref.read(driftProvider)).upsert(StoreKey.betaTimeline, widget.switchingToBeta); - }, - ); - - if (mounted) { - setState(() { - HapticFeedback.heavyImpact(); - hasMigrated = const AsyncValue.data(true); - }); - } - } catch (e, s) { - Logger("ChangeExperiencePage").severe("Error during migration", e, s); - if (mounted) { - setState(() { - hasMigrated = AsyncValue.error(e, s); - }); - } - } - } - - Future _performMigrationLogic() async { - if (widget.switchingToBeta) { - final assetNotifier = ref.read(assetProvider.notifier); - if (assetNotifier.mounted) { - assetNotifier.dispose(); - } - final albumNotifier = ref.read(albumProvider.notifier); - if (albumNotifier.mounted) { - albumNotifier.dispose(); - } - - // Cancel uploads - await Store.put(StoreKey.backgroundBackup, false); - ref - .read(backupProvider.notifier) - .configureBackgroundBackup(enabled: false, onBatteryInfo: () {}, onError: (_) {}); - ref.read(backupProvider.notifier).setAutoBackup(false); - ref.read(backupProvider.notifier).cancelBackup(); - ref.read(manualUploadProvider.notifier).cancelBackup(); - // Start listening to new websocket events - ref.read(websocketProvider.notifier).stopListenToOldEvents(); - ref.read(websocketProvider.notifier).startListeningToBetaEvents(); - - await ref.read(driftProvider).reset(); - await Store.put(StoreKey.shouldResetSync, true); - final delay = Store.get(StoreKey.backupTriggerDelay, AppSettingsEnum.backupTriggerDelay.defaultValue); - if (delay >= 1000) { - await Store.put(StoreKey.backupTriggerDelay, (delay / 1000).toInt()); - } - final permission = await ref.read(galleryPermissionNotifier.notifier).requestGalleryPermission(); - - if (permission.isGranted) { - await ref.read(backgroundSyncProvider).syncLocal(full: true); - await migrateDeviceAssetToSqlite(ref.read(isarProvider), ref.read(driftProvider)); - await migrateBackupAlbumsToSqlite(ref.read(isarProvider), ref.read(driftProvider)); - await migrateStoreToSqlite(ref.read(isarProvider), ref.read(driftProvider)); - await ref.read(backgroundServiceProvider).disableService(); - } - } else { - await ref.read(backgroundSyncProvider).cancel(); - ref.read(websocketProvider.notifier).stopListeningToBetaEvents(); - ref.read(websocketProvider.notifier).startListeningToOldEvents(); - ref.read(readonlyModeProvider.notifier).setReadonlyMode(false); - await migrateStoreToIsar(ref.read(isarProvider), ref.read(driftProvider)); - await ref.read(backgroundServiceProvider).resumeServiceIfEnabled(); - await ref.read(backgroundWorkerFgServiceProvider).disable(); - } - - await IsarStoreRepository(ref.read(isarProvider)).upsert(StoreKey.betaTimeline, widget.switchingToBeta); - await DriftStoreRepository(ref.read(driftProvider)).upsert(StoreKey.betaTimeline, widget.switchingToBeta); - } - - @override - Widget build(BuildContext context) { - return Scaffold( - body: Center( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - AnimatedSwitcher( - duration: Durations.long4, - child: hasMigrated.when( - data: (data) => const Icon(Icons.check_circle_rounded, color: Colors.green, size: 48.0), - error: (error, stackTrace) => const Icon(Icons.error, color: Colors.red, size: 48.0), - loading: () => const SizedBox(width: 50.0, height: 50.0, child: CircularProgressIndicator()), - ), - ), - const SizedBox(height: 16.0), - SizedBox( - width: 300.0, - child: AnimatedSwitcher( - duration: Durations.long4, - child: hasMigrated.when( - data: (data) => Text( - "Migration success!\nPlease close and reopen the app to apply changes", - style: context.textTheme.titleMedium, - textAlign: TextAlign.center, - ), - error: (error, stackTrace) => Text( - "Migration failed!\nError: $error", - style: context.textTheme.titleMedium, - textAlign: TextAlign.center, - ), - loading: () => Text( - "Data migration in progress...\nPlease wait and don't close this page", - style: context.textTheme.titleMedium, - textAlign: TextAlign.center, - ), - ), - ), - ), - ], - ), - ), - ); - } -} diff --git a/mobile/lib/pages/common/create_album.page.dart b/mobile/lib/pages/common/create_album.page.dart deleted file mode 100644 index 0a28dfeb5a..0000000000 --- a/mobile/lib/pages/common/create_album.page.dart +++ /dev/null @@ -1,238 +0,0 @@ -import 'dart:async'; - -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/entities/asset.entity.dart'; -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/providers/album/album.provider.dart'; -import 'package:immich_mobile/providers/album/album_title.provider.dart'; -import 'package:immich_mobile/providers/album/album_viewer.provider.dart'; -import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/widgets/album/album_action_filled_button.dart'; -import 'package:immich_mobile/widgets/album/album_title_text_field.dart'; -import 'package:immich_mobile/widgets/album/album_viewer_editable_description.dart'; -import 'package:immich_mobile/widgets/album/shared_album_thumbnail_image.dart'; - -@RoutePage() -// ignore: must_be_immutable -class CreateAlbumPage extends HookConsumerWidget { - final List? assets; - - const CreateAlbumPage({super.key, this.assets}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final albumTitleController = useTextEditingController.fromValue(TextEditingValue.empty); - final albumTitleTextFieldFocusNode = useFocusNode(); - final albumDescriptionTextFieldFocusNode = useFocusNode(); - final isAlbumTitleTextFieldFocus = useState(false); - final isAlbumTitleEmpty = useState(true); - final selectedAssets = useState>(assets != null ? Set.from(assets!) : const {}); - - void onBackgroundTapped() { - albumTitleTextFieldFocusNode.unfocus(); - albumDescriptionTextFieldFocusNode.unfocus(); - isAlbumTitleTextFieldFocus.value = false; - - if (albumTitleController.text.isEmpty) { - albumTitleController.text = 'create_album_page_untitled'.tr(); - isAlbumTitleEmpty.value = false; - ref.watch(albumTitleProvider.notifier).setAlbumTitle('create_album_page_untitled'.tr()); - } - } - - onSelectPhotosButtonPressed() async { - AssetSelectionPageResult? selectedAsset = await context.pushRoute( - AlbumAssetSelectionRoute(existingAssets: selectedAssets.value, canDeselect: true), - ); - if (selectedAsset == null) { - selectedAssets.value = const {}; - } else { - selectedAssets.value = selectedAsset.selectedAssets; - } - } - - buildTitleInputField() { - return Padding( - padding: const EdgeInsets.only(right: 10, left: 10), - child: AlbumTitleTextField( - isAlbumTitleEmpty: isAlbumTitleEmpty, - albumTitleTextFieldFocusNode: albumTitleTextFieldFocusNode, - albumTitleController: albumTitleController, - isAlbumTitleTextFieldFocus: isAlbumTitleTextFieldFocus, - ), - ); - } - - buildDescriptionInputField() { - return Padding( - padding: const EdgeInsets.only(right: 10, left: 10), - child: AlbumViewerEditableDescription( - albumDescription: '', - descriptionFocusNode: albumDescriptionTextFieldFocusNode, - ), - ); - } - - buildTitle() { - if (selectedAssets.value.isEmpty) { - return SliverToBoxAdapter( - child: Padding( - padding: const EdgeInsets.only(top: 200, left: 18), - child: Text('create_shared_album_page_share_add_assets', style: context.textTheme.labelLarge).tr(), - ), - ); - } - - return const SliverToBoxAdapter(); - } - - buildSelectPhotosButton() { - if (selectedAssets.value.isEmpty) { - return SliverToBoxAdapter( - child: Padding( - padding: const EdgeInsets.only(top: 16, left: 16, right: 16), - child: FilledButton.icon( - style: FilledButton.styleFrom( - alignment: Alignment.centerLeft, - padding: const EdgeInsets.symmetric(vertical: 24, horizontal: 16), - shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(10))), - backgroundColor: context.colorScheme.surfaceContainerHigh, - ), - onPressed: onSelectPhotosButtonPressed, - icon: Icon(Icons.add_rounded, color: context.primaryColor), - label: Padding( - padding: const EdgeInsets.only(left: 8.0), - child: Text( - 'create_shared_album_page_share_select_photos', - style: context.textTheme.titleMedium?.copyWith( - fontWeight: FontWeight.w600, - color: context.primaryColor, - ), - ).tr(), - ), - ), - ), - ); - } - - return const SliverToBoxAdapter(); - } - - buildControlButton() { - return Padding( - padding: const EdgeInsets.only(left: 12.0, top: 16, bottom: 16), - child: SizedBox( - height: 42, - child: ListView( - scrollDirection: Axis.horizontal, - children: [ - AlbumActionFilledButton( - iconData: Icons.add_photo_alternate_outlined, - onPressed: onSelectPhotosButtonPressed, - labelText: "add_photos".tr(), - ), - ], - ), - ), - ); - } - - buildSelectedImageGrid() { - if (selectedAssets.value.isNotEmpty) { - return SliverPadding( - padding: const EdgeInsets.only(top: 16), - sliver: SliverGrid( - gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 3, - crossAxisSpacing: 5.0, - mainAxisSpacing: 5, - ), - delegate: SliverChildBuilderDelegate((BuildContext context, int index) { - return GestureDetector( - onTap: onBackgroundTapped, - child: SharedAlbumThumbnailImage(asset: selectedAssets.value.elementAt(index)), - ); - }, childCount: selectedAssets.value.length), - ), - ); - } - - return const SliverToBoxAdapter(); - } - - Future createAlbum() async { - onBackgroundTapped(); - var newAlbum = await ref - .watch(albumProvider.notifier) - .createAlbum(ref.read(albumTitleProvider), selectedAssets.value); - - if (newAlbum != null) { - await ref.read(albumProvider.notifier).refreshRemoteAlbums(); - selectedAssets.value = {}; - ref.read(albumTitleProvider.notifier).clearAlbumTitle(); - ref.read(albumViewerProvider.notifier).disableEditAlbum(); - unawaited(context.replaceRoute(AlbumViewerRoute(albumId: newAlbum.id))); - } - } - - return Scaffold( - appBar: AppBar( - elevation: 0, - centerTitle: false, - backgroundColor: context.scaffoldBackgroundColor, - leading: IconButton( - onPressed: () { - selectedAssets.value = {}; - context.maybePop(); - }, - icon: const Icon(Icons.close_rounded), - ), - title: const Text('create_album').tr(), - actions: [ - TextButton( - onPressed: albumTitleController.text.isNotEmpty ? createAlbum : null, - child: Text( - 'create'.tr(), - style: TextStyle( - fontWeight: FontWeight.bold, - color: albumTitleController.text.isNotEmpty ? context.primaryColor : context.themeData.disabledColor, - ), - ), - ), - ], - ), - body: GestureDetector( - onTap: onBackgroundTapped, - child: CustomScrollView( - slivers: [ - SliverAppBar( - backgroundColor: context.scaffoldBackgroundColor, - elevation: 5, - automaticallyImplyLeading: false, - pinned: true, - floating: false, - bottom: PreferredSize( - preferredSize: const Size.fromHeight(125.0), - child: Column( - children: [ - buildTitleInputField(), - buildDescriptionInputField(), - if (selectedAssets.value.isNotEmpty) buildControlButton(), - ], - ), - ), - ), - buildTitle(), - buildSelectPhotosButton(), - buildSelectedImageGrid(), - ], - ), - ), - ); - } -} diff --git a/mobile/lib/pages/common/gallery_stacked_children.dart b/mobile/lib/pages/common/gallery_stacked_children.dart deleted file mode 100644 index 68123509ae..0000000000 --- a/mobile/lib/pages/common/gallery_stacked_children.dart +++ /dev/null @@ -1,85 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/presentation/widgets/images/remote_image_provider.dart'; -import 'package:immich_mobile/providers/asset_viewer/asset_stack.provider.dart'; -import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart'; -import 'package:immich_mobile/providers/asset_viewer/show_controls.provider.dart'; - -class GalleryStackedChildren extends HookConsumerWidget { - final ValueNotifier stackIndex; - - const GalleryStackedChildren(this.stackIndex, {super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final asset = ref.watch(currentAssetProvider); - if (asset == null) { - return const SizedBox(); - } - - final stackId = asset.stackId; - if (stackId == null) { - return const SizedBox(); - } - - final stackElements = ref.watch(assetStackStateProvider(stackId)); - final showControls = ref.watch(showControlsProvider); - - return IgnorePointer( - ignoring: !showControls, - child: AnimatedOpacity( - duration: const Duration(milliseconds: 100), - opacity: showControls ? 1.0 : 0.0, - child: SizedBox( - height: 80, - child: ListView.builder( - shrinkWrap: true, - scrollDirection: Axis.horizontal, - itemCount: stackElements.length, - padding: const EdgeInsets.only(left: 5, right: 5, bottom: 30), - itemBuilder: (context, index) { - final currentAsset = stackElements.elementAt(index); - final assetId = currentAsset.remoteId; - if (assetId == null) { - return const SizedBox(); - } - - return Padding( - key: ValueKey(currentAsset.id), - padding: const EdgeInsets.only(right: 5), - child: GestureDetector( - onTap: () { - stackIndex.value = index; - ref.read(currentAssetProvider.notifier).set(currentAsset); - }, - child: Container( - width: 60, - height: 60, - decoration: index == stackIndex.value - ? const BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.all(Radius.circular(6)), - border: Border.fromBorderSide(BorderSide(color: Colors.white, width: 2)), - ) - : const BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.all(Radius.circular(6)), - border: null, - ), - child: ClipRRect( - borderRadius: const BorderRadius.all(Radius.circular(4)), - child: Image( - fit: BoxFit.cover, - image: RemoteImageProvider.thumbnail(assetId: assetId, thumbhash: asset.thumbhash ?? ""), - ), - ), - ), - ), - ); - }, - ), - ), - ), - ); - } -} diff --git a/mobile/lib/pages/common/gallery_viewer.page.dart b/mobile/lib/pages/common/gallery_viewer.page.dart deleted file mode 100644 index 1d43bff167..0000000000 --- a/mobile/lib/pages/common/gallery_viewer.page.dart +++ /dev/null @@ -1,438 +0,0 @@ -import 'dart:async'; -import 'dart:io'; -import 'dart:math'; -import 'dart:ui' as ui; - -import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_hooks/flutter_hooks.dart' hide Store; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/extensions/scroll_extensions.dart'; -import 'package:immich_mobile/pages/common/download_panel.dart'; -import 'package:immich_mobile/pages/common/gallery_stacked_children.dart'; -import 'package:immich_mobile/pages/common/native_video_viewer.page.dart'; -import 'package:immich_mobile/providers/app_settings.provider.dart'; -import 'package:immich_mobile/providers/asset_viewer/asset_stack.provider.dart'; -import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart'; -import 'package:immich_mobile/providers/asset_viewer/is_motion_video_playing.provider.dart'; -import 'package:immich_mobile/providers/asset_viewer/show_controls.provider.dart'; -import 'package:immich_mobile/providers/cast.provider.dart'; -import 'package:immich_mobile/providers/haptic_feedback.provider.dart'; -import 'package:immich_mobile/services/app_settings.service.dart'; -import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart'; -import 'package:immich_mobile/widgets/asset_viewer/advanced_bottom_sheet.dart'; -import 'package:immich_mobile/widgets/asset_viewer/bottom_gallery_bar.dart'; -import 'package:immich_mobile/widgets/asset_viewer/detail_panel/detail_panel.dart'; -import 'package:immich_mobile/widgets/asset_viewer/gallery_app_bar.dart'; -import 'package:immich_mobile/widgets/common/immich_image.dart'; -import 'package:immich_mobile/widgets/common/immich_thumbnail.dart'; -import 'package:immich_mobile/widgets/photo_view/photo_view_gallery.dart'; -import 'package:immich_mobile/widgets/photo_view/src/photo_view_computed_scale.dart'; -import 'package:immich_mobile/widgets/photo_view/src/photo_view_scale_state.dart'; -import 'package:immich_mobile/widgets/photo_view/src/utils/photo_view_hero_attributes.dart'; - -@RoutePage() -// ignore: must_be_immutable -/// Expects [currentAssetProvider] to be set before navigating to this page -class GalleryViewerPage extends HookConsumerWidget { - final int initialIndex; - final int heroOffset; - final bool showStack; - final RenderList renderList; - - GalleryViewerPage({ - super.key, - required this.renderList, - this.initialIndex = 0, - this.heroOffset = 0, - this.showStack = false, - }) : controller = PageController(initialPage: initialIndex); - - final PageController controller; - - @override - Widget build(BuildContext context, WidgetRef ref) { - final totalAssets = useState(renderList.totalAssets); - final isZoomed = useState(false); - final stackIndex = useState(0); - final localPosition = useRef(null); - final currentIndex = useValueNotifier(initialIndex); - final loadAsset = renderList.loadAsset; - final isPlayingMotionVideo = ref.watch(isPlayingMotionVideoProvider); - final isCasting = ref.watch(castProvider.select((c) => c.isCasting)); - - final videoPlayerKeys = useRef>({}); - - GlobalKey getVideoPlayerKey(int id) { - videoPlayerKeys.value.putIfAbsent(id, () => GlobalKey()); - return videoPlayerKeys.value[id]!; - } - - Future precacheNextImage(int index) async { - if (!context.mounted) { - return; - } - - void onError(Object exception, StackTrace? stackTrace) { - // swallow error silently - log.severe('Error precaching next image: $exception, $stackTrace'); - } - - try { - if (index < totalAssets.value && index >= 0) { - final asset = loadAsset(index); - await precacheImage( - ImmichImage.imageProvider(asset: asset, width: context.width, height: context.height), - context, - onError: onError, - ); - } - } catch (e) { - // swallow error silently - log.severe('Error precaching next image: $e'); - await context.maybePop(); - } - } - - useEffect(() { - if (ref.read(showControlsProvider)) { - SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); - } else { - SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersive); - } - - // Delay this a bit so we can finish loading the page - Timer(const Duration(milliseconds: 400), () { - precacheNextImage(currentIndex.value + 1); - }); - - return null; - }, const []); - - useEffect(() { - final asset = loadAsset(currentIndex.value); - - if (asset.isRemote) { - ref.read(castProvider.notifier).loadMediaOld(asset, false); - } else { - if (isCasting) { - WidgetsBinding.instance.addPostFrameCallback((_) { - if (context.mounted) { - ref.read(castProvider.notifier).stop(); - context.scaffoldMessenger.showSnackBar( - SnackBar( - duration: const Duration(seconds: 1), - content: Text( - "local_asset_cast_failed".tr(), - style: context.textTheme.bodyLarge?.copyWith(color: context.primaryColor), - ), - ), - ); - } - }); - } - } - return null; - }, [ref.watch(castProvider).isCasting]); - - void showInfo() { - final asset = ref.read(currentAssetProvider); - if (asset == null) { - return; - } - showModalBottomSheet( - shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(15.0))), - barrierColor: Colors.transparent, - isScrollControlled: true, - showDragHandle: true, - enableDrag: true, - context: context, - useSafeArea: true, - builder: (context) { - return DraggableScrollableSheet( - minChildSize: 0.5, - maxChildSize: 1, - initialChildSize: 0.75, - expand: false, - builder: (context, scrollController) { - return Padding( - padding: EdgeInsets.only(bottom: context.viewInsets.bottom), - child: ref.watch(appSettingsServiceProvider).getSetting(AppSettingsEnum.advancedTroubleshooting) - ? AdvancedBottomSheet(assetDetail: asset, scrollController: scrollController) - : DetailPanel(asset: asset, scrollController: scrollController), - ); - }, - ); - }, - ); - } - - void handleSwipeUpDown(DragUpdateDetails details) { - const int sensitivity = 15; - const int dxThreshold = 50; - const double ratioThreshold = 3.0; - - if (isZoomed.value) { - return; - } - - // Guard [localPosition] null - if (localPosition.value == null) { - return; - } - - // Check for delta from initial down point - final d = details.localPosition - localPosition.value!; - // If the magnitude of the dx swipe is large, we probably didn't mean to go down - if (d.dx.abs() > dxThreshold) { - return; - } - - final ratio = d.dy / max(d.dx.abs(), 1); - if (d.dy > sensitivity && ratio > ratioThreshold) { - context.maybePop(); - } else if (d.dy < -sensitivity && ratio < -ratioThreshold) { - showInfo(); - } - } - - ref.listen(showControlsProvider, (_, show) { - if (show || Platform.isIOS) { - SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); - return; - } - - // This prevents the bottom bar from "dropping" while the controls are being hidden - Timer(const Duration(milliseconds: 100), () { - SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersive); - }); - }); - - PhotoViewGalleryPageOptions buildImage(Asset asset) { - return PhotoViewGalleryPageOptions( - onDragStart: (_, details, __, ___) { - localPosition.value = details.localPosition; - }, - onDragUpdate: (_, details, __) { - handleSwipeUpDown(details); - }, - onTapDown: (ctx, tapDownDetails, _) { - final tapToNavigate = ref.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.tapToNavigate); - if (!tapToNavigate) { - ref.read(showControlsProvider.notifier).toggle(); - return; - } - - double tapX = tapDownDetails.globalPosition.dx; - double screenWidth = ctx.width; - - // We want to change images if the user taps in the leftmost or - // rightmost quarter of the screen - bool tappedLeftSide = tapX < screenWidth / 4; - bool tappedRightSide = tapX > screenWidth * (3 / 4); - - int? currentPage = controller.page?.toInt(); - int maxPage = renderList.totalAssets - 1; - - if (tappedLeftSide && currentPage != null) { - // Nested if because we don't want to fallback to show/hide controls - if (currentPage != 0) { - controller.jumpToPage(currentPage - 1); - } - } else if (tappedRightSide && currentPage != null) { - // Nested if because we don't want to fallback to show/hide controls - if (currentPage != maxPage) { - controller.jumpToPage(currentPage + 1); - } - } else { - ref.read(showControlsProvider.notifier).toggle(); - } - }, - onLongPressStart: asset.isMotionPhoto - ? (_, __, ___) { - ref.read(isPlayingMotionVideoProvider.notifier).playing = true; - } - : null, - imageProvider: ImmichImage.imageProvider(asset: asset), - heroAttributes: _getHeroAttributes(asset), - filterQuality: FilterQuality.high, - tightMode: true, - initialScale: PhotoViewComputedScale.contained * 0.99, - minScale: PhotoViewComputedScale.contained * 0.99, - errorBuilder: (context, error, stackTrace) => ImmichImage(asset, fit: BoxFit.contain), - ); - } - - PhotoViewGalleryPageOptions buildVideo(BuildContext context, Asset asset) { - return PhotoViewGalleryPageOptions.customChild( - onDragStart: (_, details, __, ___) => localPosition.value = details.localPosition, - onDragUpdate: (_, details, __) => handleSwipeUpDown(details), - heroAttributes: _getHeroAttributes(asset), - filterQuality: FilterQuality.high, - initialScale: PhotoViewComputedScale.contained * 0.99, - maxScale: 1.0, - minScale: PhotoViewComputedScale.contained * 0.99, - basePosition: Alignment.center, - child: SizedBox( - width: context.width, - height: context.height, - child: NativeVideoViewerPage( - key: getVideoPlayerKey(asset.id), - asset: asset, - image: Image( - key: ValueKey(asset), - image: ImmichImage.imageProvider(asset: asset, width: context.width, height: context.height), - fit: BoxFit.contain, - height: context.height, - width: context.width, - alignment: Alignment.center, - ), - ), - ), - ); - } - - PhotoViewGalleryPageOptions buildAsset(BuildContext context, int index) { - var newAsset = loadAsset(index); - - final stackId = newAsset.stackId; - if (stackId != null && currentIndex.value == index) { - final stackElements = ref.read(assetStackStateProvider(newAsset.stackId!)); - if (stackIndex.value < stackElements.length) { - newAsset = stackElements.elementAt(stackIndex.value); - } - } - - if (newAsset.isImage && !isPlayingMotionVideo) { - return buildImage(newAsset); - } - return buildVideo(context, newAsset); - } - - return PopScope( - // Change immersive mode back to normal "edgeToEdge" mode - onPopInvokedWithResult: (didPop, _) => SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge), - child: Scaffold( - backgroundColor: Colors.black, - body: Stack( - children: [ - PhotoViewGallery.builder( - key: const ValueKey('gallery'), - scaleStateChangedCallback: (state) { - final asset = ref.read(currentAssetProvider); - if (asset == null) { - return; - } - - if (asset.isImage && !ref.read(isPlayingMotionVideoProvider)) { - isZoomed.value = state != PhotoViewScaleState.initial; - ref.read(showControlsProvider.notifier).show = !isZoomed.value; - } - }, - gaplessPlayback: true, - loadingBuilder: (context, event, index) { - final asset = loadAsset(index); - return ClipRect( - child: Stack( - fit: StackFit.expand, - children: [ - BackdropFilter(filter: ui.ImageFilter.blur(sigmaX: 10, sigmaY: 10)), - ImmichThumbnail(key: ValueKey(asset), asset: asset, fit: BoxFit.contain), - ], - ), - ); - }, - pageController: controller, - scrollPhysics: isZoomed.value - ? const NeverScrollableScrollPhysics() // Don't allow paging while scrolled in - : (Platform.isIOS - ? const FastScrollPhysics() // Use bouncing physics for iOS - : const FastClampingScrollPhysics() // Use heavy physics for Android - ), - itemCount: totalAssets.value, - scrollDirection: Axis.horizontal, - onPageChanged: (value, _) { - final next = currentIndex.value < value ? value + 1 : value - 1; - - ref.read(hapticFeedbackProvider.notifier).selectionClick(); - - final newAsset = loadAsset(value); - - currentIndex.value = value; - stackIndex.value = 0; - - ref.read(currentAssetProvider.notifier).set(newAsset); - - // Wait for page change animation to finish, then precache the next image - Timer(const Duration(milliseconds: 400), () { - precacheNextImage(next); - }); - - context.scaffoldMessenger.hideCurrentSnackBar(); - - // send image to casting if the server has it - if (newAsset.isRemote) { - ref.read(castProvider.notifier).loadMediaOld(newAsset, false); - } else { - context.scaffoldMessenger.clearSnackBars(); - - if (isCasting) { - ref.read(castProvider.notifier).stop(); - context.scaffoldMessenger.showSnackBar( - SnackBar( - duration: const Duration(seconds: 2), - content: Text( - "local_asset_cast_failed".tr(), - style: context.textTheme.bodyLarge?.copyWith(color: context.primaryColor), - ), - ), - ); - } - } - }, - builder: buildAsset, - ), - Positioned( - top: 0, - left: 0, - right: 0, - child: GalleryAppBar(key: const ValueKey('app-bar'), showInfo: showInfo), - ), - Positioned( - bottom: 0, - left: 0, - right: 0, - child: Column( - children: [ - GalleryStackedChildren(stackIndex), - BottomGalleryBar( - key: const ValueKey('bottom-bar'), - renderList: renderList, - totalAssets: totalAssets, - controller: controller, - showStack: showStack, - stackIndex: stackIndex, - assetIndex: currentIndex, - ), - ], - ), - ), - const DownloadPanel(), - ], - ), - ), - ); - } - - @pragma('vm:prefer-inline') - PhotoViewHeroAttributes _getHeroAttributes(Asset asset) { - return PhotoViewHeroAttributes( - tag: asset.isInDb ? asset.id + heroOffset : '${asset.remoteId}-$heroOffset', - transitionOnUserGestures: true, - ); - } -} diff --git a/mobile/lib/pages/common/native_video_viewer.page.dart b/mobile/lib/pages/common/native_video_viewer.page.dart deleted file mode 100644 index b1eed29c5c..0000000000 --- a/mobile/lib/pages/common/native_video_viewer.page.dart +++ /dev/null @@ -1,282 +0,0 @@ -import 'dart:async'; -import 'dart:io'; - -import 'package:auto_route/auto_route.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart' hide Store; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/domain/models/store.model.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/entities/store.entity.dart'; -import 'package:immich_mobile/providers/app_settings.provider.dart'; -import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart'; -import 'package:immich_mobile/providers/asset_viewer/is_motion_video_playing.provider.dart'; -import 'package:immich_mobile/providers/asset_viewer/video_player_provider.dart'; -import 'package:immich_mobile/providers/cast.provider.dart'; -import 'package:immich_mobile/services/api.service.dart'; -import 'package:immich_mobile/services/app_settings.service.dart'; -import 'package:immich_mobile/services/asset.service.dart'; -import 'package:immich_mobile/widgets/asset_viewer/custom_video_player_controls.dart'; -import 'package:logging/logging.dart'; -import 'package:native_video_player/native_video_player.dart'; - -@RoutePage() -class NativeVideoViewerPage extends HookConsumerWidget { - static final log = Logger('NativeVideoViewer'); - final Asset asset; - final bool showControls; - final int playbackDelayFactor; - final Widget image; - - const NativeVideoViewerPage({ - super.key, - required this.asset, - required this.image, - this.showControls = true, - this.playbackDelayFactor = 1, - }); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final videoId = asset.id.toString(); - final controller = useState(null); - final shouldPlayOnForeground = useRef(true); - - final currentAsset = useState(ref.read(currentAssetProvider)); - final isCurrent = currentAsset.value == asset; - - // Used to show the placeholder during hero animations for remote videos to avoid a stutter - final isVisible = useState(Platform.isIOS && asset.isLocal); - - final isCasting = ref.watch(castProvider.select((c) => c.isCasting)); - - final isVideoReady = useState(false); - - Future createSource() async { - if (!context.mounted) { - return null; - } - - try { - final local = asset.local; - if (local != null && asset.livePhotoVideoId == null) { - final file = await local.file; - if (file == null) { - throw Exception('No file found for the video'); - } - - final source = await VideoSource.init(path: file.path, type: VideoSourceType.file); - return source; - } - - // Use a network URL for the video player controller - final serverEndpoint = Store.get(StoreKey.serverEndpoint); - final isOriginalVideo = ref - .read(appSettingsServiceProvider) - .getSetting(AppSettingsEnum.loadOriginalVideo); - final String postfixUrl = isOriginalVideo ? 'original' : 'video/playback'; - final String videoUrl = asset.livePhotoVideoId != null - ? '$serverEndpoint/assets/${asset.livePhotoVideoId}/$postfixUrl' - : '$serverEndpoint/assets/${asset.remoteId}/$postfixUrl'; - - final source = await VideoSource.init( - path: videoUrl, - type: VideoSourceType.network, - headers: ApiService.getRequestHeaders(), - ); - return source; - } catch (error) { - log.severe('Error creating video source for asset ${asset.fileName}: $error'); - return null; - } - } - - final videoSource = useMemoized>(() => createSource()); - final aspectRatio = useState(asset.aspectRatio); - useMemoized(() async { - if (!context.mounted || aspectRatio.value != null) { - return null; - } - - try { - aspectRatio.value = await ref.read(assetServiceProvider).getAspectRatio(asset); - } catch (error) { - log.severe('Error getting aspect ratio for asset ${asset.fileName}: $error'); - } - }); - - void onPlaybackReady() async { - final videoController = controller.value; - if (videoController == null || !isCurrent || !context.mounted) { - return; - } - - final notifier = ref.read(videoPlayerProvider(videoId).notifier); - notifier.onNativePlaybackReady(); - - isVideoReady.value = true; - - try { - final autoPlayVideo = ref.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.autoPlayVideo); - if (autoPlayVideo) { - await notifier.play(); - } - await notifier.setVolume(1); - } catch (error) { - log.severe('Error playing video: $error'); - } - } - - void onPlaybackStatusChanged() { - if (!context.mounted) return; - ref.read(videoPlayerProvider(videoId).notifier).onNativeStatusChanged(); - } - - void onPlaybackPositionChanged() { - if (!context.mounted) return; - ref.read(videoPlayerProvider(videoId).notifier).onNativePositionChanged(); - } - - void onPlaybackEnded() { - if (!context.mounted) return; - - ref.read(videoPlayerProvider(videoId).notifier).onNativePlaybackEnded(); - - final videoController = controller.value; - if (videoController?.playbackInfo?.status == PlaybackStatus.stopped && - !ref.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.loopVideo)) { - ref.read(isPlayingMotionVideoProvider.notifier).playing = false; - } - } - - void removeListeners(NativeVideoPlayerController controller) { - controller.onPlaybackPositionChanged.removeListener(onPlaybackPositionChanged); - controller.onPlaybackStatusChanged.removeListener(onPlaybackStatusChanged); - controller.onPlaybackReady.removeListener(onPlaybackReady); - controller.onPlaybackEnded.removeListener(onPlaybackEnded); - } - - void initController(NativeVideoPlayerController nc) async { - if (controller.value != null || !context.mounted) { - return; - } - - final source = await videoSource; - if (source == null) { - return; - } - - final notifier = ref.read(videoPlayerProvider(videoId).notifier); - notifier.attachController(nc); - - nc.onPlaybackPositionChanged.addListener(onPlaybackPositionChanged); - nc.onPlaybackStatusChanged.addListener(onPlaybackStatusChanged); - nc.onPlaybackReady.addListener(onPlaybackReady); - nc.onPlaybackEnded.addListener(onPlaybackEnded); - - unawaited( - nc.loadVideoSource(source).catchError((error) { - log.severe('Error loading video source: $error'); - }), - ); - final loopVideo = ref.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.loopVideo); - await notifier.setLoop(loopVideo); - - controller.value = nc; - } - - ref.listen(currentAssetProvider, (_, value) { - final playerController = controller.value; - if (playerController != null && value != asset) { - removeListeners(playerController); - } - - final curAsset = currentAsset.value; - if (curAsset == asset) { - return; - } - - final imageToVideo = curAsset != null && !curAsset.isVideo; - - // No need to delay video playback when swiping from an image to a video - if (imageToVideo && Platform.isIOS) { - currentAsset.value = value; - onPlaybackReady(); - return; - } - - // Delay the video playback to avoid a stutter in the swipe animation - Timer( - Platform.isIOS - ? Duration(milliseconds: 300 * playbackDelayFactor) - : imageToVideo - ? Duration(milliseconds: 200 * playbackDelayFactor) - : Duration(milliseconds: 400 * playbackDelayFactor), - () { - if (!context.mounted) { - return; - } - - currentAsset.value = value; - if (currentAsset.value == asset) { - onPlaybackReady(); - } - }, - ); - }); - - useEffect(() { - // If opening a remote video from a hero animation, delay visibility to avoid a stutter - final timer = isVisible.value ? null : Timer(const Duration(milliseconds: 300), () => isVisible.value = true); - - return () { - timer?.cancel(); - final playerController = controller.value; - if (playerController == null) { - return; - } - removeListeners(playerController); - playerController.stop().catchError((error) { - log.fine('Error stopping video: $error'); - }); - }; - }, const []); - - useOnAppLifecycleStateChange((_, state) async { - final notifier = ref.read(videoPlayerProvider(videoId).notifier); - if (state == AppLifecycleState.resumed && shouldPlayOnForeground.value) { - await notifier.play(); - } else if (state == AppLifecycleState.paused) { - final videoPlaying = await controller.value?.isPlaying(); - if (videoPlaying ?? true) { - shouldPlayOnForeground.value = true; - await notifier.pause(); - } else { - shouldPlayOnForeground.value = false; - } - } - }); - - return Stack( - children: [ - // This remains under the video to avoid flickering - // For motion videos, this is the image portion of the asset - if (!isVideoReady.value || asset.isMotionPhoto) Center(key: ValueKey(asset.id), child: image), - if (aspectRatio.value != null && !isCasting) - Visibility.maintain( - key: ValueKey(asset), - visible: isVisible.value, - child: Center( - key: ValueKey(asset), - child: AspectRatio( - key: ValueKey(asset), - aspectRatio: aspectRatio.value!, - child: isCurrent ? NativeVideoPlayerView(key: ValueKey(asset), onViewReady: initController) : null, - ), - ), - ), - if (showControls) Center(child: CustomVideoPlayerControls(videoId: videoId)), - ], - ); - } -} diff --git a/mobile/lib/pages/common/settings.page.dart b/mobile/lib/pages/common/settings.page.dart index e8f5eb2ee2..65970ee294 100644 --- a/mobile/lib/pages/common/settings.page.dart +++ b/mobile/lib/pages/common/settings.page.dart @@ -2,14 +2,11 @@ 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' hide Store; -import 'package:immich_mobile/domain/models/store.model.dart'; -import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/widgets/settings/advanced_settings.dart'; import 'package:immich_mobile/widgets/settings/asset_list_settings/asset_list_settings.dart'; import 'package:immich_mobile/widgets/settings/asset_viewer_settings/asset_viewer_settings.dart'; -import 'package:immich_mobile/widgets/settings/backup_settings/backup_settings.dart'; import 'package:immich_mobile/widgets/settings/backup_settings/drift_backup_settings.dart'; import 'package:immich_mobile/widgets/settings/beta_sync_settings/sync_status_and_actions.dart'; import 'package:immich_mobile/widgets/settings/free_up_space_settings.dart'; @@ -38,8 +35,7 @@ enum SettingSection { Widget get widget => switch (this) { SettingSection.advanced => const AdvancedSettings(), SettingSection.assetViewer => const AssetViewerSettings(), - SettingSection.backup => - Store.tryGet(StoreKey.betaTimeline) ?? false ? const DriftBackupSettings() : const BackupSettings(), + SettingSection.backup => const DriftBackupSettings(), SettingSection.freeUpSpace => const FreeUpSpaceSettings(), SettingSection.languages => const LanguageSettings(), SettingSection.networking => const NetworkingSettings(), @@ -74,13 +70,12 @@ class _MobileLayout extends StatelessWidget { .expand( (setting) => setting == SettingSection.beta ? [ - if (Store.isBetaTimelineEnabled) - SettingsCard( - icon: Icons.sync_outlined, - title: 'sync_status'.tr(), - subtitle: 'sync_status_subtitle'.tr(), - settingRoute: const SyncStatusRoute(), - ), + SettingsCard( + icon: Icons.sync_outlined, + title: 'sync_status'.tr(), + subtitle: 'sync_status_subtitle'.tr(), + settingRoute: const SyncStatusRoute(), + ), ] : [ SettingsCard( diff --git a/mobile/lib/pages/common/splash_screen.page.dart b/mobile/lib/pages/common/splash_screen.page.dart index 37c6b95806..725f7f9e85 100644 --- a/mobile/lib/pages/common/splash_screen.page.dart +++ b/mobile/lib/pages/common/splash_screen.page.dart @@ -12,13 +12,9 @@ import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/generated/codegen_loader.g.dart'; import 'package:immich_mobile/generated/translations.g.dart'; -import 'package:path/path.dart' as path; -import 'package:path_provider/path_provider.dart'; import 'package:immich_mobile/providers/auth.provider.dart'; import 'package:immich_mobile/providers/background_sync.provider.dart'; -import 'package:immich_mobile/providers/backup/backup.provider.dart'; import 'package:immich_mobile/providers/backup/drift_backup.provider.dart'; -import 'package:immich_mobile/providers/gallery_permission.provider.dart'; import 'package:immich_mobile/providers/server_info.provider.dart'; import 'package:immich_mobile/providers/websocket.provider.dart'; import 'package:immich_mobile/routing/router.dart'; @@ -27,6 +23,8 @@ import 'package:immich_mobile/theme/theme_data.dart'; import 'package:immich_mobile/widgets/common/immich_logo.dart'; import 'package:immich_mobile/widgets/common/immich_title_text.dart'; import 'package:logging/logging.dart'; +import 'package:path/path.dart' as path; +import 'package:path_provider/path_provider.dart'; import 'package:url_launcher/url_launcher.dart' show launchUrl, LaunchMode; class BootstrapErrorWidget extends StatelessWidget { @@ -323,29 +321,27 @@ class SplashScreenPageState extends ConsumerState { wsProvider.connect(); unawaited(infoProvider.getServerInfo()); - if (Store.isBetaTimelineEnabled) { - bool syncSuccess = false; + bool syncSuccess = false; + await Future.wait([ + backgroundManager.syncLocal(full: true), + backgroundManager.syncRemote().then((success) => syncSuccess = success), + ]); + + if (syncSuccess) { await Future.wait([ - backgroundManager.syncLocal(full: true), - backgroundManager.syncRemote().then((success) => syncSuccess = success), + backgroundManager.hashAssets().then((_) { + _resumeBackup(backupProvider); + }), + _resumeBackup(backupProvider), + // TODO: Bring back when the soft freeze issue is addressed + // backgroundManager.syncCloudIds(), ]); + } else { + await backgroundManager.hashAssets(); + } - if (syncSuccess) { - await Future.wait([ - backgroundManager.hashAssets().then((_) { - _resumeBackup(backupProvider); - }), - _resumeBackup(backupProvider), - // TODO: Bring back when the soft freeze issue is addressed - // backgroundManager.syncCloudIds(), - ]); - } else { - await backgroundManager.hashAssets(); - } - - if (Store.get(StoreKey.syncAlbums, false)) { - await backgroundManager.syncLinkedAlbum(); - } + if (Store.get(StoreKey.syncAlbums, false)) { + await backgroundManager.syncLinkedAlbum(); } } catch (e) { log.severe('Failed establishing connection to the server: $e'); @@ -368,58 +364,7 @@ class SplashScreenPageState extends ConsumerState { // clean install - change the default of the flag // current install not using beta timeline if (context.router.current.name == SplashScreenRoute.name) { - final needBetaMigration = Store.get(StoreKey.needBetaMigration, false); - if (needBetaMigration) { - bool migrate = - (await showDialog( - context: context, - builder: (ctx) => AlertDialog( - title: const Text("New Timeline Experience"), - content: const Text( - "The old timeline has been deprecated and will be removed in an upcoming release. Would you like to switch to the new timeline now?", - ), - actions: [ - TextButton(onPressed: () => Navigator.of(ctx).pop(false), child: const Text("No")), - ElevatedButton(onPressed: () => Navigator.of(ctx).pop(true), child: const Text("Yes")), - ], - ), - )) ?? - false; - if (migrate != true) { - migrate = - (await showDialog( - context: context, - builder: (ctx) => AlertDialog( - title: const Text("Are you sure?"), - content: const Text( - "If you choose to remain on the old timeline, you will be automatically migrated to the new timeline in an upcoming release. Would you like to switch now?", - ), - actions: [ - TextButton(onPressed: () => Navigator.of(ctx).pop(false), child: const Text("No")), - ElevatedButton(onPressed: () => Navigator.of(ctx).pop(true), child: const Text("Yes")), - ], - ), - )) ?? - false; - } - await Store.put(StoreKey.needBetaMigration, false); - if (migrate) { - unawaited(context.router.replaceAll([ChangeExperienceRoute(switchingToBeta: true)])); - return; - } - } - - unawaited(context.replaceRoute(Store.isBetaTimelineEnabled ? const TabShellRoute() : const TabControllerRoute())); - } - - if (Store.isBetaTimelineEnabled) { - return; - } - - final hasPermission = await ref.read(galleryPermissionNotifier.notifier).hasPermission; - if (hasPermission) { - // Resume backup (if enable) then navigate - await ref.watch(backupProvider.notifier).resumeBackup(); + unawaited(context.replaceRoute(const TabShellRoute())); } } diff --git a/mobile/lib/pages/common/tab_controller.page.dart b/mobile/lib/pages/common/tab_controller.page.dart deleted file mode 100644 index ef637ba1c8..0000000000 --- a/mobile/lib/pages/common/tab_controller.page.dart +++ /dev/null @@ -1,142 +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/providers/album/album.provider.dart'; -import 'package:immich_mobile/providers/asset.provider.dart'; -import 'package:immich_mobile/providers/asset_viewer/scroll_notifier.provider.dart'; -import 'package:immich_mobile/providers/haptic_feedback.provider.dart'; -import 'package:immich_mobile/providers/multiselect.provider.dart'; -import 'package:immich_mobile/providers/search/search_input_focus.provider.dart'; -import 'package:immich_mobile/providers/tab.provider.dart'; -import 'package:immich_mobile/routing/router.dart'; - -@RoutePage() -class TabControllerPage extends HookConsumerWidget { - const TabControllerPage({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final isRefreshingAssets = ref.watch(assetProvider); - final isRefreshingRemoteAlbums = ref.watch(isRefreshingRemoteAlbumProvider); - final isScreenLandscape = MediaQuery.orientationOf(context) == Orientation.landscape; - - Widget buildIcon({required Widget icon, required bool isProcessing}) { - if (!isProcessing) return icon; - return Stack( - alignment: Alignment.center, - clipBehavior: Clip.none, - children: [ - icon, - Positioned( - right: -18, - child: SizedBox( - height: 20, - width: 20, - child: CircularProgressIndicator( - strokeWidth: 2, - valueColor: AlwaysStoppedAnimation(context.primaryColor), - ), - ), - ), - ], - ); - } - - void onNavigationSelected(TabsRouter router, int index) { - // On Photos page menu tapped - if (router.activeIndex == 0 && index == 0) { - scrollToTopNotifierProvider.scrollToTop(); - } - - // On Search page tapped - if (router.activeIndex == 1 && index == 1) { - ref.read(searchInputFocusProvider).requestFocus(); - } - - ref.read(hapticFeedbackProvider.notifier).selectionClick(); - router.setActiveIndex(index); - ref.read(tabProvider.notifier).state = TabEnum.values[index]; - } - - final navigationDestinations = [ - NavigationDestination( - label: 'photos'.tr(), - icon: const Icon(Icons.photo_library_outlined), - selectedIcon: buildIcon( - isProcessing: isRefreshingAssets, - icon: Icon(Icons.photo_library, color: context.primaryColor), - ), - ), - NavigationDestination( - label: 'search'.tr(), - icon: const Icon(Icons.search_rounded), - selectedIcon: Icon(Icons.search, color: context.primaryColor), - ), - NavigationDestination( - label: 'albums'.tr(), - icon: const Icon(Icons.photo_album_outlined), - selectedIcon: buildIcon( - isProcessing: isRefreshingRemoteAlbums, - icon: Icon(Icons.photo_album_rounded, 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), - ), - ), - ]; - - Widget bottomNavigationBar(TabsRouter tabsRouter) { - return NavigationBar( - selectedIndex: tabsRouter.activeIndex, - onDestinationSelected: (index) => onNavigationSelected(tabsRouter, index), - destinations: navigationDestinations, - ); - } - - Widget navigationRail(TabsRouter tabsRouter) { - return NavigationRail( - destinations: navigationDestinations - .map((e) => NavigationRailDestination(icon: e.icon, label: Text(e.label), selectedIcon: e.selectedIcon)) - .toList(), - onDestinationSelected: (index) => onNavigationSelected(tabsRouter, index), - selectedIndex: tabsRouter.activeIndex, - labelType: NavigationRailLabelType.all, - groupAlignment: 0.0, - ); - } - - final multiselectEnabled = ref.watch(multiselectProvider); - return AutoTabsRouter( - routes: [const PhotosRoute(), SearchRoute(), const AlbumsRoute(), const LibraryRoute()], - duration: const Duration(milliseconds: 600), - transitionBuilder: (context, child, animation) => FadeTransition(opacity: animation, child: child), - builder: (context, child) { - final tabsRouter = AutoTabsRouter.of(context); - return PopScope( - canPop: tabsRouter.activeIndex == 0, - onPopInvokedWithResult: (didPop, _) => !didPop ? tabsRouter.setActiveIndex(0) : null, - child: Scaffold( - resizeToAvoidBottomInset: false, - body: isScreenLandscape - ? Row( - children: [ - navigationRail(tabsRouter), - const VerticalDivider(), - Expanded(child: child), - ], - ) - : child, - bottomNavigationBar: multiselectEnabled || isScreenLandscape ? null : bottomNavigationBar(tabsRouter), - ), - ); - }, - ); - } -} diff --git a/mobile/lib/pages/editing/crop.page.dart b/mobile/lib/pages/editing/crop.page.dart deleted file mode 100644 index a6a66c1358..0000000000 --- a/mobile/lib/pages/editing/crop.page.dart +++ /dev/null @@ -1,177 +0,0 @@ -import 'dart:async'; - -import 'package:auto_route/auto_route.dart'; -import 'package:crop_image/crop_image.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/pages/editing/edit.page.dart'; -import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/utils/hooks/crop_controller_hook.dart'; - -/// A widget for cropping an image. -/// This widget uses [HookWidget] to manage its lifecycle and state. It allows -/// users to crop an image and then navigate to the [EditImagePage] with the -/// cropped image. - -@RoutePage() -class CropImagePage extends HookWidget { - final Image image; - final Asset asset; - const CropImagePage({super.key, required this.image, required this.asset}); - - @override - Widget build(BuildContext context) { - final cropController = useCropController(); - final aspectRatio = useState(null); - - return Scaffold( - appBar: AppBar( - backgroundColor: context.scaffoldBackgroundColor, - title: Text("crop".tr()), - leading: CloseButton(color: context.primaryColor), - actions: [ - IconButton( - icon: Icon(Icons.done_rounded, color: context.primaryColor, size: 24), - onPressed: () async { - final croppedImage = await cropController.croppedImage(); - unawaited(context.pushRoute(EditImageRoute(asset: asset, image: croppedImage, isEdited: true))); - }, - ), - ], - ), - backgroundColor: context.scaffoldBackgroundColor, - body: SafeArea( - child: LayoutBuilder( - builder: (BuildContext context, BoxConstraints constraints) { - return Column( - children: [ - Container( - padding: const EdgeInsets.only(top: 20), - width: constraints.maxWidth * 0.9, - height: constraints.maxHeight * 0.6, - child: CropImage(controller: cropController, image: image, gridColor: Colors.white), - ), - Expanded( - child: Container( - width: double.infinity, - decoration: BoxDecoration( - color: context.scaffoldBackgroundColor, - borderRadius: const BorderRadius.only( - topLeft: Radius.circular(20), - topRight: Radius.circular(20), - ), - ), - child: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Padding( - padding: const EdgeInsets.only(left: 20, right: 20, bottom: 10), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - IconButton( - icon: Icon(Icons.rotate_left, color: context.themeData.iconTheme.color), - onPressed: () { - cropController.rotateLeft(); - }, - ), - IconButton( - icon: Icon(Icons.rotate_right, color: context.themeData.iconTheme.color), - onPressed: () { - cropController.rotateRight(); - }, - ), - ], - ), - ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - _AspectRatioButton( - cropController: cropController, - aspectRatio: aspectRatio, - ratio: null, - label: 'Free', - ), - _AspectRatioButton( - cropController: cropController, - aspectRatio: aspectRatio, - ratio: 1.0, - label: '1:1', - ), - _AspectRatioButton( - cropController: cropController, - aspectRatio: aspectRatio, - ratio: 16.0 / 9.0, - label: '16:9', - ), - _AspectRatioButton( - cropController: cropController, - aspectRatio: aspectRatio, - ratio: 3.0 / 2.0, - label: '3:2', - ), - _AspectRatioButton( - cropController: cropController, - aspectRatio: aspectRatio, - ratio: 7.0 / 5.0, - label: '7:5', - ), - ], - ), - ], - ), - ), - ), - ), - ], - ); - }, - ), - ), - ); - } -} - -class _AspectRatioButton extends StatelessWidget { - final CropController cropController; - final ValueNotifier aspectRatio; - final double? ratio; - final String label; - - const _AspectRatioButton({ - required this.cropController, - required this.aspectRatio, - required this.ratio, - required this.label, - }); - - @override - Widget build(BuildContext context) { - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - IconButton( - icon: Icon(switch (label) { - 'Free' => Icons.crop_free_rounded, - '1:1' => Icons.crop_square_rounded, - '16:9' => Icons.crop_16_9_rounded, - '3:2' => Icons.crop_3_2_rounded, - '7:5' => Icons.crop_7_5_rounded, - _ => Icons.crop_free_rounded, - }, color: aspectRatio.value == ratio ? context.primaryColor : context.themeData.iconTheme.color), - onPressed: () { - cropController.crop = const Rect.fromLTRB(0.1, 0.1, 0.9, 0.9); - aspectRatio.value = ratio; - cropController.aspectRatio = ratio; - }, - ), - Text(label, style: context.textTheme.displayMedium), - ], - ); - } -} diff --git a/mobile/lib/pages/editing/edit.page.dart b/mobile/lib/pages/editing/edit.page.dart deleted file mode 100644 index 2889785d0b..0000000000 --- a/mobile/lib/pages/editing/edit.page.dart +++ /dev/null @@ -1,131 +0,0 @@ -import 'dart:typed_data'; - -import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:fluttertoast/fluttertoast.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/providers/album/album.provider.dart'; -import 'package:immich_mobile/repositories/file_media.repository.dart'; -import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/utils/image_converter.dart'; -import 'package:immich_mobile/widgets/common/immich_toast.dart'; -import 'package:path/path.dart' as p; - -/// A stateless widget that provides functionality for editing an image. -/// -/// This widget allows users to edit an image provided either as an [Asset] or -/// directly as an [Image]. It ensures that exactly one of these is provided. -/// -/// It also includes a conversion method to convert an [Image] to a [Uint8List] to save the image on the user's phone -/// They automatically navigate to the [HomePage] with the edited image saved and they eventually get backed up to the server. -@immutable -@RoutePage() -class EditImagePage extends ConsumerWidget { - final Asset asset; - final Image image; - final bool isEdited; - - const EditImagePage({super.key, required this.asset, required this.image, required this.isEdited}); - - Future _saveEditedImage(BuildContext context, Asset asset, Image image, WidgetRef ref) async { - try { - final Uint8List imageData = await imageToUint8List(image); - await ref - .read(fileMediaRepositoryProvider) - .saveImage(imageData, title: "${p.withoutExtension(asset.fileName)}_edited.jpg"); - await ref.read(albumProvider.notifier).refreshDeviceAlbums(); - context.navigator.popUntil((route) => route.isFirst); - ImmichToast.show(durationInSecond: 3, context: context, msg: 'Image Saved!', gravity: ToastGravity.CENTER); - } catch (e) { - ImmichToast.show( - durationInSecond: 6, - context: context, - msg: "error_saving_image".tr(namedArgs: {'error': e.toString()}), - gravity: ToastGravity.CENTER, - ); - } - } - - @override - Widget build(BuildContext context, WidgetRef ref) { - return Scaffold( - appBar: AppBar( - title: Text("edit".tr()), - backgroundColor: context.scaffoldBackgroundColor, - leading: IconButton( - icon: Icon(Icons.close_rounded, color: context.primaryColor, size: 24), - onPressed: () => context.navigator.popUntil((route) => route.isFirst), - ), - actions: [ - TextButton( - onPressed: isEdited ? () => _saveEditedImage(context, asset, image, ref) : null, - child: Text("save_to_gallery".tr(), style: TextStyle(color: isEdited ? context.primaryColor : Colors.grey)), - ), - ], - ), - backgroundColor: context.scaffoldBackgroundColor, - body: Center( - child: ConstrainedBox( - constraints: BoxConstraints(maxHeight: context.height * 0.7, maxWidth: context.width * 0.9), - child: Container( - decoration: BoxDecoration( - borderRadius: const BorderRadius.all(Radius.circular(7)), - boxShadow: [ - BoxShadow( - color: Colors.black.withValues(alpha: 0.2), - spreadRadius: 2, - blurRadius: 10, - offset: const Offset(0, 3), - ), - ], - ), - child: ClipRRect( - borderRadius: const BorderRadius.all(Radius.circular(7)), - child: Image(image: image.image, fit: BoxFit.contain), - ), - ), - ), - ), - bottomNavigationBar: Container( - height: 70, - margin: const EdgeInsets.only(bottom: 60, right: 10, left: 10, top: 10), - decoration: BoxDecoration( - color: context.scaffoldBackgroundColor, - borderRadius: const BorderRadius.all(Radius.circular(30)), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - IconButton( - icon: Icon(Icons.crop_rotate_rounded, color: context.themeData.iconTheme.color, size: 25), - onPressed: () { - context.pushRoute(CropImageRoute(asset: asset, image: image)); - }, - ), - Text("crop".tr(), style: context.textTheme.displayMedium), - ], - ), - Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - IconButton( - icon: Icon(Icons.filter, color: context.themeData.iconTheme.color, size: 25), - onPressed: () { - context.pushRoute(FilterImageRoute(asset: asset, image: image)); - }, - ), - Text("filter".tr(), style: context.textTheme.displayMedium), - ], - ), - ], - ), - ), - ); - } -} diff --git a/mobile/lib/pages/editing/filter.page.dart b/mobile/lib/pages/editing/filter.page.dart deleted file mode 100644 index f8b144bb96..0000000000 --- a/mobile/lib/pages/editing/filter.page.dart +++ /dev/null @@ -1,159 +0,0 @@ -import 'dart:async'; -import 'dart:ui' as ui; - -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:immich_mobile/constants/filters.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/routing/router.dart'; - -/// A widget for filtering an image. -/// This widget uses [HookWidget] to manage its lifecycle and state. It allows -/// users to add filters to an image and then navigate to the [EditImagePage] with the -/// final composition.' -@RoutePage() -class FilterImagePage extends HookWidget { - final Image image; - final Asset asset; - - const FilterImagePage({super.key, required this.image, required this.asset}); - - @override - Widget build(BuildContext context) { - final colorFilter = useState(filters[0]); - final selectedFilterIndex = useState(0); - - Future createFilteredImage(ui.Image inputImage, ColorFilter filter) { - final completer = Completer(); - final size = Size(inputImage.width.toDouble(), inputImage.height.toDouble()); - final recorder = ui.PictureRecorder(); - final canvas = Canvas(recorder); - - final paint = Paint()..colorFilter = filter; - canvas.drawImage(inputImage, Offset.zero, paint); - - recorder.endRecording().toImage(size.width.round(), size.height.round()).then((image) { - completer.complete(image); - }); - - return completer.future; - } - - void applyFilter(ColorFilter filter, int index) { - colorFilter.value = filter; - selectedFilterIndex.value = index; - } - - Future applyFilterAndConvert(ColorFilter filter) async { - final completer = Completer(); - image.image - .resolve(ImageConfiguration.empty) - .addListener( - ImageStreamListener((ImageInfo info, bool _) { - completer.complete(info.image); - }), - ); - final uiImage = await completer.future; - - final filteredUiImage = await createFilteredImage(uiImage, filter); - final byteData = await filteredUiImage.toByteData(format: ui.ImageByteFormat.png); - final pngBytes = byteData!.buffer.asUint8List(); - - return Image.memory(pngBytes, fit: BoxFit.contain); - } - - return Scaffold( - appBar: AppBar( - backgroundColor: context.scaffoldBackgroundColor, - title: Text("filter".tr()), - leading: CloseButton(color: context.primaryColor), - actions: [ - IconButton( - icon: Icon(Icons.done_rounded, color: context.primaryColor, size: 24), - onPressed: () async { - final filteredImage = await applyFilterAndConvert(colorFilter.value); - unawaited(context.pushRoute(EditImageRoute(asset: asset, image: filteredImage, isEdited: true))); - }, - ), - ], - ), - backgroundColor: context.scaffoldBackgroundColor, - body: Column( - children: [ - SizedBox( - height: context.height * 0.7, - child: Center( - child: ColorFiltered(colorFilter: colorFilter.value, child: image), - ), - ), - SizedBox( - height: 120, - child: ListView.builder( - scrollDirection: Axis.horizontal, - itemCount: filters.length, - itemBuilder: (context, index) { - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 8.0), - child: _FilterButton( - image: image, - label: filterNames[index], - filter: filters[index], - isSelected: selectedFilterIndex.value == index, - onTap: () => applyFilter(filters[index], index), - ), - ); - }, - ), - ), - ], - ), - ); - } -} - -class _FilterButton extends StatelessWidget { - final Image image; - final String label; - final ColorFilter filter; - final bool isSelected; - final VoidCallback onTap; - - const _FilterButton({ - required this.image, - required this.label, - required this.filter, - required this.isSelected, - required this.onTap, - }); - - @override - Widget build(BuildContext context) { - return Column( - children: [ - GestureDetector( - onTap: onTap, - child: Container( - width: 80, - height: 80, - decoration: BoxDecoration( - borderRadius: const BorderRadius.all(Radius.circular(10)), - border: isSelected ? Border.all(color: context.primaryColor, width: 3) : null, - ), - child: ClipRRect( - borderRadius: const BorderRadius.all(Radius.circular(10)), - child: ColorFiltered( - colorFilter: filter, - child: FittedBox(fit: BoxFit.cover, child: image), - ), - ), - ), - ), - const SizedBox(height: 10), - Text(label, style: context.themeData.textTheme.bodyMedium), - ], - ); - } -} diff --git a/mobile/lib/pages/library/archive.page.dart b/mobile/lib/pages/library/archive.page.dart deleted file mode 100644 index 8ca1bb9752..0000000000 --- a/mobile/lib/pages/library/archive.page.dart +++ /dev/null @@ -1,37 +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/providers/multiselect.provider.dart'; -import 'package:immich_mobile/providers/timeline.provider.dart'; -import 'package:immich_mobile/widgets/asset_grid/multiselect_grid.dart'; - -@RoutePage() -class ArchivePage extends HookConsumerWidget { - const ArchivePage({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - AppBar buildAppBar() { - final archiveRenderList = ref.watch(archiveTimelineProvider); - final count = archiveRenderList.value?.totalAssets.toString() ?? "?"; - return AppBar( - leading: IconButton(onPressed: () => context.maybePop(), icon: const Icon(Icons.arrow_back_ios_rounded)), - centerTitle: true, - automaticallyImplyLeading: false, - title: const Text('archive_page_title').tr(namedArgs: {'count': count}), - ); - } - - return Scaffold( - appBar: ref.watch(multiselectProvider) ? null : buildAppBar(), - body: MultiselectGrid( - renderListProvider: archiveTimelineProvider, - unarchive: true, - archiveEnabled: true, - deleteEnabled: true, - editEnabled: true, - ), - ); - } -} diff --git a/mobile/lib/pages/library/favorite.page.dart b/mobile/lib/pages/library/favorite.page.dart deleted file mode 100644 index 649d7727d5..0000000000 --- a/mobile/lib/pages/library/favorite.page.dart +++ /dev/null @@ -1,34 +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/providers/multiselect.provider.dart'; -import 'package:immich_mobile/providers/timeline.provider.dart'; -import 'package:immich_mobile/widgets/asset_grid/multiselect_grid.dart'; - -@RoutePage() -class FavoritesPage extends HookConsumerWidget { - const FavoritesPage({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - AppBar buildAppBar() { - return AppBar( - leading: IconButton(onPressed: () => context.maybePop(), icon: const Icon(Icons.arrow_back_ios_rounded)), - centerTitle: true, - automaticallyImplyLeading: false, - title: const Text('favorites').tr(), - ); - } - - return Scaffold( - appBar: ref.watch(multiselectProvider) ? null : buildAppBar(), - body: MultiselectGrid( - renderListProvider: favoriteTimelineProvider, - favoriteEnabled: true, - editEnabled: true, - unfavorite: true, - ), - ); - } -} diff --git a/mobile/lib/pages/library/library.page.dart b/mobile/lib/pages/library/library.page.dart deleted file mode 100644 index 99a534e9cf..0000000000 --- a/mobile/lib/pages/library/library.page.dart +++ /dev/null @@ -1,383 +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/domain/models/user.model.dart'; -import 'package:immich_mobile/extensions/asyncvalue_extensions.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/generated/translations.g.dart'; -import 'package:immich_mobile/providers/album/album.provider.dart'; -import 'package:immich_mobile/providers/partner.provider.dart'; -import 'package:immich_mobile/providers/search/people.provider.dart'; -import 'package:immich_mobile/providers/server_info.provider.dart'; -import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/presentation/widgets/images/remote_image_provider.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/user_avatar.dart'; -import 'package:immich_mobile/widgets/map/map_thumbnail.dart'; -import 'package:maplibre_gl/maplibre_gl.dart'; - -@RoutePage() -class LibraryPage extends ConsumerWidget { - const LibraryPage({super.key}); - @override - Widget build(BuildContext context, WidgetRef ref) { - context.locale; - final trashEnabled = ref.watch(serverInfoProvider.select((v) => v.serverFeatures.trash)); - - return Scaffold( - appBar: const ImmichAppBar(), - body: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: ListView( - shrinkWrap: true, - children: [ - Padding( - padding: const EdgeInsets.only(top: 16.0), - child: Row( - children: [ - ActionButton( - onPressed: () => context.pushRoute(const FavoritesRoute()), - icon: Icons.favorite_outline_rounded, - label: context.t.favorites, - ), - const SizedBox(width: 8), - ActionButton( - onPressed: () => context.pushRoute(const ArchiveRoute()), - icon: Icons.archive_outlined, - label: context.t.archived, - ), - ], - ), - ), - const SizedBox(height: 8), - Row( - children: [ - ActionButton( - onPressed: () => context.pushRoute(const SharedLinkRoute()), - icon: Icons.link_outlined, - label: context.t.shared_links, - ), - SizedBox(width: trashEnabled ? 8 : 0), - trashEnabled - ? ActionButton( - onPressed: () => context.pushRoute(const TrashRoute()), - icon: Icons.delete_outline_rounded, - label: context.t.trash, - ) - : const SizedBox.shrink(), - ], - ), - const SizedBox(height: 12), - const Wrap( - spacing: 8, - runSpacing: 8, - children: [PeopleCollectionCard(), PlacesCollectionCard(), LocalAlbumsCollectionCard()], - ), - const SizedBox(height: 12), - const 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: const BorderRadius.all(Radius.circular(20)), - gradient: LinearGradient( - colors: [ - context.colorScheme.primary.withAlpha(10), - context.colorScheme.primary.withAlpha(15), - context.colorScheme.primary.withAlpha(20), - ], - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - ), - ), - child: ListView( - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - children: [ - ListTile( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.only( - topLeft: const Radius.circular(20), - topRight: const Radius.circular(20), - bottomLeft: Radius.circular(partners.isEmpty ? 20 : 0), - bottomRight: Radius.circular(partners.isEmpty ? 20 : 0), - ), - ), - leading: const Icon(Icons.folder_outlined, size: 26), - title: Text(context.t.folders, style: context.textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w500)), - onTap: () => context.pushRoute(FolderRoute()), - ), - ListTile( - leading: const Icon(Icons.lock_outline_rounded, size: 26), - title: Text( - context.t.locked_folder, - style: context.textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w500), - ), - onTap: () => context.pushRoute(const LockedRoute()), - ), - ListTile( - leading: const Icon(Icons.group_outlined, size: 26), - title: Text(context.t.partners, 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 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: const Text( - "partner_list_user_photos", - style: TextStyle(fontWeight: FontWeight.w500), - ).tr(namedArgs: {'user': partner.name}), - onTap: () => context.pushRoute((PartnerDetailRoute(partner: partner))), - ); - }, - ); - } -} - -class PeopleCollectionCard extends ConsumerWidget { - const PeopleCollectionCard({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final people = ref.watch(getAllPeopleProvider); - return LayoutBuilder( - builder: (context, constraints) { - final isTablet = constraints.maxWidth > 600; - final widthFactor = isTablet ? 0.25 : 0.5; - final size = context.width * widthFactor - 20.0; - - return GestureDetector( - onTap: () => context.pushRoute(const PeopleCollectionRoute()), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Container( - height: size, - width: size, - decoration: BoxDecoration( - 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: people.widgetWhen( - onLoading: () => const Center(child: CircularProgressIndicator()), - onData: (people) { - return GridView.count( - crossAxisCount: 2, - padding: const EdgeInsets.all(12), - crossAxisSpacing: 8, - mainAxisSpacing: 8, - physics: const NeverScrollableScrollPhysics(), - children: people.take(4).map((person) { - return CircleAvatar(backgroundImage: RemoteImageProvider(url: getFaceThumbnailUrl(person.id))); - }).toList(), - ); - }, - ), - ), - Padding( - padding: const EdgeInsets.all(8.0), - child: Text( - context.t.people, - 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); - - return LayoutBuilder( - builder: (context, constraints) { - final isTablet = constraints.maxWidth > 600; - final widthFactor = isTablet ? 0.25 : 0.5; - final size = context.width * widthFactor - 20.0; - - return GestureDetector( - onTap: () => context.pushRoute(const LocalAlbumsRoute()), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox( - height: size, - width: size, - child: DecoratedBox( - decoration: BoxDecoration( - 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: 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( - context.t.on_this_device, - 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) { - return LayoutBuilder( - builder: (context, constraints) { - final isTablet = constraints.maxWidth > 600; - final widthFactor = isTablet ? 0.25 : 0.5; - final size = context.width * widthFactor - 20.0; - - return GestureDetector( - onTap: () => context.pushRoute(PlacesCollectionRoute(currentLocation: null)), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox( - height: size, - width: size, - child: DecoratedBox( - decoration: BoxDecoration( - borderRadius: const BorderRadius.all(Radius.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: const EdgeInsets.all(8.0), - child: Text( - context.t.places, - style: context.textTheme.titleSmall?.copyWith( - color: context.colorScheme.onSurface, - fontWeight: FontWeight.w500, - ), - ), - ), - ], - ), - ); - }, - ); - } -} - -class ActionButton extends StatelessWidget { - final VoidCallback onPressed; - final IconData icon; - final String label; - - const ActionButton({super.key, required this.onPressed, required this.icon, required this.label}); - - @override - Widget build(BuildContext context) { - return Expanded( - child: FilledButton.icon( - onPressed: onPressed, - label: Padding( - padding: const EdgeInsets.only(left: 4.0), - child: Text(label, style: TextStyle(color: context.colorScheme.onSurface, fontSize: 15)), - ), - style: FilledButton.styleFrom( - elevation: 0, - padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16), - backgroundColor: context.colorScheme.surfaceContainerLow, - alignment: Alignment.centerLeft, - shape: RoundedRectangleBorder( - borderRadius: const BorderRadius.all(Radius.circular(25)), - side: BorderSide(color: context.colorScheme.onSurface.withAlpha(10), width: 1), - ), - ), - icon: Icon(icon, color: context.primaryColor), - ), - ); - } -} diff --git a/mobile/lib/pages/library/local_albums.page.dart b/mobile/lib/pages/library/local_albums.page.dart deleted file mode 100644 index e52a8326df..0000000000 --- a/mobile/lib/pages/library/local_albums.page.dart +++ /dev/null @@ -1,49 +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/extensions/theme_extensions.dart'; -import 'package:immich_mobile/extensions/translate_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( - 'items_count'.t(context: context, args: {'count': albums[index].assetCount}), - style: context.textTheme.bodyMedium?.copyWith(color: context.colorScheme.onSurfaceSecondary), - ), - onTap: () => context.pushRoute(AlbumViewerRoute(albumId: albums[index].id)), - ), - ); - }, - ), - ); - } -} diff --git a/mobile/lib/pages/library/locked/locked.page.dart b/mobile/lib/pages/library/locked/locked.page.dart deleted file mode 100644 index aea62e0051..0000000000 --- a/mobile/lib/pages/library/locked/locked.page.dart +++ /dev/null @@ -1,82 +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/providers/auth.provider.dart'; -import 'package:immich_mobile/providers/multiselect.provider.dart'; -import 'package:immich_mobile/providers/timeline.provider.dart'; -import 'package:immich_mobile/widgets/asset_grid/multiselect_grid.dart'; - -@RoutePage() -class LockedPage extends HookConsumerWidget { - const LockedPage({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final appLifeCycle = useAppLifecycleState(); - final showOverlay = useState(false); - final authProviderNotifier = ref.read(authProvider.notifier); - // lock the page when it is destroyed - useEffect(() { - return () { - authProviderNotifier.lockPinCode(); - }; - }, []); - - useEffect(() { - if (context.mounted) { - if (appLifeCycle == AppLifecycleState.resumed) { - showOverlay.value = false; - } else { - showOverlay.value = true; - } - } - - return null; - }, [appLifeCycle]); - - return Scaffold( - appBar: ref.watch(multiselectProvider) ? null : const LockPageAppBar(), - body: showOverlay.value - ? const SizedBox() - : MultiselectGrid( - renderListProvider: lockedTimelineProvider, - topWidget: Padding( - padding: const EdgeInsets.all(16.0), - child: Center(child: Text('no_locked_photos_message'.tr(), style: context.textTheme.labelLarge)), - ), - editEnabled: false, - favoriteEnabled: false, - unfavorite: false, - archiveEnabled: false, - stackEnabled: false, - unarchive: false, - ), - ); - } -} - -class LockPageAppBar extends ConsumerWidget implements PreferredSizeWidget { - const LockPageAppBar({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - return AppBar( - leading: IconButton( - onPressed: () { - ref.read(authProvider.notifier).lockPinCode(); - context.maybePop(); - }, - icon: const Icon(Icons.arrow_back_ios_rounded), - ), - centerTitle: true, - automaticallyImplyLeading: false, - title: const Text('locked_folder').tr(), - ); - } - - @override - Size get preferredSize => const Size.fromHeight(kToolbarHeight); -} diff --git a/mobile/lib/pages/library/locked/pin_auth.page.dart b/mobile/lib/pages/library/locked/pin_auth.page.dart index a39c91871b..3af320dc5f 100644 --- a/mobile/lib/pages/library/locked/pin_auth.page.dart +++ b/mobile/lib/pages/library/locked/pin_auth.page.dart @@ -5,7 +5,6 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart' show useState; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/providers/local_auth.provider.dart'; import 'package:immich_mobile/routing/router.dart'; @@ -22,7 +21,6 @@ class PinAuthPage extends HookConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final localAuthState = ref.watch(localAuthProvider); final showPinRegistrationForm = useState(createPinCode); - final isBetaTimeline = Store.isBetaTimelineEnabled; Future registerBiometric(String pinCode) async { final isRegistered = await ref.read(localAuthProvider.notifier).registerBiometric(context, pinCode); @@ -36,11 +34,7 @@ class PinAuthPage extends HookConsumerWidget { ), ); - if (isBetaTimeline) { - unawaited(context.replaceRoute(const DriftLockedFolderRoute())); - } else { - unawaited(context.replaceRoute(const LockedRoute())); - } + unawaited(context.replaceRoute(const DriftLockedFolderRoute())); } } @@ -89,11 +83,7 @@ class PinAuthPage extends HookConsumerWidget { child: PinVerificationForm( autoFocus: true, onSuccess: (_) { - if (isBetaTimeline) { - context.replaceRoute(const DriftLockedFolderRoute()); - } else { - context.replaceRoute(const LockedRoute()); - } + context.replaceRoute(const DriftLockedFolderRoute()); }, ), ), diff --git a/mobile/lib/pages/library/partner/partner.page.dart b/mobile/lib/pages/library/partner/partner.page.dart deleted file mode 100644 index eae4228a2d..0000000000 --- a/mobile/lib/pages/library/partner/partner.page.dart +++ /dev/null @@ -1,139 +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/domain/models/user.model.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/providers/partner.provider.dart'; -import 'package:immich_mobile/services/partner.service.dart'; -import 'package:immich_mobile/widgets/common/confirm_dialog.dart'; -import 'package:immich_mobile/widgets/common/immich_toast.dart'; -import 'package:immich_mobile/widgets/common/user_avatar.dart'; - -@RoutePage() -class PartnerPage extends HookConsumerWidget { - const PartnerPage({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final List partners = ref.watch(partnerSharedByProvider); - final availableUsers = ref.watch(partnerAvailableProvider); - - addNewUsersHandler() async { - final users = availableUsers.value; - if (users == null || users.isEmpty) { - ImmichToast.show(context: context, msg: "partner_page_no_more_users".tr()); - return; - } - - final selectedUser = await showDialog( - context: context, - builder: (context) { - return SimpleDialog( - title: const Text("partner_page_select_partner").tr(), - children: [ - for (UserDto u in users) - SimpleDialogOption( - onPressed: () => context.pop(u), - child: Row( - children: [ - Padding(padding: const EdgeInsets.only(right: 8), child: userAvatar(context, u)), - Text(u.name), - ], - ), - ), - ], - ); - }, - ); - if (selectedUser != null) { - final ok = await ref.read(partnerServiceProvider).addPartner(selectedUser); - if (ok) { - ref.invalidate(partnerSharedByProvider); - } else { - ImmichToast.show(context: context, msg: "partner_page_partner_add_failed".tr(), toastType: ToastType.error); - } - } - } - - onDeleteUser(UserDto u) { - return showDialog( - context: context, - builder: (BuildContext context) { - return ConfirmDialog( - title: "stop_photo_sharing", - content: "partner_page_stop_sharing_content".tr(namedArgs: {'partner': u.name}), - onOk: () => ref.read(partnerServiceProvider).removePartner(u), - ); - }, - ); - } - - buildUserList(List users) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.only(left: 16.0, top: 16.0), - child: Text( - "partner_page_shared_to_title", - style: context.textTheme.titleSmall?.copyWith(color: context.colorScheme.onSurface.withAlpha(200)), - ).tr(), - ), - if (users.isNotEmpty) - ListView.builder( - shrinkWrap: true, - itemCount: users.length, - itemBuilder: ((context, index) { - return ListTile( - leading: userAvatar(context, users[index]), - title: Text(users[index].email, style: context.textTheme.bodyLarge), - trailing: IconButton( - icon: const Icon(Icons.person_remove), - onPressed: () => onDeleteUser(users[index]), - ), - ); - }), - ), - if (users.isEmpty) - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.symmetric(vertical: 8), - child: const Text("partner_page_empty_message", style: TextStyle(fontSize: 14)).tr(), - ), - Align( - alignment: Alignment.center, - child: ElevatedButton.icon( - onPressed: availableUsers.whenOrNull(data: (data) => addNewUsersHandler), - icon: const Icon(Icons.person_add), - label: const Text("add_partner").tr(), - ), - ), - ], - ), - ), - ], - ); - } - - return Scaffold( - appBar: AppBar( - title: const Text("partners").tr(), - elevation: 0, - centerTitle: false, - actions: [ - IconButton( - onPressed: availableUsers.whenOrNull(data: (data) => addNewUsersHandler), - icon: const Icon(Icons.person_add), - tooltip: "add_partner".tr(), - ), - ], - ), - body: buildUserList(partners), - ); - } -} diff --git a/mobile/lib/pages/library/partner/partner_detail.page.dart b/mobile/lib/pages/library/partner/partner_detail.page.dart deleted file mode 100644 index 1f15dab6a3..0000000000 --- a/mobile/lib/pages/library/partner/partner_detail.page.dart +++ /dev/null @@ -1,99 +0,0 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/domain/models/user.model.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/providers/asset.provider.dart'; -import 'package:immich_mobile/providers/multiselect.provider.dart'; -import 'package:immich_mobile/providers/partner.provider.dart'; -import 'package:immich_mobile/providers/timeline.provider.dart'; -import 'package:immich_mobile/widgets/asset_grid/multiselect_grid.dart'; -import 'package:immich_mobile/widgets/common/immich_toast.dart'; - -@RoutePage() -class PartnerDetailPage extends HookConsumerWidget { - const PartnerDetailPage({super.key, required this.partner}); - - final UserDto partner; - - @override - Widget build(BuildContext context, WidgetRef ref) { - final inTimeline = useState(partner.inTimeline); - bool toggleInProcess = false; - - useEffect(() { - Future.microtask(() async => {await ref.read(assetProvider.notifier).getAllAsset()}); - return null; - }, []); - - void toggleInTimeline() async { - if (toggleInProcess) return; - toggleInProcess = true; - try { - final ok = await ref - .read(partnerSharedWithProvider.notifier) - .updatePartner(partner, inTimeline: !inTimeline.value); - if (ok) { - inTimeline.value = !inTimeline.value; - final action = inTimeline.value ? "shown on" : "hidden from"; - ImmichToast.show( - context: context, - toastType: ToastType.success, - durationInSecond: 1, - msg: "${partner.name}'s assets $action your timeline", - ); - } else { - ImmichToast.show( - context: context, - toastType: ToastType.error, - durationInSecond: 1, - msg: "Failed to toggle the timeline setting", - ); - } - } finally { - toggleInProcess = false; - } - } - - return Scaffold( - appBar: ref.watch(multiselectProvider) - ? null - : AppBar(title: Text(partner.name), elevation: 0, centerTitle: false), - 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: const BorderRadius.all(Radius.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: singleUserTimelineProvider(partner.id), - onRefresh: () => ref.read(assetProvider.notifier).getAllAsset(), - deleteEnabled: false, - favoriteEnabled: false, - ), - ); - } -} diff --git a/mobile/lib/pages/library/people/people_collection.page.dart b/mobile/lib/pages/library/people/people_collection.page.dart deleted file mode 100644 index bff52df6da..0000000000 --- a/mobile/lib/pages/library/people/people_collection.page.dart +++ /dev/null @@ -1,127 +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/providers/search/people.provider.dart'; -import 'package:immich_mobile/presentation/widgets/images/remote_image_provider.dart'; -import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/utils/image_url_builder.dart'; -import 'package:immich_mobile/widgets/common/search_field.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 formFocus = useFocusNode(); - final ValueNotifier search = useState(null); - - showNameEditModel(String personId, String personName) { - return showDialog( - context: context, - useRootNavigator: false, - builder: (BuildContext context) { - return PersonNameEditForm(personId: personId, personName: personName); - }, - ); - } - - return LayoutBuilder( - builder: (context, constraints) { - final isTablet = constraints.maxWidth > 600; - final isPortrait = context.orientation == Orientation.portrait; - - return Scaffold( - appBar: AppBar( - automaticallyImplyLeading: search.value == null, - title: search.value != null - ? SearchField( - focusNode: formFocus, - onTapOutside: (_) => formFocus.unfocus(), - onChanged: (value) => search.value = value, - filled: true, - hintText: 'filter_people'.tr(), - autofocus: true, - ) - : Text('people'.tr()), - actions: [ - IconButton( - icon: Icon(search.value != null ? Icons.close : Icons.search), - onPressed: () { - search.value = search.value == null ? '' : null; - }, - ), - ], - ), - body: SafeArea( - child: people.when( - data: (people) { - if (search.value != null) { - people = people.where((person) { - return person.name.toLowerCase().contains(search.value!.toLowerCase()); - }).toList(); - } - return GridView.builder( - gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: isTablet ? 6 : 3, - childAspectRatio: 0.85, - mainAxisSpacing: isPortrait && isTablet ? 36 : 0, - ), - 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: isTablet ? 120 / 2 : 96 / 2, - backgroundImage: RemoteImageProvider(url: getFaceThumbnailUrl(person.id)), - ), - ), - ), - 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 Center(child: CircularProgressIndicator()), - ), - ), - ); - }, - ); - } -} diff --git a/mobile/lib/pages/library/places/places_collection.page.dart b/mobile/lib/pages/library/places/places_collection.page.dart deleted file mode 100644 index a4a6f66915..0000000000 --- a/mobile/lib/pages/library/places/places_collection.page.dart +++ /dev/null @@ -1,136 +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' hide Store; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/domain/models/store.model.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/presentation/widgets/images/remote_image_provider.dart'; -import 'package:immich_mobile/presentation/widgets/images/thumbnail.widget.dart'; -import 'package:immich_mobile/providers/search/search_page_state.provider.dart'; -import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/widgets/common/search_field.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, this.currentLocation}); - final LatLng? currentLocation; - @override - Widget build(BuildContext context, WidgetRef ref) { - final places = ref.watch(getAllPlacesProvider); - final formFocus = useFocusNode(); - final ValueNotifier search = useState(null); - - return Scaffold( - appBar: AppBar( - automaticallyImplyLeading: search.value == null, - title: search.value != null - ? SearchField( - autofocus: true, - filled: true, - focusNode: formFocus, - onChanged: (value) => search.value = value, - onTapOutside: (_) => formFocus.unfocus(), - hintText: 'filter_places'.tr(), - ) - : Text('places'.tr()), - actions: [ - IconButton( - icon: Icon(search.value != null ? Icons.close : Icons.search), - onPressed: () { - search.value = search.value == null ? '' : null; - }, - ), - ], - ), - body: ListView( - shrinkWrap: true, - children: [ - if (search.value == null) - Padding( - padding: const EdgeInsets.all(16.0), - child: SizedBox( - height: 200, - width: context.width, - child: MapThumbnail( - onTap: (_, __) => context.pushRoute(MapRoute(initialLocation: currentLocation)), - zoom: 8, - centre: currentLocation ?? const LatLng(21.44950, -157.91959), - showAttribution: false, - themeMode: context.isDarkTheme ? ThemeMode.dark : ThemeMode.light, - ), - ), - ), - places.when( - data: (places) { - if (search.value != null) { - places = places.where((place) { - return place.label.toLowerCase().contains(search.value!.toLowerCase()); - }).toList(); - } - 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) => Text('error_getting_places'.tr()), - loading: () => const Center(child: 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( - SearchRoute( - prefilter: SearchFilter( - people: {}, - location: SearchLocationFilter(city: name), - camera: SearchCameraFilter(), - date: SearchDateFilter(), - display: SearchDisplayFilters(isNotInAlbum: false, isArchive: false, isFavorite: false), - rating: SearchRatingFilter(), - mediaType: AssetType.other, - ), - ), - ); - } - - return LargeLeadingTile( - onTap: () => navigateToPlace(), - title: Text(name, style: context.textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w500)), - leading: ClipRRect( - borderRadius: const BorderRadius.all(Radius.circular(20)), - child: SizedBox( - width: 80, - height: 80, - child: Thumbnail(imageProvider: RemoteImageProvider(url: thumbnailUrl)), - ), - ), - ); - } -} diff --git a/mobile/lib/pages/library/trash.page.dart b/mobile/lib/pages/library/trash.page.dart deleted file mode 100644 index 2279998c2d..0000000000 --- a/mobile/lib/pages/library/trash.page.dart +++ /dev/null @@ -1,225 +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:fluttertoast/fluttertoast.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/extensions/asyncvalue_extensions.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/providers/asset.provider.dart'; -import 'package:immich_mobile/providers/server_info.provider.dart'; -import 'package:immich_mobile/providers/timeline.provider.dart'; -import 'package:immich_mobile/providers/trash.provider.dart'; -import 'package:immich_mobile/utils/immich_loading_overlay.dart'; -import 'package:immich_mobile/widgets/asset_grid/delete_dialog.dart'; -import 'package:immich_mobile/widgets/asset_grid/immich_asset_grid.dart'; -import 'package:immich_mobile/widgets/common/confirm_dialog.dart'; -import 'package:immich_mobile/widgets/common/immich_toast.dart'; - -@RoutePage() -class TrashPage extends HookConsumerWidget { - const TrashPage({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final trashRenderList = ref.watch(trashTimelineProvider); - final trashDays = ref.watch(serverInfoProvider.select((v) => v.serverConfig.trashDays)); - final selectionEnabledHook = useState(false); - final selection = useState({}); - final processing = useProcessingOverlay(); - - void selectionListener(bool multiselect, Set selectedAssets) { - selectionEnabledHook.value = multiselect; - selection.value = selectedAssets; - } - - onEmptyTrash() async { - processing.value = true; - await ref.read(trashProvider.notifier).emptyTrash(); - processing.value = false; - selectionEnabledHook.value = false; - if (context.mounted) { - ImmichToast.show(context: context, msg: 'trash_emptied'.tr(), gravity: ToastGravity.BOTTOM); - } - } - - handleEmptyTrash() async { - await showDialog( - context: context, - builder: (context) => ConfirmDialog( - onOk: () => onEmptyTrash(), - title: "empty_trash".tr(), - ok: "ok".tr(), - content: "trash_page_empty_trash_dialog_content".tr(), - ), - ); - } - - Future onPermanentlyDelete() async { - processing.value = true; - try { - if (selection.value.isNotEmpty) { - final isRemoved = await ref.read(assetProvider.notifier).deleteAssets(selection.value, force: true); - - if (isRemoved) { - if (context.mounted) { - ImmichToast.show( - context: context, - msg: 'assets_deleted_permanently'.tr(namedArgs: {'count': "${selection.value.length}"}), - gravity: ToastGravity.BOTTOM, - ); - } - } - } - } finally { - processing.value = false; - selectionEnabledHook.value = false; - } - } - - handlePermanentDelete() async { - await showDialog( - context: context, - builder: (context) => DeleteDialog(alert: "delete_dialog_alert_remote", onDelete: () => onPermanentlyDelete()), - ); - } - - Future handleRestoreAll() async { - processing.value = true; - await ref.read(trashProvider.notifier).restoreTrash(); - processing.value = false; - selectionEnabledHook.value = false; - } - - Future handleRestore() async { - processing.value = true; - try { - if (selection.value.isNotEmpty) { - final result = await ref.read(trashProvider.notifier).restoreAssets(selection.value); - - if (result && context.mounted) { - ImmichToast.show( - context: context, - msg: 'assets_restored_successfully'.tr(namedArgs: {'count': "${selection.value.length}"}), - gravity: ToastGravity.BOTTOM, - ); - } - } - } finally { - processing.value = false; - selectionEnabledHook.value = false; - } - } - - String getAppBarTitle(String count) { - if (selectionEnabledHook.value) { - return selection.value.isNotEmpty ? "${selection.value.length}" : "trash_page_select_assets_btn".tr(); - } - return 'trash_page_title'.tr(namedArgs: {'count': count}); - } - - AppBar buildAppBar(String count) { - return AppBar( - leading: IconButton( - onPressed: !selectionEnabledHook.value - ? () => context.maybePop() - : () { - selectionEnabledHook.value = false; - selection.value = {}; - }, - icon: !selectionEnabledHook.value - ? const Icon(Icons.arrow_back_ios_rounded) - : const Icon(Icons.close_rounded), - ), - centerTitle: !selectionEnabledHook.value, - automaticallyImplyLeading: false, - title: Text(getAppBarTitle(count)), - actions: [ - if (!selectionEnabledHook.value) - PopupMenuButton( - itemBuilder: (context) { - return [ - PopupMenuItem(value: () => selectionEnabledHook.value = true, child: const Text('select').tr()), - PopupMenuItem(value: handleEmptyTrash, child: const Text('empty_trash').tr()), - ]; - }, - onSelected: (fn) => fn(), - ), - ], - ); - } - - Widget buildBottomBar() { - return SafeArea( - child: Align( - alignment: Alignment.bottomCenter, - child: SizedBox( - height: 64, - child: Container( - color: context.themeData.canvasColor, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - TextButton.icon( - icon: Icon(Icons.delete_forever, color: Colors.red[400]), - label: Text( - selection.value.isEmpty ? 'trash_page_delete_all'.tr() : 'delete'.tr(), - style: TextStyle(fontSize: 14, color: Colors.red[400], fontWeight: FontWeight.bold), - ), - onPressed: processing.value - ? null - : selection.value.isEmpty - ? handleEmptyTrash - : handlePermanentDelete, - ), - TextButton.icon( - icon: const Icon(Icons.history_rounded), - label: Text( - selection.value.isEmpty ? 'trash_page_restore_all'.tr() : 'restore'.tr(), - style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold), - ), - onPressed: processing.value - ? null - : selection.value.isEmpty - ? handleRestoreAll - : handleRestore, - ), - ], - ), - ), - ), - ), - ); - } - - return Scaffold( - appBar: trashRenderList.maybeWhen( - orElse: () => buildAppBar("?"), - data: (data) => buildAppBar(data.totalAssets.toString()), - ), - body: trashRenderList.widgetWhen( - onData: (data) => data.isEmpty - ? Center(child: Text('trash_page_no_assets'.tr())) - : Stack( - children: [ - SafeArea( - child: ImmichAssetGrid( - renderList: data, - listener: selectionListener, - selectionActive: selectionEnabledHook.value, - showMultiSelectIndicator: false, - showStack: true, - topWidget: Padding( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 24), - child: const Text("trash_page_info").tr(namedArgs: {"days": "$trashDays"}), - ), - ), - ), - if (selectionEnabledHook.value) buildBottomBar(), - ], - ), - ), - ); - } -} diff --git a/mobile/lib/pages/onboarding/permission_onboarding.page.dart b/mobile/lib/pages/onboarding/permission_onboarding.page.dart deleted file mode 100644 index 52d4ac0125..0000000000 --- a/mobile/lib/pages/onboarding/permission_onboarding.page.dart +++ /dev/null @@ -1,141 +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/providers/gallery_permission.provider.dart'; -import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/widgets/common/immich_logo.dart'; -import 'package:immich_mobile/widgets/common/immich_title_text.dart'; -import 'package:permission_handler/permission_handler.dart'; - -@RoutePage() -class PermissionOnboardingPage extends HookConsumerWidget { - const PermissionOnboardingPage({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final PermissionStatus permission = ref.watch(galleryPermissionNotifier); - - // Navigate to the main Tab Controller when permission is granted - void goToBackup() => context.replaceRoute(const BackupControllerRoute()); - - // When the permission is denied, we show a request permission page - buildRequestPermission() { - return Column( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text('permission_onboarding_request', style: context.textTheme.titleMedium, textAlign: TextAlign.center).tr(), - const SizedBox(height: 18), - ElevatedButton( - onPressed: () => - ref.read(galleryPermissionNotifier.notifier).requestGalleryPermission().then((permission) async { - if (permission.isGranted) { - // If permission is limited, we will show the limited - // permission page - goToBackup(); - } - }), - child: const Text('continue').tr(), - ), - ], - ); - } - - // When permission is granted from outside the app, this will show to - // let them continue on to the main timeline - buildPermissionGranted() { - return Column( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - 'permission_onboarding_permission_granted', - style: context.textTheme.titleMedium, - textAlign: TextAlign.center, - ).tr(), - const SizedBox(height: 18), - ElevatedButton(onPressed: () => goToBackup(), child: const Text('permission_onboarding_get_started').tr()), - ], - ); - } - - // iOS 14+ has limited permission options, which let someone just share - // a few photos with the app. If someone only has limited permissions, we - // inform that Immich works best when given full permission - buildPermissionLimited() { - return Column( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Icon(Icons.warning_outlined, color: Colors.yellow, size: 48), - const SizedBox(height: 8), - Text( - 'permission_onboarding_permission_limited', - style: context.textTheme.titleMedium, - textAlign: TextAlign.center, - ).tr(), - const SizedBox(height: 18), - ElevatedButton( - onPressed: () => openAppSettings(), - child: const Text('permission_onboarding_go_to_settings').tr(), - ), - const SizedBox(height: 8.0), - TextButton(onPressed: () => goToBackup(), child: const Text('permission_onboarding_continue_anyway').tr()), - ], - ); - } - - buildPermissionDenied() { - return Column( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Icon(Icons.warning_outlined, color: Colors.red, size: 48), - const SizedBox(height: 8), - Text( - 'permission_onboarding_permission_denied', - style: context.textTheme.titleMedium, - textAlign: TextAlign.center, - ).tr(), - const SizedBox(height: 18), - ElevatedButton( - onPressed: () => openAppSettings(), - child: const Text('permission_onboarding_go_to_settings').tr(), - ), - ], - ); - } - - final Widget child = switch (permission) { - PermissionStatus.limited => buildPermissionLimited(), - PermissionStatus.denied => buildRequestPermission(), - PermissionStatus.granted || PermissionStatus.provisional => buildPermissionGranted(), - PermissionStatus.restricted || PermissionStatus.permanentlyDenied => buildPermissionDenied(), - }; - - return Scaffold( - body: SafeArea( - child: Center( - child: SizedBox( - width: 380, - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const ImmichLogo(heroTag: 'logo'), - const ImmichTitleText(), - AnimatedSwitcher( - duration: const Duration(milliseconds: 500), - child: Padding(padding: const EdgeInsets.all(18.0), child: child), - ), - TextButton(child: const Text('back').tr(), onPressed: () => context.maybePop()), - ], - ), - ), - ), - ), - ); - } -} diff --git a/mobile/lib/pages/photos/memory.page.dart b/mobile/lib/pages/photos/memory.page.dart deleted file mode 100644 index bd7973bc21..0000000000 --- a/mobile/lib/pages/photos/memory.page.dart +++ /dev/null @@ -1,324 +0,0 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/scheduler.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/models/memories/memory.model.dart'; -import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart'; -import 'package:immich_mobile/providers/haptic_feedback.provider.dart'; -import 'package:immich_mobile/widgets/common/immich_image.dart'; -import 'package:immich_mobile/widgets/memories/memory_bottom_info.dart'; -import 'package:immich_mobile/widgets/memories/memory_card.dart'; -import 'package:immich_mobile/widgets/memories/memory_epilogue.dart'; -import 'package:immich_mobile/widgets/memories/memory_progress_indicator.dart'; - -@RoutePage() -/// Expects [currentAssetProvider] to be set before navigating to this page -class MemoryPage extends HookConsumerWidget { - final List memories; - final int memoryIndex; - - const MemoryPage({required this.memories, required this.memoryIndex, super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final currentMemory = useState(memories[memoryIndex]); - final currentAssetPage = useState(0); - final currentMemoryIndex = useState(memoryIndex); - final assetProgress = useState("${currentAssetPage.value + 1}|${currentMemory.value.assets.length}"); - const bgColor = Colors.black; - final currentAsset = useState(null); - - /// The list of all of the asset page controllers - final memoryAssetPageControllers = List.generate(memories.length, (i) => usePageController()); - - /// The main vertically scrolling page controller with each list of memories - final memoryPageController = usePageController(initialPage: memoryIndex); - - useEffect(() { - // Memories is an immersive activity - SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersive); - return () { - // Clean up to normal edge to edge when we are done - SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); - }; - }); - - toNextMemory() { - memoryPageController.nextPage(duration: const Duration(milliseconds: 500), curve: Curves.easeIn); - } - - void toPreviousMemory() { - if (currentMemoryIndex.value > 0) { - // Move to the previous memory page - memoryPageController.previousPage(duration: const Duration(milliseconds: 500), curve: Curves.easeIn); - - // Wait for the next frame to ensure the page is built - SchedulerBinding.instance.addPostFrameCallback((_) { - final previousIndex = currentMemoryIndex.value - 1; - final previousMemoryController = memoryAssetPageControllers[previousIndex]; - - // Ensure the controller is attached - if (previousMemoryController.hasClients) { - previousMemoryController.jumpToPage(memories[previousIndex].assets.length - 1); - } else { - // Wait for the next frame until it is attached - SchedulerBinding.instance.addPostFrameCallback((_) { - if (previousMemoryController.hasClients) { - previousMemoryController.jumpToPage(memories[previousIndex].assets.length - 1); - } - }); - } - }); - } - } - - toNextAsset(int currentAssetIndex) { - if (currentAssetIndex + 1 < currentMemory.value.assets.length) { - // Go to the next asset - PageController controller = memoryAssetPageControllers[currentMemoryIndex.value]; - - controller.nextPage(curve: Curves.easeInOut, duration: const Duration(milliseconds: 500)); - } else { - // Go to the next memory since we are at the end of our assets - toNextMemory(); - } - } - - toPreviousAsset(int currentAssetIndex) { - if (currentAssetIndex > 0) { - // Go to the previous asset - PageController controller = memoryAssetPageControllers[currentMemoryIndex.value]; - - controller.previousPage(curve: Curves.easeInOut, duration: const Duration(milliseconds: 500)); - } else { - // Go to the previous memory since we are at the end of our assets - toPreviousMemory(); - } - } - - updateProgressText() { - assetProgress.value = "${currentAssetPage.value + 1}|${currentMemory.value.assets.length}"; - } - - /// Downloads and caches the image for the asset at this [currentMemory]'s index - precacheAsset(int index) async { - // Guard index out of range - if (index < 0) { - return; - } - - // Context might be removed due to popping out of Memory Lane during Scroll handling - if (!context.mounted) { - return; - } - - late Asset asset; - if (index < currentMemory.value.assets.length) { - // Uses the next asset in this current memory - asset = currentMemory.value.assets[index]; - } else { - // Precache the first asset in the next memory if available - final currentMemoryIndex = memories.indexOf(currentMemory.value); - - // Guard no memory found - if (currentMemoryIndex == -1) { - return; - } - - final nextMemoryIndex = currentMemoryIndex + 1; - // Guard no next memory - if (nextMemoryIndex >= memories.length) { - return; - } - - // Get the first asset from the next memory - asset = memories[nextMemoryIndex].assets.first; - } - - // Precache the asset - final size = MediaQuery.sizeOf(context); - await precacheImage( - ImmichImage.imageProvider(asset: asset, width: size.width, height: size.height), - context, - size: size, - ); - } - - // Precache the next page right away if we are on the first page - if (currentAssetPage.value == 0) { - Future.delayed(const Duration(milliseconds: 200)).then((_) => precacheAsset(1)); - } - - Future onAssetChanged(int otherIndex) async { - ref.read(hapticFeedbackProvider.notifier).selectionClick(); - currentAssetPage.value = otherIndex; - updateProgressText(); - - // Wait for page change animation to finish - await Future.delayed(const Duration(milliseconds: 400)); - // And then precache the next asset - await precacheAsset(otherIndex + 1); - - final asset = currentMemory.value.assets[otherIndex]; - currentAsset.value = asset; - ref.read(currentAssetProvider.notifier).set(asset); - } - - /* Notification listener is used instead of OnPageChanged callback since OnPageChanged is called - * when the page in the **center** of the viewer changes. We want to reset currentAssetPage only when the final - * page during the end of scroll is different than the current page - */ - return NotificationListener( - onNotification: (ScrollNotification notification) { - // Calculate OverScroll manually using the number of pixels away from maxScrollExtent - // maxScrollExtend contains the sum of horizontal pixels of all assets for depth = 1 - // or sum of vertical pixels of all memories for depth = 0 - if (notification is ScrollUpdateNotification) { - final isEpiloguePage = (memoryPageController.page?.floor() ?? 0) >= memories.length; - - final offset = notification.metrics.pixels; - if (isEpiloguePage && (offset > notification.metrics.maxScrollExtent + 150)) { - context.maybePop(); - return true; - } - } - - return false; - }, - child: Scaffold( - backgroundColor: bgColor, - body: SafeArea( - child: PageView.builder( - physics: const BouncingScrollPhysics(parent: AlwaysScrollableScrollPhysics()), - scrollDirection: Axis.vertical, - controller: memoryPageController, - onPageChanged: (pageNumber) { - ref.read(hapticFeedbackProvider.notifier).mediumImpact(); - if (pageNumber < memories.length) { - currentMemoryIndex.value = pageNumber; - currentMemory.value = memories[pageNumber]; - } - - currentAssetPage.value = 0; - - updateProgressText(); - }, - itemCount: memories.length + 1, - itemBuilder: (context, mIndex) { - // Build last page - if (mIndex == memories.length) { - return MemoryEpilogue( - onStartOver: () => memoryPageController.animateToPage( - 0, - duration: const Duration(seconds: 1), - curve: Curves.easeInOut, - ), - ); - } - // Build horizontal page - final assetController = memoryAssetPageControllers[mIndex]; - return Column( - children: [ - Padding( - padding: const EdgeInsets.only(left: 24.0, right: 24.0, top: 8.0, bottom: 2.0), - child: AnimatedBuilder( - animation: assetController, - builder: (context, child) { - double value = 0.0; - if (assetController.hasClients) { - // We can only access [page] if this has clients - value = assetController.page ?? 0; - } - return MemoryProgressIndicator( - ticks: memories[mIndex].assets.length, - value: (value + 1) / memories[mIndex].assets.length, - ); - }, - ), - ), - Expanded( - child: Stack( - children: [ - PageView.builder( - physics: const BouncingScrollPhysics(parent: AlwaysScrollableScrollPhysics()), - controller: assetController, - onPageChanged: onAssetChanged, - scrollDirection: Axis.horizontal, - itemCount: memories[mIndex].assets.length, - itemBuilder: (context, index) { - final asset = memories[mIndex].assets[index]; - return Stack( - children: [ - Container( - color: Colors.black, - child: MemoryCard(asset: asset, title: memories[mIndex].title, showTitle: index == 0), - ), - Positioned.fill( - child: Row( - children: [ - // Left side of the screen - Expanded( - child: GestureDetector( - behavior: HitTestBehavior.translucent, - onTap: () { - toPreviousAsset(index); - }, - ), - ), - - // Right side of the screen - Expanded( - child: GestureDetector( - behavior: HitTestBehavior.translucent, - onTap: () { - toNextAsset(index); - }, - ), - ), - ], - ), - ), - ], - ); - }, - ), - Positioned( - top: 8, - left: 8, - child: MaterialButton( - minWidth: 0, - onPressed: () { - // auto_route doesn't invoke pop scope, so - // turn off full screen mode here - // https://github.com/Milad-Akarie/auto_route_library/issues/1799 - context.maybePop(); - SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); - }, - shape: const CircleBorder(), - color: Colors.white.withValues(alpha: 0.2), - elevation: 0, - child: const Icon(Icons.close_rounded, color: Colors.white), - ), - ), - if (currentAsset.value != null && currentAsset.value!.isVideo) - Positioned( - bottom: 24, - right: 32, - child: Icon(Icons.videocam_outlined, color: Colors.grey[200]), - ), - ], - ), - ), - MemoryBottomInfo(memory: memories[mIndex]), - ], - ); - }, - ), - ), - ), - ); - } -} diff --git a/mobile/lib/pages/photos/photos.page.dart b/mobile/lib/pages/photos/photos.page.dart deleted file mode 100644 index 7f57247ec4..0000000000 --- a/mobile/lib/pages/photos/photos.page.dart +++ /dev/null @@ -1,130 +0,0 @@ -import 'dart:async'; - -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/providers/album/album.provider.dart'; -import 'package:immich_mobile/providers/asset.provider.dart'; -import 'package:immich_mobile/providers/multiselect.provider.dart'; -import 'package:immich_mobile/providers/server_info.provider.dart'; -import 'package:immich_mobile/providers/timeline.provider.dart'; -import 'package:immich_mobile/providers/user.provider.dart'; -import 'package:immich_mobile/providers/websocket.provider.dart'; -import 'package:immich_mobile/widgets/asset_grid/multiselect_grid.dart'; -import 'package:immich_mobile/widgets/common/immich_app_bar.dart'; -import 'package:immich_mobile/widgets/common/immich_loading_indicator.dart'; -import 'package:immich_mobile/widgets/memories/memory_lane.dart'; - -@RoutePage() -class PhotosPage extends HookConsumerWidget { - const PhotosPage({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final currentUser = ref.watch(currentUserProvider); - final timelineUsers = ref.watch(timelineUsersIdsProvider); - final tipOneOpacity = useState(0.0); - final refreshCount = useState(0); - - useEffect(() { - ref.read(websocketProvider.notifier).connect(); - Future(() => ref.read(assetProvider.notifier).getAllAsset()); - Future(() => ref.read(albumProvider.notifier).refreshRemoteAlbums()); - ref.read(serverInfoProvider.notifier).getServerInfo(); - - return; - }, []); - - Widget buildLoadingIndicator() { - Timer(const Duration(seconds: 2), () => tipOneOpacity.value = 1); - - return Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const ImmichLoadingIndicator(), - Padding( - padding: const EdgeInsets.only(top: 16.0), - child: Text( - 'home_page_building_timeline', - style: context.textTheme.titleMedium?.copyWith(color: context.primaryColor), - ).tr(), - ), - const SizedBox(height: 8), - AnimatedOpacity( - duration: const Duration(milliseconds: 1000), - opacity: tipOneOpacity.value, - child: Column( - children: [ - SizedBox( - width: 320, - child: Padding( - padding: const EdgeInsets.only(top: 8.0), - child: Text( - 'home_page_first_time_notice', - textAlign: TextAlign.center, - style: context.textTheme.bodyMedium, - ).tr(), - ), - ), - ], - ), - ), - ], - ), - ); - } - - Future refreshAssets() async { - final fullRefresh = refreshCount.value > 0; - - if (fullRefresh) { - unawaited( - Future.wait([ - ref.read(assetProvider.notifier).getAllAsset(clear: true), - ref.read(albumProvider.notifier).refreshRemoteAlbums(), - ]), - ); - - // refresh was forced: user requested another refresh within 2 seconds - refreshCount.value = 0; - } else { - await ref.read(assetProvider.notifier).getAllAsset(clear: false); - - refreshCount.value++; - // set counter back to 0 if user does not request refresh again - Timer(const Duration(seconds: 4), () => refreshCount.value = 0); - } - } - - return Stack( - children: [ - MultiselectGrid( - topWidget: (currentUser != null && currentUser.memoryEnabled) ? const MemoryLane() : const SizedBox(), - renderListProvider: timelineUsers.length > 1 - ? multiUsersTimelineProvider(timelineUsers) - : singleUserTimelineProvider(currentUser?.id), - buildLoadingIndicator: buildLoadingIndicator, - onRefresh: refreshAssets, - stackEnabled: true, - archiveEnabled: true, - editEnabled: true, - ), - AnimatedPositioned( - duration: const Duration(milliseconds: 300), - top: ref.watch(multiselectProvider) ? -(kToolbarHeight + context.padding.top) : 0, - left: 0, - right: 0, - child: Container( - height: kToolbarHeight + context.padding.top, - color: context.themeData.appBarTheme.backgroundColor, - child: const ImmichAppBar(), - ), - ), - ], - ); - } -} diff --git a/mobile/lib/pages/search/all_motion_videos.page.dart b/mobile/lib/pages/search/all_motion_videos.page.dart deleted file mode 100644 index 60bb8a6cff..0000000000 --- a/mobile/lib/pages/search/all_motion_videos.page.dart +++ /dev/null @@ -1,25 +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/asyncvalue_extensions.dart'; -import 'package:immich_mobile/widgets/asset_grid/immich_asset_grid.dart'; -import 'package:immich_mobile/providers/search/all_motion_photos.provider.dart'; - -@RoutePage() -class AllMotionPhotosPage extends HookConsumerWidget { - const AllMotionPhotosPage({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final motionPhotos = ref.watch(allMotionPhotosProvider); - - return Scaffold( - appBar: AppBar( - title: const Text('search_page_motion_photos').tr(), - leading: IconButton(onPressed: () => context.maybePop(), icon: const Icon(Icons.arrow_back_ios_rounded)), - ), - body: motionPhotos.widgetWhen(onData: (assets) => ImmichAssetGrid(assets: assets)), - ); - } -} diff --git a/mobile/lib/pages/search/all_people.page.dart b/mobile/lib/pages/search/all_people.page.dart deleted file mode 100644 index b2814e6c13..0000000000 --- a/mobile/lib/pages/search/all_people.page.dart +++ /dev/null @@ -1,31 +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/asyncvalue_extensions.dart'; -import 'package:immich_mobile/models/search/search_curated_content.model.dart'; -import 'package:immich_mobile/providers/search/people.provider.dart'; -import 'package:immich_mobile/widgets/search/explore_grid.dart'; - -@RoutePage() -class AllPeoplePage extends HookConsumerWidget { - const AllPeoplePage({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final curatedPeople = ref.watch(getAllPeopleProvider); - - return Scaffold( - appBar: AppBar( - title: const Text('people').tr(), - leading: IconButton(onPressed: () => context.maybePop(), icon: const Icon(Icons.arrow_back_ios_rounded)), - ), - body: curatedPeople.widgetWhen( - onData: (people) => ExploreGrid( - isPeople: true, - curatedContent: people.map((e) => SearchCuratedContent(label: e.name, id: e.id)).toList(), - ), - ), - ); - } -} diff --git a/mobile/lib/pages/search/all_places.page.dart b/mobile/lib/pages/search/all_places.page.dart deleted file mode 100644 index c92f87d3ac..0000000000 --- a/mobile/lib/pages/search/all_places.page.dart +++ /dev/null @@ -1,26 +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/asyncvalue_extensions.dart'; -import 'package:immich_mobile/models/search/search_curated_content.model.dart'; -import 'package:immich_mobile/providers/search/search_page_state.provider.dart'; -import 'package:immich_mobile/widgets/search/explore_grid.dart'; - -@RoutePage() -class AllPlacesPage extends HookConsumerWidget { - const AllPlacesPage({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - AsyncValue> places = ref.watch(getAllPlacesProvider); - - return Scaffold( - appBar: AppBar( - title: const Text('places').tr(), - leading: IconButton(onPressed: () => context.maybePop(), icon: const Icon(Icons.arrow_back_ios_rounded)), - ), - body: places.widgetWhen(onData: (data) => ExploreGrid(curatedContent: data)), - ); - } -} diff --git a/mobile/lib/pages/search/all_videos.page.dart b/mobile/lib/pages/search/all_videos.page.dart deleted file mode 100644 index acad043a58..0000000000 --- a/mobile/lib/pages/search/all_videos.page.dart +++ /dev/null @@ -1,22 +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/providers/timeline.provider.dart'; -import 'package:immich_mobile/widgets/asset_grid/multiselect_grid.dart'; - -@RoutePage() -class AllVideosPage extends HookConsumerWidget { - const AllVideosPage({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - return Scaffold( - appBar: AppBar( - title: const Text('videos').tr(), - leading: IconButton(onPressed: () => context.maybePop(), icon: const Icon(Icons.arrow_back_ios_rounded)), - ), - body: MultiselectGrid(renderListProvider: allVideosTimelineProvider), - ); - } -} diff --git a/mobile/lib/pages/search/map/map.page.dart b/mobile/lib/pages/search/map/map.page.dart deleted file mode 100644 index 993b91d8f7..0000000000 --- a/mobile/lib/pages/search/map/map.page.dart +++ /dev/null @@ -1,384 +0,0 @@ -import 'dart:async'; -import 'dart:math'; - -import 'package:auto_route/auto_route.dart'; -import 'package:collection/collection.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:fluttertoast/fluttertoast.dart'; -import 'package:geolocator/geolocator.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/extensions/asyncvalue_extensions.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/extensions/maplibrecontroller_extensions.dart'; -import 'package:immich_mobile/models/map/map_event.model.dart'; -import 'package:immich_mobile/models/map/map_marker.model.dart'; -import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart'; -import 'package:immich_mobile/providers/asset_viewer/show_controls.provider.dart'; -import 'package:immich_mobile/providers/db.provider.dart'; -import 'package:immich_mobile/providers/map/map_marker.provider.dart'; -import 'package:immich_mobile/providers/map/map_state.provider.dart'; -import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/utils/debounce.dart'; -import 'package:immich_mobile/utils/immich_loading_overlay.dart'; -import 'package:immich_mobile/utils/map_utils.dart'; -import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart'; -import 'package:immich_mobile/widgets/common/immich_toast.dart'; -import 'package:immich_mobile/widgets/map/map_app_bar.dart'; -import 'package:immich_mobile/widgets/map/map_asset_grid.dart'; -import 'package:immich_mobile/widgets/map/map_bottom_sheet.dart'; -import 'package:immich_mobile/widgets/map/map_theme_override.dart'; -import 'package:immich_mobile/widgets/map/positioned_asset_marker_icon.dart'; -import 'package:maplibre_gl/maplibre_gl.dart'; - -@RoutePage() -class MapPage extends HookConsumerWidget { - const MapPage({super.key, this.initialLocation}); - final LatLng? initialLocation; - - @override - Widget build(BuildContext context, WidgetRef ref) { - final mapController = useRef(null); - final markers = useRef>([]); - final markersInBounds = useRef>([]); - final bottomSheetStreamController = useStreamController(); - final selectedMarker = useValueNotifier<_AssetMarkerMeta?>(null); - final assetsDebouncer = useDebouncer(); - final layerDebouncer = useDebouncer(interval: const Duration(seconds: 1)); - final isLoading = useProcessingOverlay(); - final scrollController = useScrollController(); - final markerDebouncer = useDebouncer(interval: const Duration(milliseconds: 800)); - final selectedAssets = useValueNotifier>({}); - const mapZoomToAssetLevel = 12.0; - - // updates the markersInBounds value with the map markers that are visible in the current - // map camera bounds - Future updateAssetsInBounds() async { - // Guard map not created - if (mapController.value == null) { - return; - } - - final bounds = await mapController.value!.getVisibleRegion(); - final inBounds = markers.value - .where((m) => bounds.contains(LatLng(m.latLng.latitude, m.latLng.longitude))) - .toList(); - // Notify bottom sheet to update asset grid only when there are new assets - if (markersInBounds.value.length != inBounds.length) { - bottomSheetStreamController.add(MapAssetsInBoundsUpdated(inBounds.map((e) => e.assetRemoteId).toList())); - } - markersInBounds.value = inBounds; - } - - // removes all sources and layers and re-adds them with the updated markers - Future reloadLayers() async { - if (mapController.value != null) { - layerDebouncer.run(() => mapController.value!.reloadAllLayersForMarkers(markers.value)); - } - } - - Future loadMarkers() async { - try { - isLoading.value = true; - markers.value = await ref.read(mapMarkersProvider.future); - assetsDebouncer.run(updateAssetsInBounds); - await reloadLayers(); - } finally { - isLoading.value = false; - } - } - - useEffect(() { - final currentAssetLink = ref.read(currentAssetProvider.notifier).ref.keepAlive(); - - loadMarkers(); - return currentAssetLink.close; - }, []); - - // Refetch markers when map state is changed - ref.listen(mapStateNotifierProvider, (_, current) { - if (current.shouldRefetchMarkers) { - markerDebouncer.run(() { - ref.invalidate(mapMarkersProvider); - // Reset marker - selectedMarker.value = null; - loadMarkers(); - ref.read(mapStateNotifierProvider.notifier).setRefetchMarkers(false); - }); - } - }); - - // updates the selected markers position based on the current map camera - Future updateAssetMarkerPosition(MapMarker marker, {bool shouldAnimate = true}) async { - final assetPoint = await mapController.value!.toScreenLocation(marker.latLng); - selectedMarker.value = _AssetMarkerMeta(point: assetPoint, marker: marker, shouldAnimate: shouldAnimate); - (assetPoint, marker, shouldAnimate); - } - - // finds the nearest asset marker from the tap point and store it as the selectedMarker - Future onMarkerClicked(Point point, LatLng _) async { - // Guard map not created - if (mapController.value == null) { - return; - } - final latlngBound = await mapController.value!.getBoundsFromPoint(point, 50); - final marker = markersInBounds.value.firstWhereOrNull( - (m) => latlngBound.contains(LatLng(m.latLng.latitude, m.latLng.longitude)), - ); - - if (marker != null) { - await updateAssetMarkerPosition(marker); - } else { - // If no asset was previously selected and no new asset is available, close the bottom sheet - if (selectedMarker.value == null) { - bottomSheetStreamController.add(const MapCloseBottomSheet()); - } - selectedMarker.value = null; - } - } - - void onMapCreated(MapLibreMapController controller) async { - mapController.value = controller; - controller.addListener(() { - if (controller.isCameraMoving && selectedMarker.value != null) { - updateAssetMarkerPosition(selectedMarker.value!.marker, shouldAnimate: false); - } - }); - } - - Future onMarkerTapped() async { - final assetId = selectedMarker.value?.marker.assetRemoteId; - if (assetId == null) { - return; - } - - final asset = await ref.read(dbProvider).assets.getByRemoteId(assetId); - if (asset == null) { - return; - } - - // Since we only have a single asset, we can just show GroupAssetBy.none - final renderList = await RenderList.fromAssets([asset], GroupAssetsBy.none); - - ref.read(currentAssetProvider.notifier).set(asset); - if (asset.isVideo) { - ref.read(showControlsProvider.notifier).show = false; - } - unawaited(context.pushRoute(GalleryViewerRoute(initialIndex: 0, heroOffset: 0, renderList: renderList))); - } - - /// BOTTOM SHEET CALLBACKS - - Future onMapMoved() async { - assetsDebouncer.run(updateAssetsInBounds); - } - - void onBottomSheetScrolled(String assetRemoteId) { - final assetMarker = markersInBounds.value.firstWhereOrNull((m) => m.assetRemoteId == assetRemoteId); - if (assetMarker != null) { - updateAssetMarkerPosition(assetMarker); - } - } - - void onZoomToAsset(String assetRemoteId) { - final assetMarker = markersInBounds.value.firstWhereOrNull((m) => m.assetRemoteId == assetRemoteId); - if (mapController.value != null && assetMarker != null) { - // Offset the latitude a little to show the marker just above the viewports center - final offset = context.isMobile ? 0.02 : 0; - final latlng = LatLng(assetMarker.latLng.latitude - offset, assetMarker.latLng.longitude); - mapController.value!.animateCamera( - CameraUpdate.newLatLngZoom(latlng, mapZoomToAssetLevel), - duration: const Duration(milliseconds: 800), - ); - } - } - - void onZoomToLocation() async { - final (location, error) = await MapUtils.checkPermAndGetLocation(context: context); - if (error != null) { - if (error == LocationPermission.unableToDetermine && context.mounted) { - ImmichToast.show( - context: context, - gravity: ToastGravity.BOTTOM, - toastType: ToastType.error, - msg: "map_cannot_get_user_location".tr(), - ); - } - return; - } - - if (mapController.value != null && location != null) { - await mapController.value!.animateCamera( - CameraUpdate.newLatLngZoom(LatLng(location.latitude, location.longitude), mapZoomToAssetLevel), - duration: const Duration(milliseconds: 800), - ); - } - } - - void onAssetsSelected(bool selected, Set selection) { - selectedAssets.value = selected ? selection : {}; - } - - return MapThemeOverride( - mapBuilder: (style) => context.isMobile - // Single-column - ? Scaffold( - extendBodyBehindAppBar: true, - appBar: MapAppBar(selectedAssets: selectedAssets), - body: Stack( - children: [ - _MapWithMarker( - initialLocation: initialLocation, - style: style, - selectedMarker: selectedMarker, - onMapCreated: onMapCreated, - onMapMoved: onMapMoved, - onMapClicked: onMarkerClicked, - onStyleLoaded: reloadLayers, - onMarkerTapped: onMarkerTapped, - ), - // Should be a part of the body and not scaffold::bottomsheet for the - // location button to be hit testable - MapBottomSheet( - mapEventStream: bottomSheetStreamController.stream, - onGridAssetChanged: onBottomSheetScrolled, - onZoomToAsset: onZoomToAsset, - onAssetsSelected: onAssetsSelected, - onZoomToLocation: onZoomToLocation, - selectedAssets: selectedAssets, - ), - ], - ), - ) - // Two-pane - : Row( - children: [ - Expanded( - child: Scaffold( - extendBodyBehindAppBar: true, - appBar: MapAppBar(selectedAssets: selectedAssets), - body: Stack( - children: [ - _MapWithMarker( - initialLocation: initialLocation, - style: style, - selectedMarker: selectedMarker, - onMapCreated: onMapCreated, - onMapMoved: onMapMoved, - onMapClicked: onMarkerClicked, - onStyleLoaded: reloadLayers, - onMarkerTapped: onMarkerTapped, - ), - Positioned( - right: 0, - bottom: context.padding.bottom + 16, - child: ElevatedButton( - onPressed: onZoomToLocation, - style: ElevatedButton.styleFrom(shape: const CircleBorder()), - child: const Icon(Icons.my_location), - ), - ), - ], - ), - ), - ), - Expanded( - child: LayoutBuilder( - builder: (ctx, constraints) => MapAssetGrid( - controller: scrollController, - mapEventStream: bottomSheetStreamController.stream, - onGridAssetChanged: onBottomSheetScrolled, - onZoomToAsset: onZoomToAsset, - onAssetsSelected: onAssetsSelected, - selectedAssets: selectedAssets, - ), - ), - ), - ], - ), - ); - } -} - -class _AssetMarkerMeta { - final Point point; - final MapMarker marker; - final bool shouldAnimate; - - const _AssetMarkerMeta({required this.point, required this.marker, required this.shouldAnimate}); - - @override - String toString() => '_AssetMarkerMeta(point: $point, marker: $marker, shouldAnimate: $shouldAnimate)'; -} - -class _MapWithMarker extends StatelessWidget { - final AsyncValue style; - final MapCreatedCallback onMapCreated; - final OnCameraIdleCallback onMapMoved; - final OnMapClickCallback onMapClicked; - final OnStyleLoadedCallback onStyleLoaded; - final Function()? onMarkerTapped; - final ValueNotifier<_AssetMarkerMeta?> selectedMarker; - final LatLng? initialLocation; - - const _MapWithMarker({ - required this.style, - required this.onMapCreated, - required this.onMapMoved, - required this.onMapClicked, - required this.onStyleLoaded, - required this.selectedMarker, - this.onMarkerTapped, - this.initialLocation, - }); - - @override - Widget build(BuildContext context) { - return LayoutBuilder( - builder: (ctx, constraints) => SizedBox( - height: constraints.maxHeight, - width: constraints.maxWidth, - child: Stack( - children: [ - style.widgetWhen( - onData: (style) => MapLibreMap( - attributionButtonMargins: const Point(8, kToolbarHeight), - initialCameraPosition: CameraPosition( - target: initialLocation ?? const LatLng(0, 0), - zoom: initialLocation != null ? 12 : 0, - ), - styleString: style, - // This is needed to update the selectedMarker's position on map camera updates - // The changes are notified through the mapController ValueListener which is added in [onMapCreated] - trackCameraPosition: true, - onMapCreated: onMapCreated, - onCameraIdle: onMapMoved, - onMapClick: onMapClicked, - onStyleLoadedCallback: onStyleLoaded, - tiltGesturesEnabled: false, - dragEnabled: false, - myLocationEnabled: false, - attributionButtonPosition: AttributionButtonPosition.topRight, - rotateGesturesEnabled: false, - ), - ), - ValueListenableBuilder( - valueListenable: selectedMarker, - builder: (ctx, value, _) => value != null - ? PositionedAssetMarkerIcon( - point: value.point, - assetRemoteId: value.marker.assetRemoteId, - assetThumbhash: '', - durationInMilliseconds: value.shouldAnimate ? 100 : 0, - onTap: onMarkerTapped, - ) - : const SizedBox.shrink(), - ), - ], - ), - ), - ); - } -} diff --git a/mobile/lib/pages/search/person_result.page.dart b/mobile/lib/pages/search/person_result.page.dart deleted file mode 100644 index 8375eb14fd..0000000000 --- a/mobile/lib/pages/search/person_result.page.dart +++ /dev/null @@ -1,101 +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' hide Store; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/presentation/widgets/images/remote_image_provider.dart'; -import 'package:immich_mobile/providers/search/people.provider.dart'; -import 'package:immich_mobile/widgets/search/person_name_edit_form.dart'; -import 'package:immich_mobile/widgets/asset_grid/multiselect_grid.dart'; -import 'package:immich_mobile/utils/image_url_builder.dart'; - -@RoutePage() -class PersonResultPage extends HookConsumerWidget { - final String personId; - final String personName; - - const PersonResultPage({super.key, required this.personId, required this.personName}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final name = useState(personName); - - showEditNameDialog() { - showDialog( - context: context, - useRootNavigator: false, - builder: (BuildContext context) { - return PersonNameEditForm(personId: personId, personName: name.value); - }, - ).then((result) { - if (result != null && result.success) { - name.value = result.updatedName; - } - }); - } - - void buildBottomSheet() { - showModalBottomSheet( - backgroundColor: context.scaffoldBackgroundColor, - isScrollControlled: false, - context: context, - useSafeArea: true, - builder: (context) { - return SafeArea( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - ListTile( - leading: const Icon(Icons.edit_outlined), - title: const Text('edit_name', style: TextStyle(fontWeight: FontWeight.bold)).tr(), - onTap: showEditNameDialog, - ), - ], - ), - ); - }, - ); - } - - buildTitleBlock() { - return GestureDetector( - onTap: showEditNameDialog, - child: name.value.isEmpty - ? Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text('add_a_name', style: context.textTheme.titleMedium?.copyWith(color: context.primaryColor)).tr(), - Text('find_them_fast', style: context.textTheme.labelLarge).tr(), - ], - ) - : Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [Text(name.value, style: context.textTheme.titleLarge, overflow: TextOverflow.ellipsis)], - ), - ); - } - - return Scaffold( - appBar: AppBar( - title: Text(name.value), - leading: IconButton(onPressed: () => context.maybePop(), icon: const Icon(Icons.arrow_back_ios_rounded)), - actions: [IconButton(onPressed: buildBottomSheet, icon: const Icon(Icons.more_vert_rounded))], - ), - body: MultiselectGrid( - renderListProvider: personAssetsProvider(personId), - topWidget: Padding( - padding: const EdgeInsets.only(left: 8.0, top: 24), - child: Row( - children: [ - CircleAvatar(radius: 36, backgroundImage: RemoteImageProvider(url: getFaceThumbnailUrl(personId))), - Expanded( - child: Padding(padding: const EdgeInsets.only(left: 16.0, right: 16.0), child: buildTitleBlock()), - ), - ], - ), - ), - ), - ); - } -} diff --git a/mobile/lib/pages/search/recently_taken.page.dart b/mobile/lib/pages/search/recently_taken.page.dart deleted file mode 100644 index 988af2faf0..0000000000 --- a/mobile/lib/pages/search/recently_taken.page.dart +++ /dev/null @@ -1,25 +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/asyncvalue_extensions.dart'; -import 'package:immich_mobile/widgets/asset_grid/immich_asset_grid.dart'; -import 'package:immich_mobile/providers/search/recently_taken_asset.provider.dart'; - -@RoutePage() -class RecentlyTakenPage extends HookConsumerWidget { - const RecentlyTakenPage({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final recents = ref.watch(recentlyTakenAssetProvider); - - return Scaffold( - appBar: AppBar( - title: const Text('recently_taken_page_title').tr(), - leading: IconButton(onPressed: () => context.maybePop(), icon: const Icon(Icons.arrow_back_ios_rounded)), - ), - body: recents.widgetWhen(onData: (searchResponse) => ImmichAssetGrid(assets: searchResponse)), - ); - } -} diff --git a/mobile/lib/pages/search/search.page.dart b/mobile/lib/pages/search/search.page.dart deleted file mode 100644 index dbd32ac94b..0000000000 --- a/mobile/lib/pages/search/search.page.dart +++ /dev/null @@ -1,760 +0,0 @@ -import 'dart:async'; - -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/constants/enums.dart'; -import 'package:immich_mobile/domain/models/person.model.dart'; -import 'package:immich_mobile/entities/asset.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/providers/search/paginated_search.provider.dart'; -import 'package:immich_mobile/providers/search/search_input_focus.provider.dart'; -import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/widgets/asset_grid/multiselect_grid.dart'; -import 'package:immich_mobile/widgets/common/search_field.dart'; -import 'package:immich_mobile/widgets/search/search_filter/camera_picker.dart'; -import 'package:immich_mobile/widgets/search/search_filter/display_option_picker.dart'; -import 'package:immich_mobile/widgets/search/search_filter/filter_bottom_sheet_scaffold.dart'; -import 'package:immich_mobile/widgets/search/search_filter/location_picker.dart'; -import 'package:immich_mobile/widgets/search/search_filter/media_type_picker.dart'; -import 'package:immich_mobile/widgets/search/search_filter/people_picker.dart'; -import 'package:immich_mobile/widgets/search/search_filter/search_filter_chip.dart'; -import 'package:immich_mobile/widgets/search/search_filter/search_filter_utils.dart'; - -@RoutePage() -class SearchPage extends HookConsumerWidget { - const SearchPage({super.key, this.prefilter}); - - final SearchFilter? prefilter; - - @override - Widget build(BuildContext context, WidgetRef ref) { - final textSearchType = useState(TextSearchType.context); - final searchHintText = useState('sunrise_on_the_beach'.tr()); - final textSearchController = useTextEditingController(); - final filter = useState( - SearchFilter( - people: prefilter?.people ?? {}, - location: prefilter?.location ?? SearchLocationFilter(), - camera: prefilter?.camera ?? SearchCameraFilter(), - date: prefilter?.date ?? SearchDateFilter(), - display: prefilter?.display ?? SearchDisplayFilters(isNotInAlbum: false, isArchive: false, isFavorite: false), - mediaType: prefilter?.mediaType ?? AssetType.other, - rating: prefilter?.rating ?? SearchRatingFilter(), - language: "${context.locale.languageCode}-${context.locale.countryCode}", - ), - ); - - final previousFilter = useState(null); - - final peopleCurrentFilterWidget = useState(null); - final dateRangeCurrentFilterWidget = useState(null); - final cameraCurrentFilterWidget = useState(null); - final locationCurrentFilterWidget = useState(null); - final mediaTypeCurrentFilterWidget = useState(null); - final displayOptionCurrentFilterWidget = useState(null); - - final isSearching = useState(false); - - SnackBar searchInfoSnackBar(String message) { - return SnackBar( - content: Text(message, style: context.textTheme.labelLarge), - showCloseIcon: true, - behavior: SnackBarBehavior.fixed, - closeIconColor: context.colorScheme.onSurface, - ); - } - - search() async { - if (filter.value.isEmpty) { - return; - } - - if (prefilter == null && filter.value == previousFilter.value) { - return; - } - - isSearching.value = true; - ref.watch(paginatedSearchProvider.notifier).clear(); - final hasResult = await ref.watch(paginatedSearchProvider.notifier).search(filter.value); - - if (!hasResult) { - context.showSnackBar(searchInfoSnackBar('search_no_result'.tr())); - } - - previousFilter.value = filter.value; - isSearching.value = false; - } - - loadMoreSearchResult() async { - isSearching.value = true; - final hasResult = await ref.watch(paginatedSearchProvider.notifier).search(filter.value); - - if (!hasResult) { - context.showSnackBar(searchInfoSnackBar('search_no_more_result'.tr())); - } - - isSearching.value = false; - } - - searchPrefilter() { - if (prefilter != null) { - Future.delayed(Duration.zero, () { - search(); - - if (prefilter!.location.city != null) { - locationCurrentFilterWidget.value = Text(prefilter!.location.city!, style: context.textTheme.labelLarge); - } - }); - } - } - - useEffect(() { - Future.microtask(() => ref.invalidate(paginatedSearchProvider)); - searchPrefilter(); - - return null; - }, []); - - showPeoplePicker() { - handleOnSelect(Set value) { - filter.value = filter.value.copyWith(people: value); - - peopleCurrentFilterWidget.value = Text( - value.map((e) => e.name != '' ? e.name : 'no_name'.tr()).join(', '), - style: context.textTheme.labelLarge, - ); - } - - handleClear() { - filter.value = filter.value.copyWith(people: {}); - - peopleCurrentFilterWidget.value = null; - search(); - } - - showFilterBottomSheet( - context: context, - isScrollControlled: true, - child: FractionallySizedBox( - heightFactor: 0.8, - child: FilterBottomSheetScaffold( - title: 'search_filter_people_title'.tr(), - expanded: true, - onSearch: search, - onClear: handleClear, - child: PeoplePicker(onSelect: handleOnSelect, filter: filter.value.people), - ), - ), - ); - } - - showLocationPicker() { - handleOnSelect(Map value) { - filter.value = filter.value.copyWith( - location: SearchLocationFilter(country: value['country'], city: value['city'], state: value['state']), - ); - - final locationText = []; - if (value['country'] != null) { - locationText.add(value['country']!); - } - - if (value['state'] != null) { - locationText.add(value['state']!); - } - - if (value['city'] != null) { - locationText.add(value['city']!); - } - - locationCurrentFilterWidget.value = Text(locationText.join(', '), style: context.textTheme.labelLarge); - } - - handleClear() { - filter.value = filter.value.copyWith(location: SearchLocationFilter()); - - locationCurrentFilterWidget.value = null; - search(); - } - - showFilterBottomSheet( - context: context, - isScrollControlled: true, - isDismissible: true, - child: FilterBottomSheetScaffold( - title: 'search_filter_location_title'.tr(), - onSearch: search, - onClear: handleClear, - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 16.0), - child: Container( - padding: EdgeInsets.only(bottom: context.viewInsets.bottom), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0), - child: LocationPicker(onSelected: handleOnSelect, filter: filter.value.location), - ), - ), - ), - ), - ); - } - - showCameraPicker() { - handleOnSelect(Map value) { - filter.value = filter.value.copyWith( - camera: SearchCameraFilter(make: value['make'], model: value['model']), - ); - - cameraCurrentFilterWidget.value = Text( - '${value['make'] ?? ''} ${value['model'] ?? ''}', - style: context.textTheme.labelLarge, - ); - } - - handleClear() { - filter.value = filter.value.copyWith(camera: SearchCameraFilter()); - - cameraCurrentFilterWidget.value = null; - search(); - } - - showFilterBottomSheet( - context: context, - isScrollControlled: true, - isDismissible: true, - child: FilterBottomSheetScaffold( - title: 'search_filter_camera_title'.tr(), - onSearch: search, - onClear: handleClear, - child: Padding( - padding: const EdgeInsets.all(16.0), - child: CameraPicker(onSelect: handleOnSelect, filter: filter.value.camera), - ), - ), - ); - } - - showDatePicker() async { - final firstDate = DateTime(1900); - final lastDate = DateTime.now(); - - final date = await showDateRangePicker( - context: context, - firstDate: firstDate, - lastDate: lastDate, - currentDate: DateTime.now(), - initialDateRange: DateTimeRange( - start: filter.value.date.takenAfter ?? lastDate, - end: filter.value.date.takenBefore ?? lastDate, - ), - helpText: 'search_filter_date_title'.tr(), - cancelText: 'cancel'.tr(), - confirmText: 'select'.tr(), - saveText: 'save'.tr(), - errorFormatText: 'invalid_date_format'.tr(), - errorInvalidText: 'invalid_date'.tr(), - fieldStartHintText: 'start_date'.tr(), - fieldEndHintText: 'end_date'.tr(), - initialEntryMode: DatePickerEntryMode.calendar, - keyboardType: TextInputType.text, - ); - - if (date == null) { - filter.value = filter.value.copyWith(date: SearchDateFilter()); - - dateRangeCurrentFilterWidget.value = null; - unawaited(search()); - return; - } - - filter.value = filter.value.copyWith( - date: SearchDateFilter( - takenAfter: date.start, - takenBefore: date.end.add(const Duration(hours: 23, minutes: 59, seconds: 59)), - ), - ); - - // If date range is less than 24 hours, set the end date to the end of the day - if (date.end.difference(date.start).inHours < 24) { - dateRangeCurrentFilterWidget.value = Text( - DateFormat.yMMMd().format(date.start.toLocal()), - style: context.textTheme.labelLarge, - ); - } else { - dateRangeCurrentFilterWidget.value = Text( - 'search_filter_date_interval'.tr( - namedArgs: { - "start": DateFormat.yMMMd().format(date.start.toLocal()), - "end": DateFormat.yMMMd().format(date.end.toLocal()), - }, - ), - style: context.textTheme.labelLarge, - ); - } - - unawaited(search()); - } - - // MEDIA PICKER - showMediaTypePicker() { - handleOnSelected(AssetType assetType) { - filter.value = filter.value.copyWith(mediaType: assetType); - - mediaTypeCurrentFilterWidget.value = Text( - assetType == AssetType.image - ? 'image'.tr() - : assetType == AssetType.video - ? 'video'.tr() - : 'all'.tr(), - style: context.textTheme.labelLarge, - ); - } - - handleClear() { - filter.value = filter.value.copyWith(mediaType: AssetType.other); - - mediaTypeCurrentFilterWidget.value = null; - search(); - } - - showFilterBottomSheet( - context: context, - child: FilterBottomSheetScaffold( - title: 'search_filter_media_type_title'.tr(), - onSearch: search, - onClear: handleClear, - child: MediaTypePicker(onSelect: handleOnSelected, filter: filter.value.mediaType), - ), - ); - } - - // DISPLAY OPTION - showDisplayOptionPicker() { - handleOnSelect(Map value) { - final filterText = []; - value.forEach((key, value) { - switch (key) { - case DisplayOption.notInAlbum: - filter.value = filter.value.copyWith(display: filter.value.display.copyWith(isNotInAlbum: value)); - if (value) { - filterText.add('search_filter_display_option_not_in_album'.tr()); - } - break; - case DisplayOption.archive: - filter.value = filter.value.copyWith(display: filter.value.display.copyWith(isArchive: value)); - if (value) { - filterText.add('archive'.tr()); - } - break; - case DisplayOption.favorite: - filter.value = filter.value.copyWith(display: filter.value.display.copyWith(isFavorite: value)); - if (value) { - filterText.add('favorite'.tr()); - } - break; - } - }); - - if (filterText.isEmpty) { - displayOptionCurrentFilterWidget.value = null; - return; - } - - displayOptionCurrentFilterWidget.value = Text(filterText.join(', '), style: context.textTheme.labelLarge); - } - - handleClear() { - filter.value = filter.value.copyWith( - display: SearchDisplayFilters(isNotInAlbum: false, isArchive: false, isFavorite: false), - ); - - displayOptionCurrentFilterWidget.value = null; - search(); - } - - showFilterBottomSheet( - context: context, - child: FilterBottomSheetScaffold( - title: 'display_options'.tr(), - onSearch: search, - onClear: handleClear, - child: DisplayOptionPicker(onSelect: handleOnSelect, filter: filter.value.display), - ), - ); - } - - handleTextSubmitted(String value) { - switch (textSearchType.value) { - case TextSearchType.context: - filter.value = filter.value.copyWith(filename: '', context: value, description: '', ocr: ''); - - break; - case TextSearchType.filename: - filter.value = filter.value.copyWith(filename: value, context: '', description: '', ocr: ''); - - break; - case TextSearchType.description: - filter.value = filter.value.copyWith(filename: '', context: '', description: value, ocr: ''); - break; - case TextSearchType.ocr: - filter.value = filter.value.copyWith(filename: '', context: '', description: '', ocr: value); - break; - } - - search(); - } - - IconData getSearchPrefixIcon() => switch (textSearchType.value) { - TextSearchType.context => Icons.image_search_rounded, - TextSearchType.filename => Icons.abc_rounded, - TextSearchType.description => Icons.text_snippet_outlined, - TextSearchType.ocr => Icons.document_scanner_outlined, - }; - - return Scaffold( - resizeToAvoidBottomInset: false, - appBar: AppBar( - automaticallyImplyLeading: true, - actions: [ - Padding( - padding: const EdgeInsets.only(right: 16.0), - child: MenuAnchor( - style: MenuStyle( - elevation: const WidgetStatePropertyAll(1), - shape: WidgetStateProperty.all( - const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(24))), - ), - padding: const WidgetStatePropertyAll(EdgeInsets.all(4)), - ), - builder: (BuildContext context, MenuController controller, Widget? child) { - return IconButton( - onPressed: () { - if (controller.isOpen) { - controller.close(); - } else { - controller.open(); - } - }, - icon: const Icon(Icons.more_vert_rounded), - tooltip: 'show_text_search_menu'.tr(), - ); - }, - menuChildren: [ - MenuItemButton( - child: ListTile( - leading: const Icon(Icons.image_search_rounded), - title: Text( - 'search_by_context'.tr(), - style: context.textTheme.bodyLarge?.copyWith( - fontWeight: FontWeight.w500, - color: textSearchType.value == TextSearchType.context ? context.colorScheme.primary : null, - ), - ), - selectedColor: context.colorScheme.primary, - selected: textSearchType.value == TextSearchType.context, - ), - onPressed: () { - textSearchType.value = TextSearchType.context; - searchHintText.value = 'sunrise_on_the_beach'.tr(); - }, - ), - MenuItemButton( - child: ListTile( - leading: const Icon(Icons.abc_rounded), - title: Text( - 'search_filter_filename'.tr(), - style: context.textTheme.bodyLarge?.copyWith( - fontWeight: FontWeight.w500, - color: textSearchType.value == TextSearchType.filename ? context.colorScheme.primary : null, - ), - ), - selectedColor: context.colorScheme.primary, - selected: textSearchType.value == TextSearchType.filename, - ), - onPressed: () { - textSearchType.value = TextSearchType.filename; - searchHintText.value = 'file_name_or_extension'.tr(); - }, - ), - MenuItemButton( - child: ListTile( - leading: const Icon(Icons.text_snippet_outlined), - title: Text( - 'search_by_description'.tr(), - style: context.textTheme.bodyLarge?.copyWith( - fontWeight: FontWeight.w500, - color: textSearchType.value == TextSearchType.description ? context.colorScheme.primary : null, - ), - ), - selectedColor: context.colorScheme.primary, - selected: textSearchType.value == TextSearchType.description, - ), - onPressed: () { - textSearchType.value = TextSearchType.description; - searchHintText.value = 'search_by_description_example'.tr(); - }, - ), - MenuItemButton( - child: ListTile( - leading: const Icon(Icons.document_scanner_outlined), - title: Text( - 'search_filter_ocr'.tr(), - style: context.textTheme.bodyLarge?.copyWith( - fontWeight: FontWeight.w500, - color: textSearchType.value == TextSearchType.ocr ? context.colorScheme.primary : null, - ), - ), - selectedColor: context.colorScheme.primary, - selected: textSearchType.value == TextSearchType.ocr, - ), - onPressed: () { - textSearchType.value = TextSearchType.ocr; - searchHintText.value = 'search_by_ocr_example'.tr(); - }, - ), - ], - ), - ), - ], - title: Container( - decoration: BoxDecoration( - border: Border.all(color: context.colorScheme.onSurface.withAlpha(0), width: 0), - borderRadius: const BorderRadius.all(Radius.circular(24)), - gradient: LinearGradient( - colors: [ - context.colorScheme.primary.withValues(alpha: 0.075), - context.colorScheme.primary.withValues(alpha: 0.09), - context.colorScheme.primary.withValues(alpha: 0.075), - ], - begin: Alignment.topLeft, - end: Alignment.bottomRight, - ), - ), - child: SearchField( - hintText: searchHintText.value, - key: const Key('search_text_field'), - controller: textSearchController, - contentPadding: prefilter != null ? const EdgeInsets.only(left: 24) : const EdgeInsets.all(8), - prefixIcon: prefilter != null ? null : Icon(getSearchPrefixIcon(), color: context.colorScheme.primary), - onSubmitted: handleTextSubmitted, - focusNode: ref.watch(searchInputFocusProvider), - ), - ), - ), - body: Column( - children: [ - Padding( - padding: const EdgeInsets.only(top: 12.0), - child: SizedBox( - height: 50, - child: ListView( - key: const Key('search_filter_chip_list'), - shrinkWrap: true, - scrollDirection: Axis.horizontal, - padding: const EdgeInsets.symmetric(horizontal: 16), - children: [ - SearchFilterChip( - icon: Icons.people_alt_outlined, - onTap: showPeoplePicker, - label: 'people'.tr(), - currentFilter: peopleCurrentFilterWidget.value, - ), - SearchFilterChip( - icon: Icons.location_on_outlined, - onTap: showLocationPicker, - label: 'search_filter_location'.tr(), - currentFilter: locationCurrentFilterWidget.value, - ), - SearchFilterChip( - icon: Icons.camera_alt_outlined, - onTap: showCameraPicker, - label: 'camera'.tr(), - currentFilter: cameraCurrentFilterWidget.value, - ), - SearchFilterChip( - icon: Icons.date_range_outlined, - onTap: showDatePicker, - label: 'search_filter_date'.tr(), - currentFilter: dateRangeCurrentFilterWidget.value, - ), - SearchFilterChip( - key: const Key('media_type_chip'), - icon: Icons.video_collection_outlined, - onTap: showMediaTypePicker, - label: 'search_filter_media_type'.tr(), - currentFilter: mediaTypeCurrentFilterWidget.value, - ), - SearchFilterChip( - icon: Icons.display_settings_outlined, - onTap: showDisplayOptionPicker, - label: 'search_filter_display_options'.tr(), - currentFilter: displayOptionCurrentFilterWidget.value, - ), - ], - ), - ), - ), - if (isSearching.value) - const Expanded(child: Center(child: CircularProgressIndicator())) - else - SearchResultGrid(onScrollEnd: loadMoreSearchResult, isSearching: isSearching.value), - ], - ), - ); - } -} - -class SearchResultGrid extends StatelessWidget { - final VoidCallback onScrollEnd; - final bool isSearching; - - const SearchResultGrid({super.key, required this.onScrollEnd, this.isSearching = false}); - - @override - Widget build(BuildContext context) { - return Expanded( - child: Padding( - padding: const EdgeInsets.only(top: 8.0), - child: NotificationListener( - onNotification: (notification) { - final isBottomSheetNotification = - notification.context?.findAncestorWidgetOfExactType() != null; - - final metrics = notification.metrics; - final isVerticalScroll = metrics.axis == Axis.vertical; - - if (metrics.pixels >= metrics.maxScrollExtent && isVerticalScroll && !isBottomSheetNotification) { - onScrollEnd(); - } - - return true; - }, - child: MultiselectGrid( - renderListProvider: paginatedSearchRenderListProvider, - archiveEnabled: true, - deleteEnabled: true, - editEnabled: true, - favoriteEnabled: true, - stackEnabled: false, - dragScrollLabelEnabled: false, - emptyIndicator: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0), - child: !isSearching ? const SearchEmptyContent() : const SizedBox.shrink(), - ), - ), - ), - ), - ); - } -} - -class SearchEmptyContent extends StatelessWidget { - const SearchEmptyContent({super.key}); - - @override - Widget build(BuildContext context) { - return NotificationListener( - onNotification: (_) => true, - child: ListView( - shrinkWrap: false, - children: [ - const SizedBox(height: 40), - Center( - child: Image.asset( - context.isDarkTheme ? 'assets/polaroid-dark.png' : 'assets/polaroid-light.png', - height: 125, - ), - ), - const SizedBox(height: 16), - Center(child: Text('search_page_search_photos_videos'.tr(), style: context.textTheme.labelLarge)), - const SizedBox(height: 32), - const QuickLinkList(), - ], - ), - ); - } -} - -class QuickLinkList extends StatelessWidget { - const QuickLinkList({super.key}); - - @override - Widget build(BuildContext context) { - return Container( - decoration: BoxDecoration( - borderRadius: const BorderRadius.all(Radius.circular(20)), - border: Border.all(color: context.colorScheme.outline.withAlpha(10), width: 1), - gradient: LinearGradient( - colors: [ - context.colorScheme.primary.withAlpha(10), - context.colorScheme.primary.withAlpha(15), - context.colorScheme.primary.withAlpha(20), - ], - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - ), - ), - child: ListView( - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - children: [ - QuickLink( - title: 'recently_taken'.tr(), - icon: Icons.schedule_outlined, - isTop: true, - onTap: () => context.pushRoute(const RecentlyTakenRoute()), - ), - QuickLink( - title: 'videos'.tr(), - icon: Icons.play_circle_outline_rounded, - onTap: () => context.pushRoute(const AllVideosRoute()), - ), - QuickLink( - title: 'favorites'.tr(), - icon: Icons.favorite_border_rounded, - isBottom: true, - onTap: () => context.pushRoute(const FavoritesRoute()), - ), - ], - ), - ); - } -} - -class QuickLink extends StatelessWidget { - final String title; - final IconData icon; - final VoidCallback onTap; - final bool isTop; - final bool isBottom; - - const QuickLink({ - super.key, - required this.title, - required this.icon, - required this.onTap, - this.isTop = false, - this.isBottom = false, - }); - - @override - Widget build(BuildContext context) { - final borderRadius = BorderRadius.only( - topLeft: Radius.circular(isTop ? 20 : 0), - topRight: Radius.circular(isTop ? 20 : 0), - bottomLeft: Radius.circular(isBottom ? 20 : 0), - bottomRight: Radius.circular(isBottom ? 20 : 0), - ); - - return ListTile( - shape: RoundedRectangleBorder(borderRadius: borderRadius), - leading: Icon(icon, size: 26), - title: Text(title, style: context.textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w500)), - onTap: onTap, - ); - } -} diff --git a/mobile/lib/pages/share_intent/share_intent.page.dart b/mobile/lib/pages/share_intent/share_intent.page.dart index 2be51fbfc9..2744b187de 100644 --- a/mobile/lib/pages/share_intent/share_intent.page.dart +++ b/mobile/lib/pages/share_intent/share_intent.page.dart @@ -2,7 +2,6 @@ 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/entities/store.entity.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/models/upload/share_intent_attachment.model.dart'; import 'package:immich_mobile/pages/common/large_leading_tile.dart'; @@ -66,7 +65,7 @@ class ShareIntentPage extends ConsumerWidget { ), leading: IconButton( onPressed: () { - context.navigateTo(Store.isBetaTimelineEnabled ? const TabShellRoute() : const TabControllerRoute()); + context.navigateTo(const TabShellRoute()); }, icon: const Icon(Icons.arrow_back), ), diff --git a/mobile/lib/presentation/pages/search/drift_search.page.dart b/mobile/lib/presentation/pages/search/drift_search.page.dart index 7e47a742ae..3ba4cf3497 100644 --- a/mobile/lib/presentation/pages/search/drift_search.page.dart +++ b/mobile/lib/presentation/pages/search/drift_search.page.dart @@ -6,11 +6,11 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/constants/enums.dart'; +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/domain/models/person.model.dart'; import 'package:immich_mobile/domain/models/tag.model.dart'; import 'package:immich_mobile/domain/models/timeline.model.dart'; import 'package:immich_mobile/domain/services/timeline.service.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/models/search/search_filter.model.dart'; diff --git a/mobile/lib/presentation/widgets/action_buttons/similar_photos_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/similar_photos_action_button.widget.dart index bb42140d0a..0acbbce613 100644 --- a/mobile/lib/presentation/widgets/action_buttons/similar_photos_action_button.widget.dart +++ b/mobile/lib/presentation/widgets/action_buttons/similar_photos_action_button.widget.dart @@ -3,7 +3,7 @@ import 'dart:async'; import 'package:auto_route/auto_route.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/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/models/search/search_filter.model.dart'; import 'package:immich_mobile/presentation/pages/search/paginated_search.provider.dart'; diff --git a/mobile/lib/presentation/widgets/album/album_selector.widget.dart b/mobile/lib/presentation/widgets/album/album_selector.widget.dart index 0c039847a4..e5b4607619 100644 --- a/mobile/lib/presentation/widgets/album/album_selector.widget.dart +++ b/mobile/lib/presentation/widgets/album/album_selector.widget.dart @@ -17,9 +17,9 @@ import 'package:immich_mobile/presentation/widgets/album/new_album_name_modal.wi import 'package:immich_mobile/presentation/widgets/images/thumbnail.widget.dart'; import 'package:immich_mobile/providers/album/album_sort_by_options.provider.dart'; import 'package:immich_mobile/providers/app_settings.provider.dart'; +import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart'; import 'package:immich_mobile/providers/infrastructure/album.provider.dart'; import 'package:immich_mobile/providers/infrastructure/asset.provider.dart'; -import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart'; import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart'; import 'package:immich_mobile/routing/router.dart'; diff --git a/mobile/lib/presentation/widgets/map/map.widget.dart b/mobile/lib/presentation/widgets/map/map.widget.dart index 72f4e8bda6..3f406dd551 100644 --- a/mobile/lib/presentation/widgets/map/map.widget.dart +++ b/mobile/lib/presentation/widgets/map/map.widget.dart @@ -132,7 +132,7 @@ class _DriftMapState extends ConsumerState { // If we continue to update bounds, the map-scoped timeline service gets recreated and the previous one disposed, // which can invalidate the TimelineService instance that was passed into AssetViewerRoute (causing "loading forever"). final currentRoute = ref.read(currentRouteNameProvider); - if (currentRoute == AssetViewerRoute.name || currentRoute == GalleryViewerRoute.name) { + if (currentRoute == AssetViewerRoute.name) { return; } diff --git a/mobile/lib/providers/album/album.provider.dart b/mobile/lib/providers/album/album.provider.dart deleted file mode 100644 index 35634d77c8..0000000000 --- a/mobile/lib/providers/album/album.provider.dart +++ /dev/null @@ -1,151 +0,0 @@ -import 'dart:async'; - -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/constants/enums.dart'; -import 'package:immich_mobile/domain/models/user.model.dart'; -import 'package:immich_mobile/entities/album.entity.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/models/albums/album_search.model.dart'; -import 'package:immich_mobile/services/album.service.dart'; - -final isRefreshingRemoteAlbumProvider = StateProvider((ref) => false); - -class AlbumNotifier extends StateNotifier> { - AlbumNotifier(this.albumService, this.ref) : super([]) { - albumService.getAllRemoteAlbums().then((value) { - if (mounted) { - state = value; - } - }); - - _streamSub = albumService.watchRemoteAlbums().listen((data) => state = data); - } - - final AlbumService albumService; - final Ref ref; - late final StreamSubscription> _streamSub; - - Future refreshRemoteAlbums() async { - ref.read(isRefreshingRemoteAlbumProvider.notifier).state = true; - await albumService.refreshRemoteAlbums(); - ref.read(isRefreshingRemoteAlbumProvider.notifier).state = false; - } - - Future refreshDeviceAlbums() => albumService.refreshDeviceAlbums(); - - Future deleteAlbum(Album album) => albumService.deleteAlbum(album); - - Future createAlbum(String albumTitle, Set assets) => albumService.createAlbum(albumTitle, assets, []); - - Future getAlbumByName(String albumName, {bool? remote, bool? shared, bool? owner}) => - albumService.getAlbumByName(albumName, remote: remote, shared: shared, owner: owner); - - /// Create an album on the server with the same name as the selected album for backup - /// First this will check if the album already exists on the server with name - /// If it does not exist, it will create the album on the server - Future createSyncAlbum(String albumName) async { - final album = await getAlbumByName(albumName, remote: true, owner: true); - if (album != null) { - return; - } - - await createAlbum(albumName, {}); - } - - Future 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 addUsers(Album album, List userIds) async { - await albumService.addUsers(album, userIds); - } - - Future removeUser(Album album, UserDto 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 addAssets(Album album, Iterable assets) async { - await albumService.addAssets(album, assets); - } - - Future removeAsset(Album album, Iterable assets) async { - return await albumService.removeAsset(album, assets); - } - - Future setActivitystatus(Album album, bool enabled) { - return albumService.setActivityStatus(album, enabled); - } - - Future toggleSortOrder(Album album) { - final order = album.sortOrder == SortOrder.asc ? SortOrder.desc : SortOrder.asc; - - return albumService.updateSortOrder(album, order); - } - - @override - void dispose() { - _streamSub.cancel(); - super.dispose(); - } -} - -final albumProvider = StateNotifierProvider.autoDispose>((ref) { - return AlbumNotifier(ref.watch(albumServiceProvider), ref); -}); - -final albumWatcher = StreamProvider.autoDispose.family((ref, id) async* { - final albumService = ref.watch(albumServiceProvider); - - final album = await albumService.getAlbumById(id); - if (album != null) { - yield album; - } - - await for (final album in albumService.watchAlbum(id)) { - if (album != null) { - yield album; - } - } -}); - -class LocalAlbumsNotifier extends StateNotifier> { - LocalAlbumsNotifier(this.albumService) : super([]) { - albumService.getAllLocalAlbums().then((value) { - if (mounted) { - state = value; - } - }); - - _streamSub = albumService.watchLocalAlbums().listen((data) => state = data); - } - - final AlbumService albumService; - late final StreamSubscription> _streamSub; - - @override - void dispose() { - _streamSub.cancel(); - super.dispose(); - } -} - -final localAlbumsProvider = StateNotifierProvider.autoDispose>((ref) { - return LocalAlbumsNotifier(ref.watch(albumServiceProvider)); -}); diff --git a/mobile/lib/providers/album/album_sort_by_options.provider.dart b/mobile/lib/providers/album/album_sort_by_options.provider.dart index c969dbd37d..ec4ae71d03 100644 --- a/mobile/lib/providers/album/album_sort_by_options.provider.dart +++ b/mobile/lib/providers/album/album_sort_by_options.provider.dart @@ -1,119 +1,19 @@ -import 'package:collection/collection.dart'; import 'package:immich_mobile/constants/enums.dart'; -import 'package:immich_mobile/providers/app_settings.provider.dart'; -import 'package:immich_mobile/services/app_settings.service.dart'; -import 'package:immich_mobile/entities/album.entity.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; - -part 'album_sort_by_options.provider.g.dart'; - -typedef AlbumSortFn = List Function(List albums, bool isReverse); - -class _AlbumSortHandlers { - const _AlbumSortHandlers._(); - - static const AlbumSortFn created = _sortByCreated; - static List _sortByCreated(List albums, bool isReverse) { - final sorted = albums.sortedBy((album) => album.createdAt); - return (isReverse ? sorted.reversed : sorted).toList(); - } - - static const AlbumSortFn title = _sortByTitle; - static List _sortByTitle(List albums, bool isReverse) { - final sorted = albums.sortedBy((album) => album.name); - return (isReverse ? sorted.reversed : sorted).toList(); - } - - static const AlbumSortFn lastModified = _sortByLastModified; - static List _sortByLastModified(List albums, bool isReverse) { - final sorted = albums.sortedBy((album) => album.modifiedAt); - return (isReverse ? sorted.reversed : sorted).toList(); - } - - static const AlbumSortFn assetCount = _sortByAssetCount; - static List _sortByAssetCount(List albums, bool isReverse) { - final sorted = albums.sorted((a, b) => a.assetCount.compareTo(b.assetCount)); - return (isReverse ? sorted.reversed : sorted).toList(); - } - - static const AlbumSortFn mostRecent = _sortByMostRecent; - static List _sortByMostRecent(List albums, bool isReverse) { - final sorted = albums.sorted((a, b) { - if (a.endDate == null && b.endDate == null) { - return 0; - } - - if (a.endDate == null) { - // Put nulls at the end for recent sorting - return 1; - } - - if (b.endDate == null) { - return -1; - } - - // Sort by descending recent date - return b.endDate!.compareTo(a.endDate!); - }); - return (isReverse ? sorted.reversed : sorted).toList(); - } - - static const AlbumSortFn mostOldest = _sortByMostOldest; - static List _sortByMostOldest(List albums, bool isReverse) { - final sorted = albums.sorted((a, b) { - if (a.startDate != null && b.startDate != null) { - return a.startDate!.compareTo(b.startDate!); - } - if (a.startDate == null) return 1; - if (b.startDate == null) return -1; - return 0; - }); - return (isReverse ? sorted.reversed : sorted).toList(); - } -} // Store index allows us to re-arrange the values without affecting the saved prefs enum AlbumSortMode { - title(1, "library_page_sort_title", _AlbumSortHandlers.title, SortOrder.asc), - assetCount(4, "library_page_sort_asset_count", _AlbumSortHandlers.assetCount, SortOrder.desc), - lastModified(3, "library_page_sort_last_modified", _AlbumSortHandlers.lastModified, SortOrder.desc), - created(0, "library_page_sort_created", _AlbumSortHandlers.created, SortOrder.desc), - mostRecent(2, "sort_recent", _AlbumSortHandlers.mostRecent, SortOrder.desc), - mostOldest(5, "sort_oldest", _AlbumSortHandlers.mostOldest, SortOrder.asc); + title(1, "library_page_sort_title", SortOrder.asc), + assetCount(4, "library_page_sort_asset_count", SortOrder.desc), + lastModified(3, "library_page_sort_last_modified", SortOrder.desc), + created(0, "library_page_sort_created", SortOrder.desc), + mostRecent(2, "sort_recent", SortOrder.desc), + mostOldest(5, "sort_oldest", SortOrder.asc); final int storeIndex; final String label; - final AlbumSortFn sortFn; final SortOrder defaultOrder; - const AlbumSortMode(this.storeIndex, this.label, this.sortFn, this.defaultOrder); + const AlbumSortMode(this.storeIndex, this.label, this.defaultOrder); SortOrder effectiveOrder(bool isReverse) => isReverse ? defaultOrder.reverse() : defaultOrder; } - -@riverpod -class AlbumSortByOptions extends _$AlbumSortByOptions { - @override - AlbumSortMode build() { - final sortOpt = ref.watch(appSettingsServiceProvider).getSetting(AppSettingsEnum.selectedAlbumSortOrder); - return AlbumSortMode.values.firstWhere((e) => e.storeIndex == sortOpt, orElse: () => AlbumSortMode.title); - } - - void changeSortMode(AlbumSortMode sortOption) { - state = sortOption; - ref.watch(appSettingsServiceProvider).setSetting(AppSettingsEnum.selectedAlbumSortOrder, sortOption.storeIndex); - } -} - -@riverpod -class AlbumSortOrder extends _$AlbumSortOrder { - @override - bool build() { - return ref.watch(appSettingsServiceProvider).getSetting(AppSettingsEnum.selectedAlbumSortReverse); - } - - void changeSortDirection(bool isReverse) { - state = isReverse; - ref.watch(appSettingsServiceProvider).setSetting(AppSettingsEnum.selectedAlbumSortReverse, isReverse); - } -} diff --git a/mobile/lib/providers/album/album_sort_by_options.provider.g.dart b/mobile/lib/providers/album/album_sort_by_options.provider.g.dart deleted file mode 100644 index 750329c9d5..0000000000 --- a/mobile/lib/providers/album/album_sort_by_options.provider.g.dart +++ /dev/null @@ -1,43 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'album_sort_by_options.provider.dart'; - -// ************************************************************************** -// RiverpodGenerator -// ************************************************************************** - -String _$albumSortByOptionsHash() => - r'dd8da5e730af555de1b86c3b157b6c93183523ac'; - -/// See also [AlbumSortByOptions]. -@ProviderFor(AlbumSortByOptions) -final albumSortByOptionsProvider = - AutoDisposeNotifierProvider.internal( - AlbumSortByOptions.new, - name: r'albumSortByOptionsProvider', - debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') - ? null - : _$albumSortByOptionsHash, - dependencies: null, - allTransitiveDependencies: null, - ); - -typedef _$AlbumSortByOptions = AutoDisposeNotifier; -String _$albumSortOrderHash() => r'573dea45b4519e69386fc7104c72522e35713440'; - -/// See also [AlbumSortOrder]. -@ProviderFor(AlbumSortOrder) -final albumSortOrderProvider = - AutoDisposeNotifierProvider.internal( - AlbumSortOrder.new, - name: r'albumSortOrderProvider', - debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') - ? null - : _$albumSortOrderHash, - dependencies: null, - allTransitiveDependencies: null, - ); - -typedef _$AlbumSortOrder = AutoDisposeNotifier; -// ignore_for_file: type=lint -// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/mobile/lib/providers/album/album_viewer.provider.dart b/mobile/lib/providers/album/album_viewer.provider.dart deleted file mode 100644 index f4ce047464..0000000000 --- a/mobile/lib/providers/album/album_viewer.provider.dart +++ /dev/null @@ -1,74 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/album.entity.dart'; -import 'package:immich_mobile/models/albums/album_viewer_page_state.model.dart'; -import 'package:immich_mobile/services/album.service.dart'; - -class AlbumViewerNotifier extends StateNotifier { - AlbumViewerNotifier(this.ref) - : super(const AlbumViewerPageState(editTitleText: "", isEditAlbum: false, editDescriptionText: "")); - - final Ref ref; - - void enableEditAlbum() { - state = state.copyWith(isEditAlbum: true); - } - - void disableEditAlbum() { - state = state.copyWith(isEditAlbum: false); - } - - void setEditTitleText(String newTitle) { - state = state.copyWith(editTitleText: newTitle); - } - - void setEditDescriptionText(String newDescription) { - state = state.copyWith(editDescriptionText: newDescription); - } - - void remoteEditTitleText() { - state = state.copyWith(editTitleText: ""); - } - - void remoteEditDescriptionText() { - state = state.copyWith(editDescriptionText: ""); - } - - void resetState() { - state = state.copyWith(editTitleText: "", isEditAlbum: false, editDescriptionText: ""); - } - - Future changeAlbumTitle(Album album, String newAlbumTitle) async { - AlbumService service = ref.watch(albumServiceProvider); - - bool isSuccess = await service.changeTitleAlbum(album, newAlbumTitle); - - if (isSuccess) { - state = state.copyWith(editTitleText: "", isEditAlbum: false); - - return true; - } - - state = state.copyWith(editTitleText: "", isEditAlbum: false); - return false; - } - - Future changeAlbumDescription(Album album, String newAlbumDescription) async { - AlbumService service = ref.watch(albumServiceProvider); - - bool isSuccess = await service.changeDescriptionAlbum(album, newAlbumDescription); - - if (isSuccess) { - state = state.copyWith(editDescriptionText: "", isEditAlbum: false); - - return true; - } - - state = state.copyWith(editDescriptionText: "", isEditAlbum: false); - - return false; - } -} - -final albumViewerProvider = StateNotifierProvider((ref) { - return AlbumViewerNotifier(ref); -}); diff --git a/mobile/lib/providers/album/current_album.provider.dart b/mobile/lib/providers/album/current_album.provider.dart deleted file mode 100644 index bd22c7a7cd..0000000000 --- a/mobile/lib/providers/album/current_album.provider.dart +++ /dev/null @@ -1,15 +0,0 @@ -import 'package:immich_mobile/entities/album.entity.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; - -part 'current_album.provider.g.dart'; - -@riverpod -class CurrentAlbum extends _$CurrentAlbum { - @override - Album? build() => null; - - void set(Album? a) => state = a; -} - -/// Mock class for testing -abstract class CurrentAlbumInternal extends _$CurrentAlbum {} diff --git a/mobile/lib/providers/album/suggested_shared_users.provider.dart b/mobile/lib/providers/album/suggested_shared_users.provider.dart deleted file mode 100644 index 51146748c7..0000000000 --- a/mobile/lib/providers/album/suggested_shared_users.provider.dart +++ /dev/null @@ -1,14 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/domain/models/user.model.dart'; -import 'package:immich_mobile/domain/services/user.service.dart'; -import 'package:immich_mobile/providers/infrastructure/user.provider.dart'; -import 'package:immich_mobile/providers/user.provider.dart'; - -final otherUsersProvider = FutureProvider.autoDispose>((ref) async { - UserService userService = ref.watch(userServiceProvider); - final currentUser = ref.watch(currentUserProvider); - - final allUsers = await userService.getAll(); - allUsers.removeWhere((u) => currentUser?.id == u.id); - return allUsers; -}); diff --git a/mobile/lib/providers/app_life_cycle.provider.dart b/mobile/lib/providers/app_life_cycle.provider.dart index 68007f283a..a5f67215a8 100644 --- a/mobile/lib/providers/app_life_cycle.provider.dart +++ b/mobile/lib/providers/app_life_cycle.provider.dart @@ -5,28 +5,17 @@ import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/domain/services/log.service.dart'; import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/extensions/platform_extensions.dart'; -import 'package:immich_mobile/models/backup/backup_state.model.dart'; -import 'package:immich_mobile/providers/album/album.provider.dart'; import 'package:immich_mobile/providers/app_settings.provider.dart'; -import 'package:immich_mobile/providers/asset.provider.dart'; import 'package:immich_mobile/providers/auth.provider.dart'; import 'package:immich_mobile/providers/background_sync.provider.dart'; -import 'package:immich_mobile/providers/backup/backup.provider.dart'; import 'package:immich_mobile/providers/backup/drift_backup.provider.dart'; -import 'package:immich_mobile/providers/backup/ios_background_settings.provider.dart'; -import 'package:immich_mobile/providers/backup/manual_upload.provider.dart'; import 'package:immich_mobile/providers/gallery_permission.provider.dart'; import 'package:immich_mobile/providers/infrastructure/platform.provider.dart'; -import 'package:immich_mobile/providers/memory.provider.dart'; import 'package:immich_mobile/providers/notification_permission.provider.dart'; import 'package:immich_mobile/providers/server_info.provider.dart'; -import 'package:immich_mobile/providers/tab.provider.dart'; import 'package:immich_mobile/providers/websocket.provider.dart'; import 'package:immich_mobile/services/app_settings.service.dart'; -import 'package:immich_mobile/services/background.service.dart'; -import 'package:isar/isar.dart'; import 'package:logging/logging.dart'; -import 'package:permission_handler/permission_handler.dart'; enum AppLifeCycleEnum { active, inactive, paused, resumed, detached, hidden } @@ -87,43 +76,15 @@ class AppLifeCycleNotifier extends StateNotifier { final endpoint = await _ref.read(authProvider.notifier).setOpenApiServiceEndpoint(); _log.info("Using server URL: $endpoint"); - if (!Store.isBetaTimelineEnabled) { - final permission = _ref.watch(galleryPermissionNotifier); - if (permission.isGranted || permission.isLimited) { - await _ref.read(backupProvider.notifier).resumeBackup(); - await _ref.read(backgroundServiceProvider).resumeServiceIfEnabled(); - } - } - await _ref.read(serverInfoProvider.notifier).getServerVersion(); } - if (!Store.isBetaTimelineEnabled) { - switch (_ref.read(tabProvider)) { - case TabEnum.home: - await _ref.read(assetProvider.notifier).getAllAsset(); - - case TabEnum.albums: - await _ref.read(albumProvider.notifier).refreshRemoteAlbums(); - - case TabEnum.library: - case TabEnum.search: - break; - } - } else { - _ref.read(websocketProvider.notifier).connect(); - await _handleBetaTimelineResume(); - } + _ref.read(websocketProvider.notifier).connect(); + await _handleBetaTimelineResume(); await _ref.read(notificationPermissionProvider.notifier).getNotificationPermission(); await _ref.read(galleryPermissionNotifier.notifier).getGalleryPermissionStatus(); - - if (!Store.isBetaTimelineEnabled) { - await _ref.read(iOSBackgroundSettingsProvider.notifier).refresh(); - - _ref.invalidate(memoryFutureProvider); - } } Future _safeRun(Future action, String debugName) async { @@ -139,7 +100,6 @@ class AppLifeCycleNotifier extends StateNotifier { } Future _handleBetaTimelineResume() async { - _ref.read(backupProvider.notifier).cancelBackup(); unawaited(_ref.read(backgroundWorkerLockServiceProvider).lock()); // Give isolates time to complete any ongoing database transactions @@ -218,9 +178,7 @@ class AppLifeCycleNotifier extends StateNotifier { _pauseOperation = Completer(); try { - if (Store.isBetaTimelineEnabled) { - unawaited(_ref.read(backgroundWorkerLockServiceProvider).unlock()); - } + unawaited(_ref.read(backgroundWorkerLockServiceProvider).unlock()); await _performPause(); } catch (e, stackTrace) { _log.severe("Error during app pause", e, stackTrace); @@ -234,14 +192,7 @@ class AppLifeCycleNotifier extends StateNotifier { Future _performPause() { if (_ref.read(authProvider).isAuthenticated) { - if (!Store.isBetaTimelineEnabled) { - // Do not cancel backup if manual upload is in progress - if (_ref.read(backupProvider.notifier).backupProgress != BackUpProgressEnum.manualInProgress) { - _ref.read(backupProvider.notifier).cancelBackup(); - } - } else { - _ref.read(driftBackupProvider.notifier).stopForegroundBackup(); - } + _ref.read(driftBackupProvider.notifier).stopForegroundBackup(); _ref.read(websocketProvider.notifier).disconnect(); } @@ -252,31 +203,12 @@ class AppLifeCycleNotifier extends StateNotifier { Future handleAppDetached() async { state = AppLifeCycleEnum.detached; - if (Store.isBetaTimelineEnabled) { - unawaited(_ref.read(backgroundWorkerLockServiceProvider).unlock()); - } + unawaited(_ref.read(backgroundWorkerLockServiceProvider).unlock()); // Flush logs before closing database try { await LogService.I.flush(); } catch (_) {} - - // Close Isar database safely - try { - final isar = Isar.getInstance(); - if (isar != null && isar.isOpen) { - await isar.close(); - } - } catch (_) {} - - if (Store.isBetaTimelineEnabled) { - return; - } - - // no guarantee this is called at all - try { - _ref.read(manualUploadProvider.notifier).cancelBackup(); - } catch (_) {} } void handleAppHidden() { diff --git a/mobile/lib/providers/asset.provider.dart b/mobile/lib/providers/asset.provider.dart deleted file mode 100644 index d5a4e42b74..0000000000 --- a/mobile/lib/providers/asset.provider.dart +++ /dev/null @@ -1,182 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/constants/enums.dart'; -import 'package:immich_mobile/domain/models/store.model.dart'; -import 'package:immich_mobile/domain/services/user.service.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/entities/store.entity.dart'; -import 'package:immich_mobile/providers/infrastructure/user.provider.dart'; -import 'package:immich_mobile/providers/memory.provider.dart'; -import 'package:immich_mobile/services/album.service.dart'; -import 'package:immich_mobile/services/asset.service.dart'; -import 'package:immich_mobile/services/etag.service.dart'; -import 'package:immich_mobile/services/exif.service.dart'; -import 'package:immich_mobile/services/sync.service.dart'; -import 'package:logging/logging.dart'; -import 'package:immich_mobile/utils/debug_print.dart'; - -final assetProvider = StateNotifierProvider((ref) { - return AssetNotifier( - ref.watch(assetServiceProvider), - ref.watch(albumServiceProvider), - ref.watch(userServiceProvider), - ref.watch(syncServiceProvider), - ref.watch(etagServiceProvider), - ref.watch(exifServiceProvider), - ref, - ); -}); - -class AssetNotifier extends StateNotifier { - final AssetService _assetService; - final AlbumService _albumService; - final UserService _userService; - final SyncService _syncService; - final ETagService _etagService; - final ExifService _exifService; - final Ref _ref; - final log = Logger('AssetNotifier'); - bool _getAllAssetInProgress = false; - bool _deleteInProgress = false; - - AssetNotifier( - this._assetService, - this._albumService, - this._userService, - this._syncService, - this._etagService, - this._exifService, - this._ref, - ) : super(false); - - Future getAllAsset({bool clear = false}) async { - if (_getAllAssetInProgress || _deleteInProgress) { - // guard against multiple calls to this method while it's still working - return; - } - final stopwatch = Stopwatch()..start(); - try { - _getAllAssetInProgress = true; - state = true; - if (clear) { - await clearAllAssets(); - log.info("Manual refresh requested, cleared assets and albums from db"); - } - final users = await _syncService.getUsersFromServer(); - bool changedUsers = false; - if (users != null) { - changedUsers = await _syncService.syncUsersFromServer(users); - } - final bool newRemote = await _assetService.refreshRemoteAssets(); - final bool newLocal = await _albumService.refreshDeviceAlbums(); - dPrint(() => "changedUsers: $changedUsers, newRemote: $newRemote, newLocal: $newLocal"); - if (newRemote) { - _ref.invalidate(memoryFutureProvider); - } - - log.info("Load assets: ${stopwatch.elapsedMilliseconds}ms"); - } catch (error) { - // If there is error in getting the remote assets, still showing the new local assets - await _albumService.refreshDeviceAlbums(); - } finally { - _getAllAssetInProgress = false; - if (mounted) { - state = false; - } - } - } - - Future clearAllAssets() async { - await Store.delete(StoreKey.assetETag); - await Future.wait([ - _assetService.clearTable(), - _exifService.clearTable(), - _albumService.clearTable(), - _userService.deleteAll(), - _etagService.clearTable(), - ]); - } - - Future onNewAssetUploaded(Asset newAsset) async { - // eTag on device is not valid after partially modifying the assets - await Store.delete(StoreKey.assetETag); - await _syncService.syncNewAssetToDb(newAsset); - } - - Future deleteLocalAssets(List assets) async { - _deleteInProgress = true; - state = true; - try { - await _assetService.deleteLocalAssets(assets); - return true; - } catch (error) { - log.severe("Failed to delete local assets", error); - return false; - } finally { - _deleteInProgress = false; - state = false; - } - } - - /// Delete remote asset only - /// - /// Default behavior is trashing the asset - Future deleteRemoteAssets(Iterable deleteAssets, {bool shouldDeletePermanently = false}) async { - _deleteInProgress = true; - state = true; - try { - await _assetService.deleteRemoteAssets(deleteAssets, shouldDeletePermanently: shouldDeletePermanently); - return true; - } catch (error) { - log.severe("Failed to delete remote assets", error); - return false; - } finally { - _deleteInProgress = false; - state = false; - } - } - - Future deleteAssets(Iterable deleteAssets, {bool force = false}) async { - _deleteInProgress = true; - state = true; - try { - await _assetService.deleteAssets(deleteAssets, shouldDeletePermanently: force); - return true; - } catch (error) { - log.severe("Failed to delete assets", error); - return false; - } finally { - _deleteInProgress = false; - state = false; - } - } - - Future toggleFavorite(List assets, [bool? status]) { - status ??= !assets.every((a) => a.isFavorite); - return _assetService.changeFavoriteStatus(assets, status); - } - - Future toggleArchive(List assets, [bool? status]) { - status ??= !assets.every((a) => a.isArchived); - return _assetService.changeArchiveStatus(assets, status); - } - - Future setLockedView(List selection, AssetVisibilityEnum visibility) { - return _assetService.setVisibility(selection, visibility); - } -} - -final assetDetailProvider = StreamProvider.autoDispose.family((ref, asset) async* { - final assetService = ref.watch(assetServiceProvider); - yield await assetService.loadExif(asset); - - await for (final asset in assetService.watchAsset(asset.id)) { - if (asset != null) { - yield await ref.watch(assetServiceProvider).loadExif(asset); - } - } -}); - -final assetWatcher = StreamProvider.autoDispose.family((ref, asset) { - final assetService = ref.watch(assetServiceProvider); - return assetService.watchAsset(asset.id, fireImmediately: true); -}); diff --git a/mobile/lib/providers/asset_viewer/asset_people.provider.dart b/mobile/lib/providers/asset_viewer/asset_people.provider.dart deleted file mode 100644 index e2227920c7..0000000000 --- a/mobile/lib/providers/asset_viewer/asset_people.provider.dart +++ /dev/null @@ -1,49 +0,0 @@ -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/services/asset.service.dart'; -import 'package:logging/logging.dart'; -import 'package:openapi/api.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; - -part 'asset_people.provider.g.dart'; - -/// Maintains the list of people for an asset. -@riverpod -class AssetPeopleNotifier extends _$AssetPeopleNotifier { - final log = Logger('AssetPeopleNotifier'); - - @override - Future> build(Asset asset) async { - if (!asset.isRemote) { - return []; - } - - final list = await ref.watch(assetServiceProvider).getRemotePeopleOfAsset(asset.remoteId!); - if (list == null) { - return []; - } - - // explicitly a sorted slice to make it deterministic - // named people will be at the beginning, and names are sorted - // ascendingly - list.sort((a, b) { - final aNotEmpty = a.name.isNotEmpty; - final bNotEmpty = b.name.isNotEmpty; - if (aNotEmpty && !bNotEmpty) { - return -1; - } else if (!aNotEmpty && bNotEmpty) { - return 1; - } else if (!aNotEmpty && !bNotEmpty) { - return 0; - } - - return a.name.compareTo(b.name); - }); - return list; - } - - Future refresh() async { - // invalidate the state – this way we don't have to - // duplicate the code from build. - ref.invalidateSelf(); - } -} diff --git a/mobile/lib/providers/asset_viewer/asset_people.provider.g.dart b/mobile/lib/providers/asset_viewer/asset_people.provider.g.dart deleted file mode 100644 index 031a70e0d9..0000000000 --- a/mobile/lib/providers/asset_viewer/asset_people.provider.g.dart +++ /dev/null @@ -1,192 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'asset_people.provider.dart'; - -// ************************************************************************** -// RiverpodGenerator -// ************************************************************************** - -String _$assetPeopleNotifierHash() => - r'9835b180984a750c91e923e7b64dbda94f6d7574'; - -/// Copied from Dart SDK -class _SystemHash { - _SystemHash._(); - - static int combine(int hash, int value) { - // ignore: parameter_assignments - hash = 0x1fffffff & (hash + value); - // ignore: parameter_assignments - hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10)); - return hash ^ (hash >> 6); - } - - static int finish(int hash) { - // ignore: parameter_assignments - hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3)); - // ignore: parameter_assignments - hash = hash ^ (hash >> 11); - return 0x1fffffff & (hash + ((0x00003fff & hash) << 15)); - } -} - -abstract class _$AssetPeopleNotifier - extends - BuildlessAutoDisposeAsyncNotifier> { - late final Asset asset; - - FutureOr> build(Asset asset); -} - -/// Maintains the list of people for an asset. -/// -/// Copied from [AssetPeopleNotifier]. -@ProviderFor(AssetPeopleNotifier) -const assetPeopleNotifierProvider = AssetPeopleNotifierFamily(); - -/// Maintains the list of people for an asset. -/// -/// Copied from [AssetPeopleNotifier]. -class AssetPeopleNotifierFamily - extends Family>> { - /// Maintains the list of people for an asset. - /// - /// Copied from [AssetPeopleNotifier]. - const AssetPeopleNotifierFamily(); - - /// Maintains the list of people for an asset. - /// - /// Copied from [AssetPeopleNotifier]. - AssetPeopleNotifierProvider call(Asset asset) { - return AssetPeopleNotifierProvider(asset); - } - - @override - AssetPeopleNotifierProvider getProviderOverride( - covariant AssetPeopleNotifierProvider provider, - ) { - return call(provider.asset); - } - - static const Iterable? _dependencies = null; - - @override - Iterable? get dependencies => _dependencies; - - static const Iterable? _allTransitiveDependencies = null; - - @override - Iterable? get allTransitiveDependencies => - _allTransitiveDependencies; - - @override - String? get name => r'assetPeopleNotifierProvider'; -} - -/// Maintains the list of people for an asset. -/// -/// Copied from [AssetPeopleNotifier]. -class AssetPeopleNotifierProvider - extends - AutoDisposeAsyncNotifierProviderImpl< - AssetPeopleNotifier, - List - > { - /// Maintains the list of people for an asset. - /// - /// Copied from [AssetPeopleNotifier]. - AssetPeopleNotifierProvider(Asset asset) - : this._internal( - () => AssetPeopleNotifier()..asset = asset, - from: assetPeopleNotifierProvider, - name: r'assetPeopleNotifierProvider', - debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') - ? null - : _$assetPeopleNotifierHash, - dependencies: AssetPeopleNotifierFamily._dependencies, - allTransitiveDependencies: - AssetPeopleNotifierFamily._allTransitiveDependencies, - asset: asset, - ); - - AssetPeopleNotifierProvider._internal( - super._createNotifier, { - required super.name, - required super.dependencies, - required super.allTransitiveDependencies, - required super.debugGetCreateSourceHash, - required super.from, - required this.asset, - }) : super.internal(); - - final Asset asset; - - @override - FutureOr> runNotifierBuild( - covariant AssetPeopleNotifier notifier, - ) { - return notifier.build(asset); - } - - @override - Override overrideWith(AssetPeopleNotifier Function() create) { - return ProviderOverride( - origin: this, - override: AssetPeopleNotifierProvider._internal( - () => create()..asset = asset, - from: from, - name: null, - dependencies: null, - allTransitiveDependencies: null, - debugGetCreateSourceHash: null, - asset: asset, - ), - ); - } - - @override - AutoDisposeAsyncNotifierProviderElement< - AssetPeopleNotifier, - List - > - createElement() { - return _AssetPeopleNotifierProviderElement(this); - } - - @override - bool operator ==(Object other) { - return other is AssetPeopleNotifierProvider && other.asset == asset; - } - - @override - int get hashCode { - var hash = _SystemHash.combine(0, runtimeType.hashCode); - hash = _SystemHash.combine(hash, asset.hashCode); - - return _SystemHash.finish(hash); - } -} - -@Deprecated('Will be removed in 3.0. Use Ref instead') -// ignore: unused_element -mixin AssetPeopleNotifierRef - on AutoDisposeAsyncNotifierProviderRef> { - /// The parameter `asset` of this provider. - Asset get asset; -} - -class _AssetPeopleNotifierProviderElement - extends - AutoDisposeAsyncNotifierProviderElement< - AssetPeopleNotifier, - List - > - with AssetPeopleNotifierRef { - _AssetPeopleNotifierProviderElement(super.provider); - - @override - Asset get asset => (origin as AssetPeopleNotifierProvider).asset; -} - -// ignore_for_file: type=lint -// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/mobile/lib/providers/asset_viewer/asset_stack.provider.dart b/mobile/lib/providers/asset_viewer/asset_stack.provider.dart deleted file mode 100644 index 8772e3d0cb..0000000000 --- a/mobile/lib/providers/asset_viewer/asset_stack.provider.dart +++ /dev/null @@ -1,42 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/services/asset.service.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; - -part 'asset_stack.provider.g.dart'; - -class AssetStackNotifier extends StateNotifier> { - final AssetService assetService; - final String _stackId; - - AssetStackNotifier(this.assetService, this._stackId) : super([]) { - _fetchStack(_stackId); - } - - void _fetchStack(String stackId) async { - if (!mounted) { - return; - } - - final stack = await assetService.getStackAssets(stackId); - if (stack.isNotEmpty) { - state = stack; - } - } - - void removeChild(int index) { - if (index < state.length) { - state.removeAt(index); - state = List.from(state); - } - } -} - -final assetStackStateProvider = StateNotifierProvider.autoDispose.family, String>( - (ref, stackId) => AssetStackNotifier(ref.watch(assetServiceProvider), stackId), -); - -@riverpod -int assetStackIndex(Ref _) { - return -1; -} diff --git a/mobile/lib/providers/asset_viewer/asset_stack.provider.g.dart b/mobile/lib/providers/asset_viewer/asset_stack.provider.g.dart deleted file mode 100644 index dcf82cdebd..0000000000 --- a/mobile/lib/providers/asset_viewer/asset_stack.provider.g.dart +++ /dev/null @@ -1,27 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'asset_stack.provider.dart'; - -// ************************************************************************** -// RiverpodGenerator -// ************************************************************************** - -String _$assetStackIndexHash() => r'086ddb782e3eb38b80d755666fe35be8fe7322d7'; - -/// See also [assetStackIndex]. -@ProviderFor(assetStackIndex) -final assetStackIndexProvider = AutoDisposeProvider.internal( - assetStackIndex, - name: r'assetStackIndexProvider', - debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') - ? null - : _$assetStackIndexHash, - dependencies: null, - allTransitiveDependencies: null, -); - -@Deprecated('Will be removed in 3.0. Use Ref instead') -// ignore: unused_element -typedef AssetStackIndexRef = AutoDisposeProviderRef; -// ignore_for_file: type=lint -// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/mobile/lib/providers/asset_viewer/current_asset.provider.dart b/mobile/lib/providers/asset_viewer/current_asset.provider.dart deleted file mode 100644 index 0e25660ab0..0000000000 --- a/mobile/lib/providers/asset_viewer/current_asset.provider.dart +++ /dev/null @@ -1,15 +0,0 @@ -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; - -part 'current_asset.provider.g.dart'; - -@riverpod -class CurrentAsset extends _$CurrentAsset { - @override - Asset? build() => null; - - void set(Asset? a) => state = a; -} - -/// Mock class for testing -abstract class CurrentAssetInternal extends _$CurrentAsset {} diff --git a/mobile/lib/providers/asset_viewer/current_asset.provider.g.dart b/mobile/lib/providers/asset_viewer/current_asset.provider.g.dart deleted file mode 100644 index e0d8d47d3a..0000000000 --- a/mobile/lib/providers/asset_viewer/current_asset.provider.g.dart +++ /dev/null @@ -1,26 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'current_asset.provider.dart'; - -// ************************************************************************** -// RiverpodGenerator -// ************************************************************************** - -String _$currentAssetHash() => r'2def10ea594152c984ae2974d687ab6856d7bdd0'; - -/// See also [CurrentAsset]. -@ProviderFor(CurrentAsset) -final currentAssetProvider = - AutoDisposeNotifierProvider.internal( - CurrentAsset.new, - name: r'currentAssetProvider', - debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') - ? null - : _$currentAssetHash, - dependencies: null, - allTransitiveDependencies: null, - ); - -typedef _$CurrentAsset = AutoDisposeNotifier; -// ignore_for_file: type=lint -// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/mobile/lib/providers/asset_viewer/download.provider.dart b/mobile/lib/providers/asset_viewer/download.provider.dart index a461d5766a..25db76b077 100644 --- a/mobile/lib/providers/asset_viewer/download.provider.dart +++ b/mobile/lib/providers/asset_viewer/download.provider.dart @@ -1,26 +1,15 @@ import 'dart:async'; import 'package:background_downloader/background_downloader.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:fluttertoast/fluttertoast.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/models/download/download_state.model.dart'; import 'package:immich_mobile/models/download/livephotos_medatada.model.dart'; -import 'package:immich_mobile/services/album.service.dart'; import 'package:immich_mobile/services/download.service.dart'; -import 'package:immich_mobile/services/share.service.dart'; -import 'package:immich_mobile/widgets/common/immich_toast.dart'; -import 'package:immich_mobile/widgets/common/share_dialog.dart'; class DownloadStateNotifier extends StateNotifier { final DownloadService _downloadService; - final ShareService _shareService; - final AlbumService _albumService; - DownloadStateNotifier(this._downloadService, this._shareService, this._albumService) + DownloadStateNotifier(this._downloadService) : super( const DownloadState( downloadStatus: TaskStatus.complete, @@ -132,18 +121,9 @@ class DownloadStateNotifier extends StateNotifier { if (state.taskProgress.isEmpty) { state = state.copyWith(showProgress: false); } - _albumService.refreshDeviceAlbums(); }); } - Future> downloadAllAsset(List assets) async { - return await _downloadService.downloadAll(assets); - } - - void downloadAsset(Asset asset) async { - await _downloadService.download(asset); - } - void cancelDownload(String id) async { final isCanceled = await _downloadService.cancelDownload(id); @@ -159,36 +139,8 @@ class DownloadStateNotifier extends StateNotifier { state = state.copyWith(showProgress: false); } } - - void shareAsset(Asset asset, BuildContext context) async { - unawaited( - showDialog( - context: context, - builder: (BuildContext buildContext) { - _shareService.shareAsset(asset, context).then((bool status) { - if (!status) { - ImmichToast.show( - context: context, - msg: 'image_viewer_page_state_provider_share_error'.tr(), - toastType: ToastType.error, - gravity: ToastGravity.BOTTOM, - ); - } - buildContext.pop(); - }); - return const ShareDialog(); - }, - barrierDismissible: false, - useRootNavigator: false, - ), - ); - } } final downloadStateProvider = StateNotifierProvider( - ((ref) => DownloadStateNotifier( - ref.watch(downloadServiceProvider), - ref.watch(shareServiceProvider), - ref.watch(albumServiceProvider), - )), + ((ref) => DownloadStateNotifier(ref.watch(downloadServiceProvider))), ); diff --git a/mobile/lib/providers/asset_viewer/render_list_status_provider.dart b/mobile/lib/providers/asset_viewer/render_list_status_provider.dart deleted file mode 100644 index 189ac85452..0000000000 --- a/mobile/lib/providers/asset_viewer/render_list_status_provider.dart +++ /dev/null @@ -1,19 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; - -enum RenderListStatusEnum { complete, empty, error, loading } - -final renderListStatusProvider = StateNotifierProvider((ref) { - return RenderListStatus(ref); -}); - -class RenderListStatus extends StateNotifier { - RenderListStatus(this.ref) : super(RenderListStatusEnum.complete); - - final Ref ref; - - RenderListStatusEnum get status => state; - - set status(RenderListStatusEnum value) { - state = value; - } -} diff --git a/mobile/lib/providers/backup/backup.provider.dart b/mobile/lib/providers/backup/backup.provider.dart index 5f3ad3d058..a6dc272313 100644 --- a/mobile/lib/providers/backup/backup.provider.dart +++ b/mobile/lib/providers/backup/backup.provider.dart @@ -1,672 +1,23 @@ import 'dart:async'; -import 'dart:io'; -import 'package:collection/collection.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/domain/models/store.model.dart'; -import 'package:immich_mobile/entities/album.entity.dart'; -import 'package:immich_mobile/entities/backup_album.entity.dart'; -import 'package:immich_mobile/entities/store.entity.dart'; -import 'package:immich_mobile/models/auth/auth_state.model.dart'; -import 'package:immich_mobile/models/backup/available_album.model.dart'; -import 'package:immich_mobile/models/backup/backup_candidate.model.dart'; -import 'package:immich_mobile/models/backup/backup_state.model.dart'; -import 'package:immich_mobile/models/backup/current_upload_asset.model.dart'; -import 'package:immich_mobile/models/backup/error_upload_asset.model.dart'; -import 'package:immich_mobile/models/backup/success_upload_asset.model.dart'; import 'package:immich_mobile/models/server_info/server_disk_info.model.dart'; -import 'package:immich_mobile/providers/app_life_cycle.provider.dart'; -import 'package:immich_mobile/providers/auth.provider.dart'; -import 'package:immich_mobile/providers/backup/error_backup_list.provider.dart'; -import 'package:immich_mobile/providers/gallery_permission.provider.dart'; -import 'package:immich_mobile/repositories/album_media.repository.dart'; -import 'package:immich_mobile/repositories/backup.repository.dart'; -import 'package:immich_mobile/repositories/file_media.repository.dart'; -import 'package:immich_mobile/services/background.service.dart'; -import 'package:immich_mobile/services/backup.service.dart'; -import 'package:immich_mobile/services/backup_album.service.dart'; import 'package:immich_mobile/services/server_info.service.dart'; -import 'package:immich_mobile/utils/backup_progress.dart'; -import 'package:immich_mobile/utils/diff.dart'; -import 'package:logging/logging.dart'; -import 'package:permission_handler/permission_handler.dart'; -import 'package:photo_manager/photo_manager.dart' show PMProgressHandler; -import 'package:immich_mobile/utils/debug_print.dart'; -final backupProvider = StateNotifierProvider((ref) { - return BackupNotifier( - ref.watch(backupServiceProvider), - ref.watch(serverInfoServiceProvider), - ref.watch(authProvider), - ref.watch(backgroundServiceProvider), - ref.watch(galleryPermissionNotifier.notifier), - ref.watch(albumMediaRepositoryProvider), - ref.watch(fileMediaRepositoryProvider), - ref.watch(backupAlbumServiceProvider), - ref, - ); +final backupProvider = StateNotifierProvider((ref) { + return BackupNotifier(ref.watch(serverInfoServiceProvider)); }); -class BackupNotifier extends StateNotifier { - BackupNotifier( - this._backupService, - this._serverInfoService, - this._authState, - this._backgroundService, - this._galleryPermissionNotifier, - this._albumMediaRepository, - this._fileMediaRepository, - this._backupAlbumService, - this.ref, - ) : super( - BackUpState( - backupProgress: BackUpProgressEnum.idle, - allAssetsInDatabase: const [], - progressInPercentage: 0, - progressInFileSize: "0 B / 0 B", - progressInFileSpeed: 0, - progressInFileSpeeds: const [], - progressInFileSpeedUpdateTime: DateTime.now(), - progressInFileSpeedUpdateSentBytes: 0, - autoBackup: Store.get(StoreKey.autoBackup, false), - backgroundBackup: Store.get(StoreKey.backgroundBackup, false), - backupRequireWifi: Store.get(StoreKey.backupRequireWifi, true), - backupRequireCharging: Store.get(StoreKey.backupRequireCharging, false), - backupTriggerDelay: Store.get(StoreKey.backupTriggerDelay, 5000), - serverInfo: const ServerDiskInfo(diskAvailable: "0", diskSize: "0", diskUse: "0", diskUsagePercentage: 0), - availableAlbums: const [], - selectedBackupAlbums: const {}, - excludedBackupAlbums: const {}, - allUniqueAssets: const {}, - selectedAlbumsBackupAssetsIds: const {}, - currentUploadAsset: CurrentUploadAsset( - id: '...', - fileCreatedAt: DateTime.parse('2020-10-04'), - fileName: '...', - fileType: '...', - fileSize: 0, - iCloudAsset: false, - ), - iCloudDownloadProgress: 0.0, - ), - ); +class BackupNotifier extends StateNotifier { + BackupNotifier(this._serverInfoService) + : super(const ServerDiskInfo(diskAvailable: "0", diskSize: "0", diskUse: "0", diskUsagePercentage: 0)); - final log = Logger('BackupNotifier'); - final BackupService _backupService; final ServerInfoService _serverInfoService; - final AuthState _authState; - final BackgroundService _backgroundService; - final GalleryPermissionNotifier _galleryPermissionNotifier; - final AlbumMediaRepository _albumMediaRepository; - final FileMediaRepository _fileMediaRepository; - final BackupAlbumService _backupAlbumService; - final Ref ref; - Completer? _cancelToken; - - /// - /// UI INTERACTION - /// - /// Album selection - /// Due to the overlapping assets across multiple albums on the device - /// We have method to include and exclude albums - /// The total unique assets will be used for backing mechanism - /// - void addAlbumForBackup(AvailableAlbum album) { - if (state.excludedBackupAlbums.contains(album)) { - removeExcludedAlbumForBackup(album); - } - - state = state.copyWith(selectedBackupAlbums: {...state.selectedBackupAlbums, album}); - } - - void addExcludedAlbumForBackup(AvailableAlbum album) { - if (state.selectedBackupAlbums.contains(album)) { - removeAlbumForBackup(album); - } - state = state.copyWith(excludedBackupAlbums: {...state.excludedBackupAlbums, album}); - } - - void removeAlbumForBackup(AvailableAlbum album) { - Set currentSelectedAlbums = state.selectedBackupAlbums; - - currentSelectedAlbums.removeWhere((a) => a == album); - - state = state.copyWith(selectedBackupAlbums: currentSelectedAlbums); - } - - void removeExcludedAlbumForBackup(AvailableAlbum album) { - Set currentExcludedAlbums = state.excludedBackupAlbums; - - currentExcludedAlbums.removeWhere((a) => a == album); - - state = state.copyWith(excludedBackupAlbums: currentExcludedAlbums); - } - - Future backupAlbumSelectionDone() { - if (state.selectedBackupAlbums.isEmpty) { - // disable any backup - cancelBackup(); - setAutoBackup(false); - configureBackgroundBackup(enabled: false, onError: (msg) {}, onBatteryInfo: () {}); - } - return _updateBackupAssetCount(); - } - - void setAutoBackup(bool enabled) { - Store.put(StoreKey.autoBackup, enabled); - state = state.copyWith(autoBackup: enabled); - } - - void configureBackgroundBackup({ - bool? enabled, - bool? requireWifi, - bool? requireCharging, - int? triggerDelay, - required void Function(String msg) onError, - required void Function() onBatteryInfo, - }) async { - assert(enabled != null || requireWifi != null || requireCharging != null || triggerDelay != null); - final bool wasEnabled = state.backgroundBackup; - final bool wasWifi = state.backupRequireWifi; - final bool wasCharging = state.backupRequireCharging; - final int oldTriggerDelay = state.backupTriggerDelay; - state = state.copyWith( - backgroundBackup: enabled, - backupRequireWifi: requireWifi, - backupRequireCharging: requireCharging, - backupTriggerDelay: triggerDelay, - ); - - if (state.backgroundBackup) { - bool success = true; - if (!wasEnabled) { - if (!await _backgroundService.isIgnoringBatteryOptimizations()) { - onBatteryInfo(); - } - success &= await _backgroundService.enableService(immediate: true); - } - success &= - success && - await _backgroundService.configureService( - requireUnmetered: state.backupRequireWifi, - requireCharging: state.backupRequireCharging, - triggerUpdateDelay: state.backupTriggerDelay, - triggerMaxDelay: state.backupTriggerDelay * 10, - ); - if (success) { - await Store.put(StoreKey.backupRequireWifi, state.backupRequireWifi); - await Store.put(StoreKey.backupRequireCharging, state.backupRequireCharging); - await Store.put(StoreKey.backupTriggerDelay, state.backupTriggerDelay); - await Store.put(StoreKey.backgroundBackup, state.backgroundBackup); - } else { - state = state.copyWith( - backgroundBackup: wasEnabled, - backupRequireWifi: wasWifi, - backupRequireCharging: wasCharging, - backupTriggerDelay: oldTriggerDelay, - ); - onError("backup_controller_page_background_configure_error"); - } - } else { - final bool success = await _backgroundService.disableService(); - if (!success) { - state = state.copyWith(backgroundBackup: wasEnabled); - onError("backup_controller_page_background_configure_error"); - } - } - } - - /// - /// Get all album on the device - /// Get all selected and excluded album from the user's persistent storage - /// If this is the first time performing backup - set the default selected album to be - /// the one that has all assets (`Recent` on Android, `Recents` on iOS) - /// - Future _getBackupAlbumsInfo() async { - Stopwatch stopwatch = Stopwatch()..start(); - // Get all albums on the device - List availableAlbums = []; - List albums = await _albumMediaRepository.getAll(); - - // Map of id -> album for quick album lookup later on. - Map albumMap = {}; - - log.info('Found ${albums.length} local albums'); - - for (Album album in albums) { - AvailableAlbum availableAlbum = AvailableAlbum( - album: album, - assetCount: await ref.read(albumMediaRepositoryProvider).getAssetCount(album.localId!), - ); - - availableAlbums.add(availableAlbum); - - albumMap[album.localId!] = album; - } - state = state.copyWith(availableAlbums: availableAlbums); - - final List excludedBackupAlbums = await _backupAlbumService.getAllBySelection(BackupSelection.exclude); - final List selectedBackupAlbums = await _backupAlbumService.getAllBySelection(BackupSelection.select); - - final Set selectedAlbums = {}; - for (final BackupAlbum ba in selectedBackupAlbums) { - final albumAsset = albumMap[ba.id]; - - if (albumAsset != null) { - selectedAlbums.add( - AvailableAlbum( - album: albumAsset, - assetCount: await _albumMediaRepository.getAssetCount(albumAsset.localId!), - lastBackup: ba.lastBackup, - ), - ); - } else { - log.severe('Selected album not found'); - } - } - - final Set excludedAlbums = {}; - for (final BackupAlbum ba in excludedBackupAlbums) { - final albumAsset = albumMap[ba.id]; - - if (albumAsset != null) { - excludedAlbums.add( - AvailableAlbum( - album: albumAsset, - assetCount: await ref.read(albumMediaRepositoryProvider).getAssetCount(albumAsset.localId!), - lastBackup: ba.lastBackup, - ), - ); - } else { - log.severe('Excluded album not found'); - } - } - - state = state.copyWith(selectedBackupAlbums: selectedAlbums, excludedBackupAlbums: excludedAlbums); - - log.info("_getBackupAlbumsInfo: Found ${availableAlbums.length} available albums"); - dPrint(() => "_getBackupAlbumsInfo takes ${stopwatch.elapsedMilliseconds}ms"); - } - - /// - /// From all the selected and albums assets - /// Find the assets that are not overlapping between the two sets - /// Those assets are unique and are used as the total assets - /// - Future _updateBackupAssetCount() async { - // Save to persistent storage - await _updatePersistentAlbumsSelection(); - - final duplicatedAssetIds = await _backupService.getDuplicatedAssetIds(); - final Set assetsFromSelectedAlbums = {}; - final Set assetsFromExcludedAlbums = {}; - - for (final album in state.selectedBackupAlbums) { - final assetCount = await ref.read(albumMediaRepositoryProvider).getAssetCount(album.album.localId!); - - if (assetCount == 0) { - continue; - } - - final assets = await ref.read(albumMediaRepositoryProvider).getAssets(album.album.localId!); - - // Add album's name to the asset info - for (final asset in assets) { - List albumNames = [album.name]; - - final existingAsset = assetsFromSelectedAlbums.firstWhereOrNull((a) => a.asset.localId == asset.localId); - - if (existingAsset != null) { - albumNames.addAll(existingAsset.albumNames); - assetsFromSelectedAlbums.remove(existingAsset); - } - - assetsFromSelectedAlbums.add(BackupCandidate(asset: asset, albumNames: albumNames)); - } - } - - for (final album in state.excludedBackupAlbums) { - final assetCount = await ref.read(albumMediaRepositoryProvider).getAssetCount(album.album.localId!); - - if (assetCount == 0) { - continue; - } - - final assets = await ref.read(albumMediaRepositoryProvider).getAssets(album.album.localId!); - - for (final asset in assets) { - assetsFromExcludedAlbums.add(BackupCandidate(asset: asset, albumNames: [album.name])); - } - } - - final Set allUniqueAssets = assetsFromSelectedAlbums.difference(assetsFromExcludedAlbums); - - final allAssetsInDatabase = await _backupService.getDeviceBackupAsset(); - - if (allAssetsInDatabase == null) { - return; - } - - // Find asset that were backup from selected albums - final Set selectedAlbumsBackupAssets = Set.from(allUniqueAssets.map((e) => e.asset.localId)); - - selectedAlbumsBackupAssets.removeWhere((assetId) => !allAssetsInDatabase.contains(assetId)); - - // Remove duplicated asset from all unique assets - allUniqueAssets.removeWhere((candidate) => duplicatedAssetIds.contains(candidate.asset.localId)); - - if (allUniqueAssets.isEmpty) { - log.info("No assets are selected for back up"); - state = state.copyWith( - backupProgress: BackUpProgressEnum.idle, - allAssetsInDatabase: allAssetsInDatabase, - allUniqueAssets: {}, - selectedAlbumsBackupAssetsIds: selectedAlbumsBackupAssets, - ); - } else { - state = state.copyWith( - allAssetsInDatabase: allAssetsInDatabase, - allUniqueAssets: allUniqueAssets, - selectedAlbumsBackupAssetsIds: selectedAlbumsBackupAssets, - ); - } - } - - /// Get all necessary information for calculating the available albums, - /// which albums are selected or excluded - /// and then update the UI according to those information - Future getBackupInfo() async { - final isEnabled = await _backgroundService.isBackgroundBackupEnabled(); - - state = state.copyWith(backgroundBackup: isEnabled); - if (isEnabled != Store.get(StoreKey.backgroundBackup, !isEnabled)) { - await Store.put(StoreKey.backgroundBackup, isEnabled); - } - - if (state.backupProgress != BackUpProgressEnum.inBackground) { - await _getBackupAlbumsInfo(); - await updateDiskInfo(); - await _updateBackupAssetCount(); - } else { - log.warning("cannot get backup info - background backup is in progress!"); - } - } - - /// Save user selection of selected albums and excluded albums to database - Future _updatePersistentAlbumsSelection() async { - final epoch = DateTime.fromMillisecondsSinceEpoch(0, isUtc: true); - final selected = state.selectedBackupAlbums.map( - (e) => BackupAlbum(e.id, e.lastBackup ?? epoch, BackupSelection.select), - ); - final excluded = state.excludedBackupAlbums.map( - (e) => BackupAlbum(e.id, e.lastBackup ?? epoch, BackupSelection.exclude), - ); - final candidates = selected.followedBy(excluded).toList(); - candidates.sortBy((e) => e.id); - - final savedBackupAlbums = await _backupAlbumService.getAll(sort: BackupAlbumSort.id); - final List toDelete = []; - final List toUpsert = []; - - diffSortedListsSync( - savedBackupAlbums, - candidates, - compare: (BackupAlbum a, BackupAlbum b) => a.id.compareTo(b.id), - both: (BackupAlbum a, BackupAlbum b) { - b.lastBackup = a.lastBackup.isAfter(b.lastBackup) ? a.lastBackup : b.lastBackup; - toUpsert.add(b); - return true; - }, - onlyFirst: (BackupAlbum a) => toDelete.add(a.isarId), - onlySecond: (BackupAlbum b) => toUpsert.add(b), - ); - - await _backupAlbumService.deleteAll(toDelete); - await _backupAlbumService.updateAll(toUpsert); - } - - /// Invoke backup process - Future startBackupProcess() async { - dPrint(() => "Start backup process"); - assert(state.backupProgress == BackUpProgressEnum.idle); - state = state.copyWith(backupProgress: BackUpProgressEnum.inProgress); - - await getBackupInfo(); - - final hasPermission = _galleryPermissionNotifier.hasPermission; - if (hasPermission) { - await _fileMediaRepository.clearFileCache(); - - if (state.allUniqueAssets.isEmpty) { - log.info("No Asset On Device - Abort Backup Process"); - state = state.copyWith(backupProgress: BackUpProgressEnum.idle); - return; - } - - Set assetsWillBeBackup = Set.from(state.allUniqueAssets); - // Remove item that has already been backed up - for (final assetId in state.allAssetsInDatabase) { - assetsWillBeBackup.removeWhere((e) => e.asset.localId == assetId); - } - - if (assetsWillBeBackup.isEmpty) { - state = state.copyWith(backupProgress: BackUpProgressEnum.idle); - } - - // Perform Backup - _cancelToken?.complete(); - _cancelToken = Completer(); - - final pmProgressHandler = Platform.isIOS ? PMProgressHandler() : null; - - pmProgressHandler?.stream.listen((event) { - final double progress = event.progress; - state = state.copyWith(iCloudDownloadProgress: progress); - }); - - await _backupService.backupAsset( - assetsWillBeBackup, - _cancelToken!, - pmProgressHandler: pmProgressHandler, - onSuccess: _onAssetUploaded, - onProgress: _onUploadProgress, - onCurrentAsset: _onSetCurrentBackupAsset, - onError: _onBackupError, - ); - await notifyBackgroundServiceCanRun(); - } else { - await openAppSettings(); - } - } - - void setAvailableAlbums(availableAlbums) { - state = state.copyWith(availableAlbums: availableAlbums); - } - - void _onBackupError(ErrorUploadAsset errorAssetInfo) { - ref.watch(errorBackupListProvider.notifier).add(errorAssetInfo); - } - - void _onSetCurrentBackupAsset(CurrentUploadAsset currentUploadAsset) { - state = state.copyWith(currentUploadAsset: currentUploadAsset); - } - - void cancelBackup() { - if (state.backupProgress != BackUpProgressEnum.inProgress) { - notifyBackgroundServiceCanRun(); - } - _cancelToken?.complete(); - _cancelToken = null; - state = state.copyWith( - backupProgress: BackUpProgressEnum.idle, - progressInPercentage: 0.0, - progressInFileSize: "0 B / 0 B", - progressInFileSpeed: 0, - progressInFileSpeedUpdateTime: DateTime.now(), - progressInFileSpeedUpdateSentBytes: 0, - ); - } - - void _onAssetUploaded(SuccessUploadAsset result) async { - if (result.isDuplicate) { - state = state.copyWith( - allUniqueAssets: state.allUniqueAssets - .where((candidate) => candidate.asset.localId != result.candidate.asset.localId) - .toSet(), - ); - } else { - state = state.copyWith( - selectedAlbumsBackupAssetsIds: {...state.selectedAlbumsBackupAssetsIds, result.candidate.asset.localId!}, - allAssetsInDatabase: [...state.allAssetsInDatabase, result.candidate.asset.localId!], - ); - } - - if (state.allUniqueAssets.length - state.selectedAlbumsBackupAssetsIds.length == 0) { - final latestAssetBackup = state.allUniqueAssets - .map((candidate) => candidate.asset.fileModifiedAt) - .reduce((v, e) => e.isAfter(v) ? e : v); - state = state.copyWith( - selectedBackupAlbums: state.selectedBackupAlbums.map((e) => e.copyWith(lastBackup: latestAssetBackup)).toSet(), - excludedBackupAlbums: state.excludedBackupAlbums.map((e) => e.copyWith(lastBackup: latestAssetBackup)).toSet(), - backupProgress: BackUpProgressEnum.done, - progressInPercentage: 0.0, - progressInFileSize: "0 B / 0 B", - progressInFileSpeed: 0, - progressInFileSpeedUpdateTime: DateTime.now(), - progressInFileSpeedUpdateSentBytes: 0, - ); - await _updatePersistentAlbumsSelection(); - } - - await updateDiskInfo(); - } - - void _onUploadProgress(int sent, int total) { - double lastUploadSpeed = state.progressInFileSpeed; - List lastUploadSpeeds = state.progressInFileSpeeds.toList(); - DateTime lastUpdateTime = state.progressInFileSpeedUpdateTime; - int lastSentBytes = state.progressInFileSpeedUpdateSentBytes; - - final now = DateTime.now(); - final duration = now.difference(lastUpdateTime); - - // Keep the upload speed average span limited, to keep it somewhat relevant - if (lastUploadSpeeds.length > 10) { - lastUploadSpeeds.removeAt(0); - } - - if (duration.inSeconds > 0) { - lastUploadSpeeds.add(((sent - lastSentBytes) / duration.inSeconds).abs().roundToDouble()); - - lastUploadSpeed = lastUploadSpeeds.average.abs().roundToDouble(); - lastUpdateTime = now; - lastSentBytes = sent; - } - - state = state.copyWith( - progressInPercentage: (sent.toDouble() / total.toDouble() * 100), - progressInFileSize: humanReadableFileBytesProgress(sent, total), - progressInFileSpeed: lastUploadSpeed, - progressInFileSpeeds: lastUploadSpeeds, - progressInFileSpeedUpdateTime: lastUpdateTime, - progressInFileSpeedUpdateSentBytes: lastSentBytes, - ); - } Future updateDiskInfo() async { final diskInfo = await _serverInfoService.getDiskInfo(); - - // Update server info if (diskInfo != null) { - state = state.copyWith(serverInfo: diskInfo); + state = diskInfo; } } - - Future _resumeBackup() async { - // Check if user is login - final accessKey = Store.tryGet(StoreKey.accessToken); - - // User has been logged out return - if (accessKey == null || !_authState.isAuthenticated) { - log.info("[_resumeBackup] not authenticated - abort"); - return; - } - - // Check if this device is enable backup by the user - if (state.autoBackup) { - // check if backup is already in process - then return - if (state.backupProgress == BackUpProgressEnum.inProgress) { - log.info("[_resumeBackup] Auto Backup is already in progress - abort"); - return; - } - - if (state.backupProgress == BackUpProgressEnum.inBackground) { - log.info("[_resumeBackup] Background backup is running - abort"); - return; - } - - if (state.backupProgress == BackUpProgressEnum.manualInProgress) { - log.info("[_resumeBackup] Manual upload is running - abort"); - return; - } - - // Run backup - log.info("[_resumeBackup] Start back up"); - await startBackupProcess(); - } - return; - } - - Future resumeBackup() async { - final List selectedBackupAlbums = await _backupAlbumService.getAllBySelection(BackupSelection.select); - final List excludedBackupAlbums = await _backupAlbumService.getAllBySelection(BackupSelection.exclude); - Set selectedAlbums = state.selectedBackupAlbums; - Set excludedAlbums = state.excludedBackupAlbums; - if (selectedAlbums.isNotEmpty) { - selectedAlbums = _updateAlbumsBackupTime(selectedAlbums, selectedBackupAlbums); - } - - if (excludedAlbums.isNotEmpty) { - excludedAlbums = _updateAlbumsBackupTime(excludedAlbums, excludedBackupAlbums); - } - final BackUpProgressEnum previous = state.backupProgress; - state = state.copyWith( - backupProgress: BackUpProgressEnum.inBackground, - selectedBackupAlbums: selectedAlbums, - excludedBackupAlbums: excludedAlbums, - ); - // assumes the background service is currently running - // if true, waits until it has stopped to start the backup - final bool hasLock = await _backgroundService.acquireLock(); - if (hasLock) { - state = state.copyWith(backupProgress: previous); - } - return _resumeBackup(); - } - - Set _updateAlbumsBackupTime(Set albums, List backupAlbums) { - Set result = {}; - for (BackupAlbum ba in backupAlbums) { - try { - AvailableAlbum a = albums.firstWhere((e) => e.id == ba.id); - result.add(a.copyWith(lastBackup: ba.lastBackup)); - } on StateError { - log.severe("[_updateAlbumBackupTime] failed to find album in state", "State Error", StackTrace.current); - } - } - return result; - } - - Future notifyBackgroundServiceCanRun() async { - const allowedStates = [AppLifeCycleEnum.inactive, AppLifeCycleEnum.paused, AppLifeCycleEnum.detached]; - if (allowedStates.contains(ref.read(appStateProvider.notifier).state)) { - _backgroundService.releaseLock(); - } - } - - BackUpProgressEnum get backupProgress => state.backupProgress; - - void updateBackupProgress(BackUpProgressEnum backupProgress) { - state = state.copyWith(backupProgress: backupProgress); - } } diff --git a/mobile/lib/providers/backup/backup_verification.provider.dart b/mobile/lib/providers/backup/backup_verification.provider.dart deleted file mode 100644 index 50270e87ca..0000000000 --- a/mobile/lib/providers/backup/backup_verification.provider.dart +++ /dev/null @@ -1,101 +0,0 @@ -import 'dart:async'; - -import 'package:connectivity_plus/connectivity_plus.dart'; -import 'package:flutter/material.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/providers/asset.provider.dart'; -import 'package:immich_mobile/providers/backup/backup.provider.dart'; -import 'package:immich_mobile/services/backup_verification.service.dart'; -import 'package:immich_mobile/widgets/common/confirm_dialog.dart'; -import 'package:immich_mobile/widgets/common/immich_toast.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; -import 'package:wakelock_plus/wakelock_plus.dart'; - -part 'backup_verification.provider.g.dart'; - -@riverpod -class BackupVerification extends _$BackupVerification { - @override - bool build() => false; - - void performBackupCheck(BuildContext context) async { - try { - state = true; - final backupState = ref.read(backupProvider); - - if (backupState.allUniqueAssets.length > backupState.selectedAlbumsBackupAssetsIds.length) { - if (context.mounted) { - ImmichToast.show( - context: context, - msg: "Backup all assets before starting this check!", - toastType: ToastType.error, - ); - } - return; - } - final connection = await Connectivity().checkConnectivity(); - if (!connection.contains(ConnectivityResult.wifi)) { - if (context.mounted) { - ImmichToast.show( - context: context, - msg: "Make sure to be connected to unmetered Wi-Fi", - toastType: ToastType.error, - ); - } - return; - } - unawaited(WakelockPlus.enable()); - - const limit = 100; - final toDelete = await ref.read(backupVerificationServiceProvider).findWronglyBackedUpAssets(limit: limit); - if (toDelete.isEmpty) { - if (context.mounted) { - ImmichToast.show( - context: context, - msg: "Did not find any corrupt asset backups!", - toastType: ToastType.success, - ); - } - } else { - if (context.mounted) { - await showDialog( - context: context, - builder: (ctx) => ConfirmDialog( - onOk: () => _performDeletion(context, toDelete), - title: "Corrupt backups!", - ok: "Delete", - content: - "Found ${toDelete.length} (max $limit at once) corrupt asset backups. " - "Run the check again to find more.\n" - "Do you want to delete the corrupt asset backups now?", - ), - ); - } - } - } finally { - unawaited(WakelockPlus.disable()); - state = false; - } - } - - Future _performDeletion(BuildContext context, List assets) async { - try { - state = true; - if (context.mounted) { - ImmichToast.show(context: context, msg: "Deleting ${assets.length} assets on the server..."); - } - await ref.read(assetProvider.notifier).deleteAssets(assets, force: true); - if (context.mounted) { - ImmichToast.show( - context: context, - msg: - "Deleted ${assets.length} assets on the server. " - "You can now start a manual backup", - toastType: ToastType.success, - ); - } - } finally { - state = false; - } - } -} diff --git a/mobile/lib/providers/backup/backup_verification.provider.g.dart b/mobile/lib/providers/backup/backup_verification.provider.g.dart deleted file mode 100644 index 13f6819fa7..0000000000 --- a/mobile/lib/providers/backup/backup_verification.provider.g.dart +++ /dev/null @@ -1,27 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'backup_verification.provider.dart'; - -// ************************************************************************** -// RiverpodGenerator -// ************************************************************************** - -String _$backupVerificationHash() => - r'b4b34909ed1af3f28877ea457d53a4a18b6417f8'; - -/// See also [BackupVerification]. -@ProviderFor(BackupVerification) -final backupVerificationProvider = - AutoDisposeNotifierProvider.internal( - BackupVerification.new, - name: r'backupVerificationProvider', - debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') - ? null - : _$backupVerificationHash, - dependencies: null, - allTransitiveDependencies: null, - ); - -typedef _$BackupVerification = AutoDisposeNotifier; -// ignore_for_file: type=lint -// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/mobile/lib/providers/backup/error_backup_list.provider.dart b/mobile/lib/providers/backup/error_backup_list.provider.dart deleted file mode 100644 index db116e4bb9..0000000000 --- a/mobile/lib/providers/backup/error_backup_list.provider.dart +++ /dev/null @@ -1,22 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/models/backup/error_upload_asset.model.dart'; - -class ErrorBackupListNotifier extends StateNotifier> { - ErrorBackupListNotifier() : super({}); - - add(ErrorUploadAsset errorAsset) { - state = state.union({errorAsset}); - } - - remove(ErrorUploadAsset errorAsset) { - state = state.difference({errorAsset}); - } - - empty() { - state = {}; - } -} - -final errorBackupListProvider = StateNotifierProvider>( - (ref) => ErrorBackupListNotifier(), -); diff --git a/mobile/lib/providers/backup/ios_background_settings.provider.dart b/mobile/lib/providers/backup/ios_background_settings.provider.dart deleted file mode 100644 index 98d55882cc..0000000000 --- a/mobile/lib/providers/backup/ios_background_settings.provider.dart +++ /dev/null @@ -1,54 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/services/background.service.dart'; - -class IOSBackgroundSettings { - final bool appRefreshEnabled; - final int numberOfBackgroundTasksQueued; - final DateTime? timeOfLastFetch; - final DateTime? timeOfLastProcessing; - - const IOSBackgroundSettings({ - required this.appRefreshEnabled, - required this.numberOfBackgroundTasksQueued, - this.timeOfLastFetch, - this.timeOfLastProcessing, - }); -} - -class IOSBackgroundSettingsNotifier extends StateNotifier { - final BackgroundService _service; - IOSBackgroundSettingsNotifier(this._service) : super(null); - - IOSBackgroundSettings? get settings => state; - - Future refresh() async { - final lastFetchTime = await _service.getIOSBackupLastRun(IosBackgroundTask.fetch); - final lastProcessingTime = await _service.getIOSBackupLastRun(IosBackgroundTask.processing); - int numberOfProcesses = await _service.getIOSBackupNumberOfProcesses(); - final appRefreshEnabled = await _service.getIOSBackgroundAppRefreshEnabled(); - - // If this is enabled and there are no background processes, - // the user just enabled app refresh in Settings. - // But we don't have any background services running, since it was disabled - // before. - if (await _service.isBackgroundBackupEnabled() && numberOfProcesses == 0) { - // We need to restart the background service - await _service.enableService(); - numberOfProcesses = await _service.getIOSBackupNumberOfProcesses(); - } - - final settings = IOSBackgroundSettings( - appRefreshEnabled: appRefreshEnabled, - numberOfBackgroundTasksQueued: numberOfProcesses, - timeOfLastFetch: lastFetchTime, - timeOfLastProcessing: lastProcessingTime, - ); - - state = settings; - return settings; - } -} - -final iOSBackgroundSettingsProvider = StateNotifierProvider( - (ref) => IOSBackgroundSettingsNotifier(ref.watch(backgroundServiceProvider)), -); diff --git a/mobile/lib/providers/backup/manual_upload.provider.dart b/mobile/lib/providers/backup/manual_upload.provider.dart deleted file mode 100644 index 40efcd7422..0000000000 --- a/mobile/lib/providers/backup/manual_upload.provider.dart +++ /dev/null @@ -1,391 +0,0 @@ -import 'dart:async'; -import 'dart:io'; - -import 'package:collection/collection.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/widgets.dart'; -import 'package:fluttertoast/fluttertoast.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/entities/backup_album.entity.dart'; -import 'package:immich_mobile/models/backup/backup_candidate.model.dart'; -import 'package:immich_mobile/models/backup/backup_state.model.dart'; -import 'package:immich_mobile/models/backup/current_upload_asset.model.dart'; -import 'package:immich_mobile/models/backup/error_upload_asset.model.dart'; -import 'package:immich_mobile/models/backup/manual_upload_state.model.dart'; -import 'package:immich_mobile/models/backup/success_upload_asset.model.dart'; -import 'package:immich_mobile/providers/app_life_cycle.provider.dart'; -import 'package:immich_mobile/providers/app_settings.provider.dart'; -import 'package:immich_mobile/providers/backup/backup.provider.dart'; -import 'package:immich_mobile/providers/backup/error_backup_list.provider.dart'; -import 'package:immich_mobile/providers/gallery_permission.provider.dart'; -import 'package:immich_mobile/repositories/file_media.repository.dart'; -import 'package:immich_mobile/services/app_settings.service.dart'; -import 'package:immich_mobile/services/background.service.dart'; -import 'package:immich_mobile/services/backup.service.dart'; -import 'package:immich_mobile/services/backup_album.service.dart'; -import 'package:immich_mobile/services/local_notification.service.dart'; -import 'package:immich_mobile/utils/backup_progress.dart'; -import 'package:immich_mobile/utils/debug_print.dart'; -import 'package:immich_mobile/widgets/common/immich_toast.dart'; -import 'package:logging/logging.dart'; -import 'package:permission_handler/permission_handler.dart'; -import 'package:photo_manager/photo_manager.dart' show PMProgressHandler; - -final manualUploadProvider = StateNotifierProvider((ref) { - return ManualUploadNotifier( - ref.watch(localNotificationService), - ref.watch(backupProvider.notifier), - ref.watch(backupServiceProvider), - ref.watch(backupAlbumServiceProvider), - ref, - ); -}); - -class ManualUploadNotifier extends StateNotifier { - final Logger _log = Logger("ManualUploadNotifier"); - final LocalNotificationService _localNotificationService; - final BackupNotifier _backupProvider; - final BackupService _backupService; - final BackupAlbumService _backupAlbumService; - final Ref ref; - Completer? _cancelToken; - - ManualUploadNotifier( - this._localNotificationService, - this._backupProvider, - this._backupService, - this._backupAlbumService, - this.ref, - ) : super( - ManualUploadState( - progressInPercentage: 0, - progressInFileSize: "0 B / 0 B", - progressInFileSpeed: 0, - progressInFileSpeeds: const [], - progressInFileSpeedUpdateTime: DateTime.now(), - progressInFileSpeedUpdateSentBytes: 0, - currentUploadAsset: CurrentUploadAsset( - id: '...', - fileCreatedAt: DateTime.parse('2020-10-04'), - fileName: '...', - fileType: '...', - ), - totalAssetsToUpload: 0, - successfulUploads: 0, - currentAssetIndex: 0, - showDetailedNotification: false, - ), - ); - - String _lastPrintedDetailContent = ''; - String? _lastPrintedDetailTitle; - - static const notifyInterval = Duration(milliseconds: 500); - late final ThrottleProgressUpdate _throttledNotifiy = ThrottleProgressUpdate(_updateProgress, notifyInterval); - late final ThrottleProgressUpdate _throttledDetailNotify = ThrottleProgressUpdate( - _updateDetailProgress, - notifyInterval, - ); - - void _updateProgress(String? title, int progress, int total) { - // Guard against throttling calling this method after the upload is done - if (_backupProvider.backupProgress == BackUpProgressEnum.manualInProgress) { - _localNotificationService.showOrUpdateManualUploadStatus( - "backup_background_service_in_progress_notification".tr(), - formatAssetBackupProgress(state.currentAssetIndex, state.totalAssetsToUpload), - maxProgress: state.totalAssetsToUpload, - progress: state.currentAssetIndex, - showActions: true, - ); - } - } - - void _updateDetailProgress(String? title, int progress, int total) { - // Guard against throttling calling this method after the upload is done - if (_backupProvider.backupProgress == BackUpProgressEnum.manualInProgress) { - final String msg = total > 0 ? humanReadableBytesProgress(progress, total) : ""; - // only update if message actually differs (to stop many useless notification updates on large assets or slow connections) - if (msg != _lastPrintedDetailContent || title != _lastPrintedDetailTitle) { - _lastPrintedDetailContent = msg; - _lastPrintedDetailTitle = title; - _localNotificationService.showOrUpdateManualUploadStatus( - title ?? 'Uploading', - msg, - progress: total > 0 ? (progress * 1000) ~/ total : 0, - maxProgress: 1000, - isDetailed: true, - // Detailed noitifcation is displayed for Single asset uploads. Show actions for such case - showActions: state.totalAssetsToUpload == 1, - ); - } - } - } - - void _onAssetUploaded(SuccessUploadAsset result) { - state = state.copyWith(successfulUploads: state.successfulUploads + 1); - _backupProvider.updateDiskInfo(); - } - - void _onAssetUploadError(ErrorUploadAsset errorAssetInfo) { - ref.watch(errorBackupListProvider.notifier).add(errorAssetInfo); - } - - void _onProgress(int sent, int total) { - double lastUploadSpeed = state.progressInFileSpeed; - List lastUploadSpeeds = state.progressInFileSpeeds.toList(); - DateTime lastUpdateTime = state.progressInFileSpeedUpdateTime; - int lastSentBytes = state.progressInFileSpeedUpdateSentBytes; - - final now = DateTime.now(); - final duration = now.difference(lastUpdateTime); - - // Keep the upload speed average span limited, to keep it somewhat relevant - if (lastUploadSpeeds.length > 10) { - lastUploadSpeeds.removeAt(0); - } - - if (duration.inSeconds > 0) { - lastUploadSpeeds.add(((sent - lastSentBytes) / duration.inSeconds).abs().roundToDouble()); - - lastUploadSpeed = lastUploadSpeeds.average.abs().roundToDouble(); - lastUpdateTime = now; - lastSentBytes = sent; - } - - state = state.copyWith( - progressInPercentage: (sent.toDouble() / total.toDouble() * 100), - progressInFileSize: humanReadableFileBytesProgress(sent, total), - progressInFileSpeed: lastUploadSpeed, - progressInFileSpeeds: lastUploadSpeeds, - progressInFileSpeedUpdateTime: lastUpdateTime, - progressInFileSpeedUpdateSentBytes: lastSentBytes, - ); - - if (state.showDetailedNotification) { - final title = "backup_background_service_current_upload_notification".tr( - namedArgs: {'filename': state.currentUploadAsset.fileName}, - ); - _throttledDetailNotify(title: title, progress: sent, total: total); - } - } - - void _onSetCurrentBackupAsset(CurrentUploadAsset currentUploadAsset) { - state = state.copyWith(currentUploadAsset: currentUploadAsset, currentAssetIndex: state.currentAssetIndex + 1); - if (state.totalAssetsToUpload > 1) { - _throttledNotifiy(); - } - if (state.showDetailedNotification) { - _throttledDetailNotify.title = "backup_background_service_current_upload_notification".tr( - namedArgs: {'filename': currentUploadAsset.fileName}, - ); - _throttledDetailNotify.progress = 0; - _throttledDetailNotify.total = 0; - } - } - - Future _startUpload(Iterable allManualUploads) async { - bool hasErrors = false; - try { - _backupProvider.updateBackupProgress(BackUpProgressEnum.manualInProgress); - - if (ref.read(galleryPermissionNotifier.notifier).hasPermission) { - await ref.read(fileMediaRepositoryProvider).clearFileCache(); - - final allAssetsFromDevice = allManualUploads.where((e) => e.isLocal && !e.isRemote).toList(); - - if (allAssetsFromDevice.length != allManualUploads.length) { - _log.warning( - '[_startUpload] Refreshed upload list -> ${allManualUploads.length - allAssetsFromDevice.length} asset will not be uploaded', - ); - } - - final selectedBackupAlbums = await _backupAlbumService.getAllBySelection(BackupSelection.select); - final excludedBackupAlbums = await _backupAlbumService.getAllBySelection(BackupSelection.exclude); - - // Get candidates from selected albums and excluded albums - Set candidates = await _backupService.buildUploadCandidates( - selectedBackupAlbums, - excludedBackupAlbums, - useTimeFilter: false, - ); - - // Extrack candidate from allAssetsFromDevice - final uploadAssets = candidates.where( - (candidate) => - allAssetsFromDevice.firstWhereOrNull((asset) => asset.localId == candidate.asset.localId) != null, - ); - - if (uploadAssets.isEmpty) { - dPrint(() => "[_startUpload] No Assets to upload - Abort Process"); - _backupProvider.updateBackupProgress(BackUpProgressEnum.idle); - return false; - } - - state = state.copyWith( - progressInPercentage: 0, - progressInFileSize: "0 B / 0 B", - progressInFileSpeed: 0, - totalAssetsToUpload: uploadAssets.length, - successfulUploads: 0, - currentAssetIndex: 0, - currentUploadAsset: CurrentUploadAsset( - id: '...', - fileCreatedAt: DateTime.parse('2020-10-04'), - fileName: '...', - fileType: '...', - ), - ); - // Reset Error List - ref.watch(errorBackupListProvider.notifier).empty(); - - if (state.totalAssetsToUpload > 1) { - _throttledNotifiy(); - } - - // Show detailed asset if enabled in settings or if a single asset is uploaded - bool showDetailedNotification = - ref.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.backgroundBackupSingleProgress) || - state.totalAssetsToUpload == 1; - state = state.copyWith(showDetailedNotification: showDetailedNotification); - final pmProgressHandler = Platform.isIOS ? PMProgressHandler() : null; - - _cancelToken?.complete(); - _cancelToken = Completer(); - final bool ok = await ref - .read(backupServiceProvider) - .backupAsset( - uploadAssets, - _cancelToken!, - pmProgressHandler: pmProgressHandler, - onSuccess: _onAssetUploaded, - onProgress: _onProgress, - onCurrentAsset: _onSetCurrentBackupAsset, - onError: _onAssetUploadError, - ); - - // Close detailed notification - await _localNotificationService.closeNotification(LocalNotificationService.manualUploadDetailedNotificationID); - - _log.info( - '[_startUpload] Manual Upload Completed - success: ${state.successfulUploads},' - ' failed: ${state.totalAssetsToUpload - state.successfulUploads}', - ); - - // User cancelled upload - if (!ok && _cancelToken == null) { - await _localNotificationService.showOrUpdateManualUploadStatus( - "backup_manual_title".tr(), - "backup_manual_cancelled".tr(), - presentBanner: true, - ); - hasErrors = true; - } else if (state.successfulUploads == 0 || (!ok && _cancelToken != null)) { - await _localNotificationService.showOrUpdateManualUploadStatus( - "backup_manual_title".tr(), - "failed".tr(), - presentBanner: true, - ); - hasErrors = true; - } else { - await _localNotificationService.showOrUpdateManualUploadStatus( - "backup_manual_title".tr(), - "backup_manual_success".tr(), - presentBanner: true, - ); - } - } else { - unawaited(openAppSettings()); - dPrint(() => "[_startUpload] Do not have permission to the gallery"); - } - } catch (e) { - dPrint(() => "ERROR _startUpload: ${e.toString()}"); - hasErrors = true; - } finally { - _backupProvider.updateBackupProgress(BackUpProgressEnum.idle); - _handleAppInActivity(); - await _localNotificationService.closeNotification(LocalNotificationService.manualUploadDetailedNotificationID); - await _backupProvider.notifyBackgroundServiceCanRun(); - } - return !hasErrors; - } - - void _handleAppInActivity() { - final appState = ref.read(appStateProvider.notifier).getAppState(); - // The app is currently in background. Perform the necessary cleanups which - // are on-hold for upload completion - if (appState != AppLifeCycleEnum.active && appState != AppLifeCycleEnum.resumed) { - ref.read(backupProvider.notifier).cancelBackup(); - } - } - - void cancelBackup() { - if (_backupProvider.backupProgress != BackUpProgressEnum.inProgress && - _backupProvider.backupProgress != BackUpProgressEnum.manualInProgress) { - _backupProvider.notifyBackgroundServiceCanRun(); - } - _cancelToken?.complete(); - _cancelToken = null; - if (_backupProvider.backupProgress != BackUpProgressEnum.manualInProgress) { - _backupProvider.updateBackupProgress(BackUpProgressEnum.idle); - } - state = state.copyWith( - progressInPercentage: 0, - progressInFileSize: "0 B / 0 B", - progressInFileSpeed: 0, - progressInFileSpeedUpdateTime: DateTime.now(), - progressInFileSpeedUpdateSentBytes: 0, - ); - } - - Future uploadAssets(BuildContext context, Iterable allManualUploads) async { - // assumes the background service is currently running and - // waits until it has stopped to start the backup. - final bool hasLock = await ref.read(backgroundServiceProvider).acquireLock(); - if (!hasLock) { - dPrint(() => "[uploadAssets] could not acquire lock, exiting"); - ImmichToast.show( - context: context, - msg: "failed".tr(), - toastType: ToastType.info, - gravity: ToastGravity.BOTTOM, - durationInSecond: 3, - ); - return false; - } - - bool showInProgress = false; - - // check if backup is already in process - then return - if (_backupProvider.backupProgress == BackUpProgressEnum.manualInProgress) { - dPrint(() => "[uploadAssets] Manual upload is already running - abort"); - showInProgress = true; - } - - if (_backupProvider.backupProgress == BackUpProgressEnum.inProgress) { - dPrint(() => "[uploadAssets] Auto Backup is already in progress - abort"); - showInProgress = true; - return false; - } - - if (_backupProvider.backupProgress == BackUpProgressEnum.inBackground) { - dPrint(() => "[uploadAssets] Background backup is running - abort"); - showInProgress = true; - } - - if (showInProgress) { - if (context.mounted) { - ImmichToast.show( - context: context, - msg: "backup_manual_in_progress".tr(), - toastType: ToastType.info, - gravity: ToastGravity.BOTTOM, - durationInSecond: 3, - ); - } - return false; - } - - return _startUpload(allManualUploads); - } -} diff --git a/mobile/lib/providers/cast.provider.dart b/mobile/lib/providers/cast.provider.dart index fea95f42aa..b298514d67 100644 --- a/mobile/lib/providers/cast.provider.dart +++ b/mobile/lib/providers/cast.provider.dart @@ -1,6 +1,5 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; -import 'package:immich_mobile/entities/asset.entity.dart' as old_asset_entity; import 'package:immich_mobile/models/cast/cast_manager_state.dart'; import 'package:immich_mobile/services/gcast.service.dart'; @@ -55,26 +54,6 @@ class CastNotifier extends StateNotifier { _gCastService.loadMedia(asset, reload); } - // TODO: remove this when we migrate to new timeline - void loadMediaOld(old_asset_entity.Asset asset, bool reload) { - final remoteAsset = RemoteAsset( - id: asset.remoteId.toString(), - name: asset.name, - ownerId: asset.ownerId.toString(), - checksum: asset.checksum, - type: asset.type == old_asset_entity.AssetType.image - ? AssetType.image - : asset.type == old_asset_entity.AssetType.video - ? AssetType.video - : AssetType.other, - createdAt: asset.fileCreatedAt, - updatedAt: asset.updatedAt, - isEdited: false, - ); - - _gCastService.loadMedia(remoteAsset, reload); - } - Future connect(CastDestinationType type, dynamic device) async { switch (type) { case CastDestinationType.googleCast: diff --git a/mobile/lib/providers/db.provider.dart b/mobile/lib/providers/db.provider.dart deleted file mode 100644 index e03e037f36..0000000000 --- a/mobile/lib/providers/db.provider.dart +++ /dev/null @@ -1,5 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:isar/isar.dart'; - -// overwritten in main.dart due to async loading -final dbProvider = Provider((_) => throw UnimplementedError()); diff --git a/mobile/lib/providers/image/exceptions/image_loading_exception.dart b/mobile/lib/providers/image/exceptions/image_loading_exception.dart deleted file mode 100644 index 98f633a88f..0000000000 --- a/mobile/lib/providers/image/exceptions/image_loading_exception.dart +++ /dev/null @@ -1,5 +0,0 @@ -/// An exception for the [ImageLoader] and the Immich image providers -class ImageLoadingException implements Exception { - final String message; - const ImageLoadingException(this.message); -} diff --git a/mobile/lib/providers/infrastructure/action.provider.dart b/mobile/lib/providers/infrastructure/action.provider.dart index d24e2cc6cd..434e930dcf 100644 --- a/mobile/lib/providers/infrastructure/action.provider.dart +++ b/mobile/lib/providers/infrastructure/action.provider.dart @@ -19,16 +19,12 @@ import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/services/action.service.dart'; import 'package:immich_mobile/services/download.service.dart'; import 'package:immich_mobile/services/foreground_upload.service.dart'; -import 'package:immich_mobile/services/timeline.service.dart'; import 'package:immich_mobile/widgets/asset_grid/delete_dialog.dart'; import 'package:logging/logging.dart'; import 'package:openapi/api.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; -final actionProvider = NotifierProvider( - ActionNotifier.new, - dependencies: [multiSelectProvider, timelineServiceProvider], -); +final actionProvider = NotifierProvider(ActionNotifier.new, dependencies: [multiSelectProvider]); class ActionResult { final int count; diff --git a/mobile/lib/providers/infrastructure/db.provider.dart b/mobile/lib/providers/infrastructure/db.provider.dart index d38bcbfb55..2b4ba0129f 100644 --- a/mobile/lib/providers/infrastructure/db.provider.dart +++ b/mobile/lib/providers/infrastructure/db.provider.dart @@ -2,13 +2,6 @@ import 'dart:async'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; -import 'package:isar/isar.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; - -part 'db.provider.g.dart'; - -@Riverpod(keepAlive: true) -Isar isar(Ref ref) => throw UnimplementedError('isar'); Drift Function(Ref ref) driftOverride(Drift drift) => (ref) { ref.onDispose(() => unawaited(drift.close())); diff --git a/mobile/lib/providers/infrastructure/db.provider.g.dart b/mobile/lib/providers/infrastructure/db.provider.g.dart deleted file mode 100644 index 46abfb66a9..0000000000 --- a/mobile/lib/providers/infrastructure/db.provider.g.dart +++ /dev/null @@ -1,27 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'db.provider.dart'; - -// ************************************************************************** -// RiverpodGenerator -// ************************************************************************** - -String _$isarHash() => r'69d3a06aa7e69a4381478e03f7956eb07d7f7feb'; - -/// See also [isar]. -@ProviderFor(isar) -final isarProvider = Provider.internal( - isar, - name: r'isarProvider', - debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') - ? null - : _$isarHash, - dependencies: null, - allTransitiveDependencies: null, -); - -@Deprecated('Will be removed in 3.0. Use Ref instead') -// ignore: unused_element -typedef IsarRef = ProviderRef; -// ignore_for_file: type=lint -// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/mobile/lib/providers/infrastructure/device_asset.provider.dart b/mobile/lib/providers/infrastructure/device_asset.provider.dart deleted file mode 100644 index 7854af016a..0000000000 --- a/mobile/lib/providers/infrastructure/device_asset.provider.dart +++ /dev/null @@ -1,7 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/infrastructure/repositories/device_asset.repository.dart'; -import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; - -final deviceAssetRepositoryProvider = Provider( - (ref) => IsarDeviceAssetRepository(ref.watch(isarProvider)), -); diff --git a/mobile/lib/providers/infrastructure/exif.provider.dart b/mobile/lib/providers/infrastructure/exif.provider.dart deleted file mode 100644 index c126f6cac0..0000000000 --- a/mobile/lib/providers/infrastructure/exif.provider.dart +++ /dev/null @@ -1,9 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/infrastructure/repositories/exif.repository.dart'; -import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; - -part 'exif.provider.g.dart'; - -@Riverpod(keepAlive: true) -IsarExifRepository exifRepository(Ref ref) => IsarExifRepository(ref.watch(isarProvider)); diff --git a/mobile/lib/providers/infrastructure/exif.provider.g.dart b/mobile/lib/providers/infrastructure/exif.provider.g.dart deleted file mode 100644 index 0261558707..0000000000 --- a/mobile/lib/providers/infrastructure/exif.provider.g.dart +++ /dev/null @@ -1,27 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'exif.provider.dart'; - -// ************************************************************************** -// RiverpodGenerator -// ************************************************************************** - -String _$exifRepositoryHash() => r'bf4a3f6a50d954a23d317659b4f3e2f381066463'; - -/// See also [exifRepository]. -@ProviderFor(exifRepository) -final exifRepositoryProvider = Provider.internal( - exifRepository, - name: r'exifRepositoryProvider', - debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') - ? null - : _$exifRepositoryHash, - dependencies: null, - allTransitiveDependencies: null, -); - -@Deprecated('Will be removed in 3.0. Use Ref instead') -// ignore: unused_element -typedef ExifRepositoryRef = ProviderRef; -// ignore_for_file: type=lint -// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/mobile/lib/providers/infrastructure/store.provider.dart b/mobile/lib/providers/infrastructure/store.provider.dart index 0bf42f3e8b..f867d30fdc 100644 --- a/mobile/lib/providers/infrastructure/store.provider.dart +++ b/mobile/lib/providers/infrastructure/store.provider.dart @@ -1,13 +1,8 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/services/store.service.dart'; -import 'package:immich_mobile/infrastructure/repositories/store.repository.dart'; -import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'store.provider.g.dart'; -@Riverpod(keepAlive: true) -IsarStoreRepository storeRepository(Ref ref) => IsarStoreRepository(ref.watch(isarProvider)); - @Riverpod(keepAlive: true) StoreService storeService(Ref _) => StoreService.I; diff --git a/mobile/lib/providers/infrastructure/store.provider.g.dart b/mobile/lib/providers/infrastructure/store.provider.g.dart index 98c978cb60..b5af7de3e0 100644 --- a/mobile/lib/providers/infrastructure/store.provider.g.dart +++ b/mobile/lib/providers/infrastructure/store.provider.g.dart @@ -6,23 +6,6 @@ part of 'store.provider.dart'; // RiverpodGenerator // ************************************************************************** -String _$storeRepositoryHash() => r'659cb134466e4b0d5f04e2fc93e426350d99545f'; - -/// See also [storeRepository]. -@ProviderFor(storeRepository) -final storeRepositoryProvider = Provider.internal( - storeRepository, - name: r'storeRepositoryProvider', - debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') - ? null - : _$storeRepositoryHash, - dependencies: null, - allTransitiveDependencies: null, -); - -@Deprecated('Will be removed in 3.0. Use Ref instead') -// ignore: unused_element -typedef StoreRepositoryRef = ProviderRef; String _$storeServiceHash() => r'250e10497c42df360e9e1f9a618d0b19c1b5b0a0'; /// See also [storeService]. diff --git a/mobile/lib/providers/infrastructure/user.provider.dart b/mobile/lib/providers/infrastructure/user.provider.dart index 922b9866bb..6c3263229e 100644 --- a/mobile/lib/providers/infrastructure/user.provider.dart +++ b/mobile/lib/providers/infrastructure/user.provider.dart @@ -3,7 +3,6 @@ import 'package:immich_mobile/domain/models/user.model.dart'; import 'package:immich_mobile/domain/services/partner.service.dart'; import 'package:immich_mobile/domain/services/user.service.dart'; import 'package:immich_mobile/infrastructure/repositories/partner.repository.dart'; -import 'package:immich_mobile/infrastructure/repositories/user.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/user_api.repository.dart'; import 'package:immich_mobile/providers/api.provider.dart'; import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; @@ -14,18 +13,12 @@ import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'user.provider.g.dart'; -@Riverpod(keepAlive: true) -IsarUserRepository userRepository(Ref ref) => IsarUserRepository(ref.watch(isarProvider)); - @Riverpod(keepAlive: true) UserApiRepository userApiRepository(Ref ref) => UserApiRepository(ref.watch(apiServiceProvider).usersApi); @Riverpod(keepAlive: true) -UserService userService(Ref ref) => UserService( - isarUserRepository: ref.watch(userRepositoryProvider), - userApiRepository: ref.watch(userApiRepositoryProvider), - storeService: ref.watch(storeServiceProvider), -); +UserService userService(Ref ref) => + UserService(userApiRepository: ref.watch(userApiRepositoryProvider), storeService: ref.watch(storeServiceProvider)); /// Drifts final driftPartnerRepositoryProvider = Provider( diff --git a/mobile/lib/providers/infrastructure/user.provider.g.dart b/mobile/lib/providers/infrastructure/user.provider.g.dart index f9148bf3a7..2e9115dad9 100644 --- a/mobile/lib/providers/infrastructure/user.provider.g.dart +++ b/mobile/lib/providers/infrastructure/user.provider.g.dart @@ -6,23 +6,6 @@ part of 'user.provider.dart'; // RiverpodGenerator // ************************************************************************** -String _$userRepositoryHash() => r'538791a4ad126ed086c9db682c67fc5c654d54f3'; - -/// See also [userRepository]. -@ProviderFor(userRepository) -final userRepositoryProvider = Provider.internal( - userRepository, - name: r'userRepositoryProvider', - debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') - ? null - : _$userRepositoryHash, - dependencies: null, - allTransitiveDependencies: null, -); - -@Deprecated('Will be removed in 3.0. Use Ref instead') -// ignore: unused_element -typedef UserRepositoryRef = ProviderRef; String _$userApiRepositoryHash() => r'8a7340ca4544c8c6b20225c65bff2abb9e96baa2'; /// See also [userApiRepository]. @@ -40,7 +23,7 @@ final userApiRepositoryProvider = Provider.internal( @Deprecated('Will be removed in 3.0. Use Ref instead') // ignore: unused_element typedef UserApiRepositoryRef = ProviderRef; -String _$userServiceHash() => r'181414dddc7891be6237e13d568c287a804228d1'; +String _$userServiceHash() => r'47e607f3b484b51bcb634d47e3cbf1f6ef25da97'; /// See also [userService]. @ProviderFor(userService) diff --git a/mobile/lib/providers/memory.provider.dart b/mobile/lib/providers/memory.provider.dart deleted file mode 100644 index 7fef3060cc..0000000000 --- a/mobile/lib/providers/memory.provider.dart +++ /dev/null @@ -1,9 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/models/memories/memory.model.dart'; -import 'package:immich_mobile/services/memory.service.dart'; - -final memoryFutureProvider = FutureProvider.autoDispose?>((ref) async { - final service = ref.watch(memoryServiceProvider); - - return await service.getMemoryLane(); -}); diff --git a/mobile/lib/providers/partner.provider.dart b/mobile/lib/providers/partner.provider.dart deleted file mode 100644 index 5a85cea1d4..0000000000 --- a/mobile/lib/providers/partner.provider.dart +++ /dev/null @@ -1,89 +0,0 @@ -import 'dart:async'; - -import 'package:collection/collection.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/domain/models/user.model.dart'; -import 'package:immich_mobile/providers/album/suggested_shared_users.provider.dart'; -import 'package:immich_mobile/services/partner.service.dart'; - -class PartnerSharedWithNotifier extends StateNotifier> { - final PartnerService _partnerService; - late final StreamSubscription> streamSub; - - PartnerSharedWithNotifier(this._partnerService) : super([]) { - Function eq = const ListEquality().equals; - _partnerService - .getSharedWith() - .then((partners) { - if (!eq(state, partners)) { - state = partners; - } - }) - .then((_) { - streamSub = _partnerService.watchSharedWith().listen((partners) { - if (!eq(state, partners)) { - state = partners; - } - }); - }); - } - - Future updatePartner(UserDto partner, {required bool inTimeline}) { - return _partnerService.updatePartner(partner, inTimeline: inTimeline); - } - - @override - void dispose() { - if (mounted) { - streamSub.cancel(); - } - super.dispose(); - } -} - -final partnerSharedWithProvider = StateNotifierProvider>((ref) { - return PartnerSharedWithNotifier(ref.watch(partnerServiceProvider)); -}); - -class PartnerSharedByNotifier extends StateNotifier> { - final PartnerService _partnerService; - late final StreamSubscription> streamSub; - - PartnerSharedByNotifier(this._partnerService) : super([]) { - Function eq = const ListEquality().equals; - _partnerService - .getSharedBy() - .then((partners) { - if (!eq(state, partners)) { - state = partners; - } - }) - .then((_) { - streamSub = _partnerService.watchSharedBy().listen((partners) { - if (!eq(state, partners)) { - state = partners; - } - }); - }); - } - - @override - void dispose() { - if (mounted) { - streamSub.cancel(); - } - super.dispose(); - } -} - -final partnerSharedByProvider = StateNotifierProvider>((ref) { - return PartnerSharedByNotifier(ref.watch(partnerServiceProvider)); -}); - -final partnerAvailableProvider = FutureProvider.autoDispose>((ref) async { - final otherUsers = await ref.watch(otherUsersProvider.future); - final currentPartners = ref.watch(partnerSharedByProvider); - final available = Set.of(otherUsers); - available.removeAll(currentPartners); - return available.toList(); -}); diff --git a/mobile/lib/providers/search/all_motion_photos.provider.dart b/mobile/lib/providers/search/all_motion_photos.provider.dart deleted file mode 100644 index 48bc1bb80c..0000000000 --- a/mobile/lib/providers/search/all_motion_photos.provider.dart +++ /dev/null @@ -1,7 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/services/asset.service.dart'; - -final allMotionPhotosProvider = FutureProvider>((ref) async { - return ref.watch(assetServiceProvider).getMotionAssets(); -}); diff --git a/mobile/lib/providers/search/paginated_search.provider.dart b/mobile/lib/providers/search/paginated_search.provider.dart deleted file mode 100644 index 9a37d83320..0000000000 --- a/mobile/lib/providers/search/paginated_search.provider.dart +++ /dev/null @@ -1,46 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/models/search/search_result.model.dart'; -import 'package:immich_mobile/services/timeline.service.dart'; -import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart'; -import 'package:immich_mobile/models/search/search_filter.model.dart'; -import 'package:immich_mobile/services/search.service.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; - -part 'paginated_search.provider.g.dart'; - -final paginatedSearchProvider = StateNotifierProvider( - (ref) => PaginatedSearchNotifier(ref.watch(searchServiceProvider)), -); - -class PaginatedSearchNotifier extends StateNotifier { - final SearchService _searchService; - - PaginatedSearchNotifier(this._searchService) : super(const SearchResult(assets: [], nextPage: 1)); - - Future search(SearchFilter filter) async { - if (state.nextPage == null) { - return false; - } - - final result = await _searchService.search(filter, state.nextPage!); - - if (result == null) { - return false; - } - - state = SearchResult(assets: [...state.assets, ...result.assets], nextPage: result.nextPage); - - return true; - } - - clear() { - state = const SearchResult(assets: [], nextPage: 1); - } -} - -@riverpod -Future paginatedSearchRenderList(Ref ref) { - final result = ref.watch(paginatedSearchProvider); - final timelineService = ref.watch(timelineServiceProvider); - return timelineService.getTimelineFromAssets(result.assets, GroupAssetsBy.none); -} diff --git a/mobile/lib/providers/search/paginated_search.provider.g.dart b/mobile/lib/providers/search/paginated_search.provider.g.dart deleted file mode 100644 index e984997967..0000000000 --- a/mobile/lib/providers/search/paginated_search.provider.g.dart +++ /dev/null @@ -1,29 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'paginated_search.provider.dart'; - -// ************************************************************************** -// RiverpodGenerator -// ************************************************************************** - -String _$paginatedSearchRenderListHash() => - r'22d715ff7864e5a946be38322ce7813616f899c2'; - -/// See also [paginatedSearchRenderList]. -@ProviderFor(paginatedSearchRenderList) -final paginatedSearchRenderListProvider = - AutoDisposeFutureProvider.internal( - paginatedSearchRenderList, - name: r'paginatedSearchRenderListProvider', - debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') - ? null - : _$paginatedSearchRenderListHash, - dependencies: null, - allTransitiveDependencies: null, - ); - -@Deprecated('Will be removed in 3.0. Use Ref instead') -// ignore: unused_element -typedef PaginatedSearchRenderListRef = AutoDisposeFutureProviderRef; -// ignore_for_file: type=lint -// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/mobile/lib/providers/search/people.provider.dart b/mobile/lib/providers/search/people.provider.dart index 3ff8d67983..1f6f983154 100644 --- a/mobile/lib/providers/search/people.provider.dart +++ b/mobile/lib/providers/search/people.provider.dart @@ -1,9 +1,6 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/person.model.dart'; -import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart'; import 'package:immich_mobile/services/person.service.dart'; -import 'package:immich_mobile/providers/app_settings.provider.dart'; -import 'package:immich_mobile/services/app_settings.service.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'people.provider.g.dart'; @@ -17,16 +14,6 @@ Future> getAllPeople(Ref ref) async { return people; } -@riverpod -Future personAssets(Ref ref, String personId) async { - final PersonService personService = ref.read(personServiceProvider); - final assets = await personService.getPersonAssets(personId); - - final settings = ref.read(appSettingsServiceProvider); - final groupBy = GroupAssetsBy.values[settings.getSetting(AppSettingsEnum.groupAssetsBy)]; - return await RenderList.fromAssets(assets, groupBy); -} - @riverpod Future updatePersonName(Ref ref, String personId, String updatedName) async { final PersonService personService = ref.read(personServiceProvider); diff --git a/mobile/lib/providers/search/people.provider.g.dart b/mobile/lib/providers/search/people.provider.g.dart index 9595c36eec..23424c068f 100644 --- a/mobile/lib/providers/search/people.provider.g.dart +++ b/mobile/lib/providers/search/people.provider.g.dart @@ -24,7 +24,7 @@ final getAllPeopleProvider = @Deprecated('Will be removed in 3.0. Use Ref instead') // ignore: unused_element typedef GetAllPeopleRef = AutoDisposeFutureProviderRef>; -String _$personAssetsHash() => r'c1d35ee0e024bd6915e21bc724be4b458a14bc24'; +String _$updatePersonNameHash() => r'45f7693172de522a227406d8198811434cf2bbbc'; /// Copied from Dart SDK class _SystemHash { @@ -47,126 +47,6 @@ class _SystemHash { } } -/// See also [personAssets]. -@ProviderFor(personAssets) -const personAssetsProvider = PersonAssetsFamily(); - -/// See also [personAssets]. -class PersonAssetsFamily extends Family> { - /// See also [personAssets]. - const PersonAssetsFamily(); - - /// See also [personAssets]. - PersonAssetsProvider call(String personId) { - return PersonAssetsProvider(personId); - } - - @override - PersonAssetsProvider getProviderOverride( - covariant PersonAssetsProvider provider, - ) { - return call(provider.personId); - } - - static const Iterable? _dependencies = null; - - @override - Iterable? get dependencies => _dependencies; - - static const Iterable? _allTransitiveDependencies = null; - - @override - Iterable? get allTransitiveDependencies => - _allTransitiveDependencies; - - @override - String? get name => r'personAssetsProvider'; -} - -/// See also [personAssets]. -class PersonAssetsProvider extends AutoDisposeFutureProvider { - /// See also [personAssets]. - PersonAssetsProvider(String personId) - : this._internal( - (ref) => personAssets(ref as PersonAssetsRef, personId), - from: personAssetsProvider, - name: r'personAssetsProvider', - debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') - ? null - : _$personAssetsHash, - dependencies: PersonAssetsFamily._dependencies, - allTransitiveDependencies: - PersonAssetsFamily._allTransitiveDependencies, - personId: personId, - ); - - PersonAssetsProvider._internal( - super._createNotifier, { - required super.name, - required super.dependencies, - required super.allTransitiveDependencies, - required super.debugGetCreateSourceHash, - required super.from, - required this.personId, - }) : super.internal(); - - final String personId; - - @override - Override overrideWith( - FutureOr Function(PersonAssetsRef provider) create, - ) { - return ProviderOverride( - origin: this, - override: PersonAssetsProvider._internal( - (ref) => create(ref as PersonAssetsRef), - from: from, - name: null, - dependencies: null, - allTransitiveDependencies: null, - debugGetCreateSourceHash: null, - personId: personId, - ), - ); - } - - @override - AutoDisposeFutureProviderElement createElement() { - return _PersonAssetsProviderElement(this); - } - - @override - bool operator ==(Object other) { - return other is PersonAssetsProvider && other.personId == personId; - } - - @override - int get hashCode { - var hash = _SystemHash.combine(0, runtimeType.hashCode); - hash = _SystemHash.combine(hash, personId.hashCode); - - return _SystemHash.finish(hash); - } -} - -@Deprecated('Will be removed in 3.0. Use Ref instead') -// ignore: unused_element -mixin PersonAssetsRef on AutoDisposeFutureProviderRef { - /// The parameter `personId` of this provider. - String get personId; -} - -class _PersonAssetsProviderElement - extends AutoDisposeFutureProviderElement - with PersonAssetsRef { - _PersonAssetsProviderElement(super.provider); - - @override - String get personId => (origin as PersonAssetsProvider).personId; -} - -String _$updatePersonNameHash() => r'45f7693172de522a227406d8198811434cf2bbbc'; - /// See also [updatePersonName]. @ProviderFor(updatePersonName) const updatePersonNameProvider = UpdatePersonNameFamily(); diff --git a/mobile/lib/providers/search/recently_taken_asset.provider.dart b/mobile/lib/providers/search/recently_taken_asset.provider.dart deleted file mode 100644 index 157e7c2a74..0000000000 --- a/mobile/lib/providers/search/recently_taken_asset.provider.dart +++ /dev/null @@ -1,9 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/services/asset.service.dart'; - -final recentlyTakenAssetProvider = FutureProvider>((ref) async { - final assetService = ref.read(assetServiceProvider); - - return assetService.getRecentlyTakenAssets(); -}); diff --git a/mobile/lib/providers/timeline.provider.dart b/mobile/lib/providers/timeline.provider.dart deleted file mode 100644 index 71ea308dbf..0000000000 --- a/mobile/lib/providers/timeline.provider.dart +++ /dev/null @@ -1,68 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/providers/album/album.provider.dart'; -import 'package:immich_mobile/providers/locale_provider.dart'; -import 'package:immich_mobile/services/timeline.service.dart'; -import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart'; - -final singleUserTimelineProvider = StreamProvider.family((ref, userId) { - if (userId == null) { - return const Stream.empty(); - } - - ref.watch(localeProvider); - final timelineService = ref.watch(timelineServiceProvider); - return timelineService.watchHomeTimeline(userId); -}, dependencies: [localeProvider]); - -final multiUsersTimelineProvider = StreamProvider.family>((ref, userIds) { - ref.watch(localeProvider); - final timelineService = ref.watch(timelineServiceProvider); - return timelineService.watchMultiUsersTimeline(userIds); -}, dependencies: [localeProvider]); - -final albumTimelineProvider = StreamProvider.autoDispose.family((ref, id) { - final album = ref.watch(albumWatcher(id)).value; - final timelineService = ref.watch(timelineServiceProvider); - - if (album != null) { - return timelineService.watchAlbumTimeline(album); - } - - return const Stream.empty(); -}); - -final archiveTimelineProvider = StreamProvider((ref) { - final timelineService = ref.watch(timelineServiceProvider); - return timelineService.watchArchiveTimeline(); -}); - -final favoriteTimelineProvider = StreamProvider((ref) { - final timelineService = ref.watch(timelineServiceProvider); - return timelineService.watchFavoriteTimeline(); -}); - -final trashTimelineProvider = StreamProvider((ref) { - final timelineService = ref.watch(timelineServiceProvider); - return timelineService.watchTrashTimeline(); -}); - -final allVideosTimelineProvider = StreamProvider((ref) { - final timelineService = ref.watch(timelineServiceProvider); - return timelineService.watchAllVideosTimeline(); -}); - -final assetSelectionTimelineProvider = StreamProvider((ref) { - final timelineService = ref.watch(timelineServiceProvider); - return timelineService.watchAssetSelectionTimeline(); -}); - -final assetsTimelineProvider = FutureProvider.family>((ref, assets) { - final timelineService = ref.watch(timelineServiceProvider); - return timelineService.getTimelineFromAssets(assets, null); -}); - -final lockedTimelineProvider = StreamProvider((ref) { - final timelineService = ref.watch(timelineServiceProvider); - return timelineService.watchLockedTimelineProvider(); -}); diff --git a/mobile/lib/providers/trash.provider.dart b/mobile/lib/providers/trash.provider.dart deleted file mode 100644 index 41b9160b9b..0000000000 --- a/mobile/lib/providers/trash.provider.dart +++ /dev/null @@ -1,45 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/services/trash.service.dart'; -import 'package:logging/logging.dart'; - -class TrashNotifier extends StateNotifier { - final TrashService _trashService; - final _log = Logger('TrashNotifier'); - - TrashNotifier(this._trashService) : super(false); - - Future emptyTrash() async { - try { - await _trashService.emptyTrash(); - state = true; - } catch (error, stack) { - _log.severe("Cannot empty trash", error, stack); - state = false; - } - } - - Future restoreAssets(Iterable assetList) async { - try { - await _trashService.restoreAssets(assetList); - return true; - } catch (error, stack) { - _log.severe("Cannot restore assets", error, stack); - return false; - } - } - - Future restoreTrash() async { - try { - await _trashService.restoreTrash(); - state = true; - } catch (error, stack) { - _log.severe("Cannot restore trash", error, stack); - state = false; - } - } -} - -final trashProvider = StateNotifierProvider((ref) { - return TrashNotifier(ref.watch(trashServiceProvider)); -}); diff --git a/mobile/lib/providers/user.provider.dart b/mobile/lib/providers/user.provider.dart index 10dcb2aff5..5a56b65793 100644 --- a/mobile/lib/providers/user.provider.dart +++ b/mobile/lib/providers/user.provider.dart @@ -1,11 +1,9 @@ import 'dart:async'; -import 'package:collection/collection.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/user.model.dart'; import 'package:immich_mobile/domain/services/user.service.dart'; import 'package:immich_mobile/providers/infrastructure/user.provider.dart'; -import 'package:immich_mobile/services/timeline.service.dart'; class CurrentUserProvider extends StateNotifier { CurrentUserProvider(this._userService) : super(null) { @@ -32,28 +30,3 @@ class CurrentUserProvider extends StateNotifier { final currentUserProvider = StateNotifierProvider((ref) { return CurrentUserProvider(ref.watch(userServiceProvider)); }); - -class TimelineUserIdsProvider extends StateNotifier> { - TimelineUserIdsProvider(this._timelineService) : super([]) { - final listEquality = const ListEquality(); - _timelineService.getTimelineUserIds().then((users) => state = users); - streamSub = _timelineService.watchTimelineUserIds().listen((users) { - if (!listEquality.equals(state, users)) { - state = users; - } - }); - } - - late final StreamSubscription> streamSub; - final TimelineService _timelineService; - - @override - void dispose() { - streamSub.cancel(); - super.dispose(); - } -} - -final timelineUsersIdsProvider = StateNotifierProvider>((ref) { - return TimelineUserIdsProvider(ref.watch(timelineServiceProvider)); -}); diff --git a/mobile/lib/providers/websocket.provider.dart b/mobile/lib/providers/websocket.provider.dart index 8059e54605..c79f40a25d 100644 --- a/mobile/lib/providers/websocket.provider.dart +++ b/mobile/lib/providers/websocket.provider.dart @@ -1,60 +1,27 @@ import 'dart:async'; -import 'package:collection/collection.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/store.model.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/infrastructure/repositories/network.repository.dart'; import 'package:immich_mobile/models/server_info/server_version.model.dart'; -import 'package:immich_mobile/providers/asset.provider.dart'; import 'package:immich_mobile/providers/auth.provider.dart'; import 'package:immich_mobile/providers/background_sync.provider.dart'; -import 'package:immich_mobile/providers/db.provider.dart'; import 'package:immich_mobile/providers/server_info.provider.dart'; -import 'package:immich_mobile/services/sync.service.dart'; import 'package:immich_mobile/utils/debounce.dart'; import 'package:immich_mobile/utils/debug_print.dart'; import 'package:logging/logging.dart'; import 'package:openapi/api.dart'; import 'package:socket_io_client/socket_io_client.dart'; -enum PendingAction { assetDelete, assetUploaded, assetHidden, assetTrash } - -class PendingChange { - final String id; - final PendingAction action; - final dynamic value; - - const PendingChange(this.id, this.action, this.value); - - @override - String toString() => 'PendingChange(id: $id, action: $action, value: $value)'; - - @override - bool operator ==(Object other) { - if (identical(this, other)) return true; - - return other is PendingChange && other.id == id && other.action == action; - } - - @override - int get hashCode => id.hashCode ^ action.hashCode; -} - class WebsocketState { final Socket? socket; final bool isConnected; - final List pendingChanges; - const WebsocketState({this.socket, required this.isConnected, required this.pendingChanges}); + const WebsocketState({this.socket, required this.isConnected}); - WebsocketState copyWith({Socket? socket, bool? isConnected, List? pendingChanges}) { - return WebsocketState( - socket: socket ?? this.socket, - isConnected: isConnected ?? this.isConnected, - pendingChanges: pendingChanges ?? this.pendingChanges, - ); + WebsocketState copyWith({Socket? socket, bool? isConnected}) { + return WebsocketState(socket: socket ?? this.socket, isConnected: isConnected ?? this.isConnected); } @override @@ -72,11 +39,10 @@ class WebsocketState { } class WebsocketNotifier extends StateNotifier { - WebsocketNotifier(this._ref) : super(const WebsocketState(socket: null, isConnected: false, pendingChanges: [])); + WebsocketNotifier(this._ref) : super(const WebsocketState(socket: null, isConnected: false)); final _log = Logger('WebsocketNotifier'); final Ref _ref; - final Debouncer _debounce = Debouncer(interval: const Duration(milliseconds: 500)); final Debouncer _batchDebouncer = Debouncer( interval: const Duration(seconds: 5), @@ -115,32 +81,21 @@ class WebsocketNotifier extends StateNotifier { socket.onConnect((_) { dPrint(() => "Established Websocket Connection"); - state = WebsocketState(isConnected: true, socket: socket, pendingChanges: state.pendingChanges); + state = WebsocketState(isConnected: true, socket: socket); }); socket.onDisconnect((_) { dPrint(() => "Disconnect to Websocket Connection"); - state = WebsocketState(isConnected: false, socket: null, pendingChanges: state.pendingChanges); + state = const WebsocketState(isConnected: false, socket: null); }); socket.on('error', (errorMessage) { _log.severe("Websocket Error - $errorMessage"); - state = WebsocketState(isConnected: false, socket: null, pendingChanges: state.pendingChanges); + state = const WebsocketState(isConnected: false, socket: null); }); - if (!Store.isBetaTimelineEnabled) { - socket.on('on_upload_success', _handleOnUploadSuccess); - socket.on('on_asset_delete', _handleOnAssetDelete); - socket.on('on_asset_trash', _handleOnAssetTrash); - socket.on('on_asset_restore', _handleServerUpdates); - socket.on('on_asset_update', _handleServerUpdates); - socket.on('on_asset_stack_update', _handleServerUpdates); - socket.on('on_asset_hidden', _handleOnAssetHidden); - } else { - socket.on('AssetUploadReadyV1', _handleSyncAssetUploadReady); - socket.on('AssetEditReadyV1', _handleSyncAssetEditReady); - } - + socket.on('AssetUploadReadyV1', _handleSyncAssetUploadReady); + socket.on('AssetEditReadyV1', _handleSyncAssetEditReady); socket.on('on_config_update', _handleOnConfigUpdate); socket.on('on_new_release', _handleReleaseUpdates); } catch (e) { @@ -155,46 +110,7 @@ class WebsocketNotifier extends StateNotifier { _batchedAssetUploadReady.clear(); state.socket?.dispose(); - state = WebsocketState(isConnected: false, socket: null, pendingChanges: state.pendingChanges); - } - - void stopListenToEvent(String eventName) { - state.socket?.off(eventName); - } - - void stopListenToOldEvents() { - state.socket?.off('on_upload_success'); - state.socket?.off('on_asset_delete'); - state.socket?.off('on_asset_trash'); - state.socket?.off('on_asset_restore'); - state.socket?.off('on_asset_update'); - state.socket?.off('on_asset_stack_update'); - state.socket?.off('on_asset_hidden'); - } - - void startListeningToOldEvents() { - state.socket?.on('on_upload_success', _handleOnUploadSuccess); - state.socket?.on('on_asset_delete', _handleOnAssetDelete); - state.socket?.on('on_asset_trash', _handleOnAssetTrash); - state.socket?.on('on_asset_restore', _handleServerUpdates); - state.socket?.on('on_asset_update', _handleServerUpdates); - state.socket?.on('on_asset_stack_update', _handleServerUpdates); - state.socket?.on('on_asset_hidden', _handleOnAssetHidden); - } - - void stopListeningToBetaEvents() { - state.socket?.off('AssetUploadReadyV1'); - state.socket?.off('AssetEditReadyV1'); - } - - void startListeningToBetaEvents() { - state.socket?.on('AssetUploadReadyV1', _handleSyncAssetUploadReady); - state.socket?.on('AssetEditReadyV1', _handleSyncAssetEditReady); - } - - void listenUploadEvent() { - dPrint(() => "Start listening to event on_upload_success"); - state.socket?.on('on_upload_success', _handleOnUploadSuccess); + state = const WebsocketState(isConnected: false, socket: null); } Future waitForEvent(String event, bool Function(dynamic)? predicate, Duration timeout) { @@ -218,89 +134,11 @@ class WebsocketNotifier extends StateNotifier { ); } - void addPendingChange(PendingAction action, dynamic value) { - final now = DateTime.now(); - state = state.copyWith( - pendingChanges: [...state.pendingChanges, PendingChange(now.millisecondsSinceEpoch.toString(), action, value)], - ); - _debounce.run(handlePendingChanges); - } - - Future _handlePendingTrashes() async { - final trashChanges = state.pendingChanges.where((c) => c.action == PendingAction.assetTrash).toList(); - if (trashChanges.isNotEmpty) { - List remoteIds = trashChanges.expand((a) => (a.value as List).map((e) => e.toString())).toList(); - - await _ref.read(syncServiceProvider).handleRemoteAssetRemoval(remoteIds); - await _ref.read(assetProvider.notifier).getAllAsset(); - - state = state.copyWith(pendingChanges: state.pendingChanges.whereNot((c) => trashChanges.contains(c)).toList()); - } - } - - Future _handlePendingDeletes() async { - final deleteChanges = state.pendingChanges.where((c) => c.action == PendingAction.assetDelete).toList(); - if (deleteChanges.isNotEmpty) { - List remoteIds = deleteChanges.map((a) => a.value.toString()).toList(); - await _ref.read(syncServiceProvider).handleRemoteAssetRemoval(remoteIds); - state = state.copyWith(pendingChanges: state.pendingChanges.whereNot((c) => deleteChanges.contains(c)).toList()); - } - } - - Future _handlePendingUploaded() async { - final uploadedChanges = state.pendingChanges.where((c) => c.action == PendingAction.assetUploaded).toList(); - if (uploadedChanges.isNotEmpty) { - List remoteAssets = uploadedChanges.map((a) => AssetResponseDto.fromJson(a.value)).toList(); - for (final dto in remoteAssets) { - if (dto != null) { - final newAsset = Asset.remote(dto); - await _ref.watch(assetProvider.notifier).onNewAssetUploaded(newAsset); - } - } - state = state.copyWith( - pendingChanges: state.pendingChanges.whereNot((c) => uploadedChanges.contains(c)).toList(), - ); - } - } - - Future _handlingPendingHidden() async { - final hiddenChanges = state.pendingChanges.where((c) => c.action == PendingAction.assetHidden).toList(); - if (hiddenChanges.isNotEmpty) { - List remoteIds = hiddenChanges.map((a) => a.value.toString()).toList(); - final db = _ref.watch(dbProvider); - await db.writeTxn(() => db.assets.deleteAllByRemoteId(remoteIds)); - - state = state.copyWith(pendingChanges: state.pendingChanges.whereNot((c) => hiddenChanges.contains(c)).toList()); - } - } - - Future handlePendingChanges() async { - await _handlePendingUploaded(); - await _handlePendingDeletes(); - await _handlingPendingHidden(); - await _handlePendingTrashes(); - } - void _handleOnConfigUpdate(dynamic _) { _ref.read(serverInfoProvider.notifier).getServerFeatures(); _ref.read(serverInfoProvider.notifier).getServerConfig(); } - // Refresh updated assets - void _handleServerUpdates(dynamic _) { - _ref.read(assetProvider.notifier).getAllAsset(); - } - - void _handleOnUploadSuccess(dynamic data) => addPendingChange(PendingAction.assetUploaded, data); - - void _handleOnAssetDelete(dynamic data) => addPendingChange(PendingAction.assetDelete, data); - - void _handleOnAssetTrash(dynamic data) { - addPendingChange(PendingAction.assetTrash, data); - } - - void _handleOnAssetHidden(dynamic data) => addPendingChange(PendingAction.assetHidden, data); - _handleReleaseUpdates(dynamic data) { // Json guard if (data is! Map) { diff --git a/mobile/lib/repositories/album.repository.dart b/mobile/lib/repositories/album.repository.dart deleted file mode 100644 index 2d24004944..0000000000 --- a/mobile/lib/repositories/album.repository.dart +++ /dev/null @@ -1,139 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/domain/models/store.model.dart'; -import 'package:immich_mobile/domain/models/user.model.dart'; -import 'package:immich_mobile/entities/album.entity.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/entities/store.entity.dart'; -import 'package:immich_mobile/infrastructure/entities/user.entity.dart' as entity; -import 'package:immich_mobile/models/albums/album_search.model.dart'; -import 'package:immich_mobile/providers/db.provider.dart'; -import 'package:immich_mobile/repositories/database.repository.dart'; -import 'package:immich_mobile/utils/hash.dart'; -import 'package:isar/isar.dart'; - -enum AlbumSort { remoteId, localId } - -final albumRepositoryProvider = Provider((ref) => AlbumRepository(ref.watch(dbProvider))); - -class AlbumRepository extends DatabaseRepository { - const AlbumRepository(super.db); - - Future count({bool? local}) { - final baseQuery = db.albums.where(); - final QueryBuilder query = switch (local) { - null => baseQuery.noOp(), - true => baseQuery.localIdIsNotNull(), - false => baseQuery.remoteIdIsNotNull(), - }; - return query.count(); - } - - Future create(Album album) => txn(() => db.albums.store(album)); - - Future getByName(String name, {bool? shared, bool? remote, bool? owner}) { - var query = db.albums.filter().nameEqualTo(name); - if (shared != null) { - query = query.sharedEqualTo(shared); - } - final isarUserId = fastHash(Store.get(StoreKey.currentUser).id); - if (owner == true) { - query = query.owner((q) => q.isarIdEqualTo(isarUserId)); - } else if (owner == false) { - query = query.owner((q) => q.not().isarIdEqualTo(isarUserId)); - } - if (remote == true) { - query = query.localIdIsNull(); - } else if (remote == false) { - query = query.remoteIdIsNull(); - } - return query.findFirst(); - } - - Future update(Album album) => txn(() => db.albums.store(album)); - - Future delete(int albumId) => txn(() => db.albums.delete(albumId)); - - Future> getAll({bool? shared, bool? remote, int? ownerId, AlbumSort? sortBy}) { - final baseQuery = db.albums.where(); - final QueryBuilder afterWhere; - if (remote == null) { - afterWhere = baseQuery.noOp(); - } else if (remote) { - afterWhere = baseQuery.remoteIdIsNotNull(); - } else { - afterWhere = baseQuery.localIdIsNotNull(); - } - QueryBuilder filterQuery = afterWhere.filter().noOp(); - if (shared != null) { - filterQuery = filterQuery.sharedEqualTo(true); - } - if (ownerId != null) { - filterQuery = filterQuery.owner((q) => q.isarIdEqualTo(ownerId)); - } - final QueryBuilder query = switch (sortBy) { - null => filterQuery.noOp(), - AlbumSort.remoteId => filterQuery.sortByRemoteId(), - AlbumSort.localId => filterQuery.sortByLocalId(), - }; - return query.findAll(); - } - - Future get(int id) => db.albums.get(id); - - Future getByRemoteId(String remoteId) { - return db.albums.filter().remoteIdEqualTo(remoteId).findFirst(); - } - - Future removeUsers(Album album, List users) => - txn(() => album.sharedUsers.update(unlink: users.map(entity.User.fromDto))); - - Future addAssets(Album album, List assets) => txn(() => album.assets.update(link: assets)); - - Future removeAssets(Album album, List assets) => txn(() => album.assets.update(unlink: assets)); - - Future recalculateMetadata(Album album) async { - album.startDate = await album.assets.filter().fileCreatedAtProperty().min(); - album.endDate = await album.assets.filter().fileCreatedAtProperty().max(); - album.lastModifiedAssetTimestamp = await album.assets.filter().updatedAtProperty().max(); - return album; - } - - Future addUsers(Album album, List users) => - txn(() => album.sharedUsers.update(link: users.map(entity.User.fromDto))); - - Future deleteAllLocal() => txn(() => db.albums.where().localIdIsNotNull().deleteAll()); - - Future> search(String searchTerm, QuickFilterMode filterMode) async { - var query = db.albums.filter().nameContains(searchTerm, caseSensitive: false).remoteIdIsNotNull(); - final isarUserId = fastHash(Store.get(StoreKey.currentUser).id); - - switch (filterMode) { - case QuickFilterMode.sharedWithMe: - query = query.owner((q) => q.not().isarIdEqualTo(isarUserId)); - case QuickFilterMode.myAlbums: - query = query.owner((q) => q.isarIdEqualTo(isarUserId)); - case QuickFilterMode.all: - break; - } - - return await query.findAll(); - } - - Future clearTable() async { - await txn(() async { - await db.albums.clear(); - }); - } - - Stream> watchRemoteAlbums() { - return db.albums.where().remoteIdIsNotNull().watch(); - } - - Stream> watchLocalAlbums() { - return db.albums.where().localIdIsNotNull().watch(); - } - - Stream watchAlbum(int id) { - return db.albums.watchObject(id, fireImmediately: true); - } -} diff --git a/mobile/lib/repositories/album_api.repository.dart b/mobile/lib/repositories/album_api.repository.dart deleted file mode 100644 index 367c2447f2..0000000000 --- a/mobile/lib/repositories/album_api.repository.dart +++ /dev/null @@ -1,171 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/constants/enums.dart'; -import 'package:immich_mobile/domain/models/album/album.model.dart' show AlbumAssetOrder, RemoteAlbum; -import 'package:immich_mobile/entities/album.entity.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/infrastructure/entities/user.entity.dart' as entity; -import 'package:immich_mobile/infrastructure/utils/user.converter.dart'; -import 'package:immich_mobile/providers/api.provider.dart'; -import 'package:immich_mobile/repositories/api.repository.dart'; -import 'package:openapi/api.dart'; - -final albumApiRepositoryProvider = Provider((ref) => AlbumApiRepository(ref.watch(apiServiceProvider).albumsApi)); - -class AlbumApiRepository extends ApiRepository { - final AlbumsApi _api; - - AlbumApiRepository(this._api); - - Future get(String id) async { - final dto = await checkNull(_api.getAlbumInfo(id)); - return _toAlbum(dto); - } - - Future> getAll({bool? shared}) async { - final dtos = await checkNull(_api.getAllAlbums(shared: shared)); - return dtos.map(_toAlbum).toList(); - } - - Future create( - String name, { - required Iterable assetIds, - Iterable sharedUserIds = const [], - String? description, - }) async { - final users = sharedUserIds.map((id) => AlbumUserCreateDto(userId: id, role: AlbumUserRole.editor)); - final responseDto = await checkNull( - _api.createAlbum( - CreateAlbumDto( - albumName: name, - description: description, - assetIds: assetIds.toList(), - albumUsers: users.toList(), - ), - ), - ); - return _toAlbum(responseDto); - } - - // TODO: Change name after removing old method - Future createDriftAlbum(String name, {required Iterable assetIds, String? description}) async { - final responseDto = await checkNull( - _api.createAlbum(CreateAlbumDto(albumName: name, description: description, assetIds: assetIds.toList())), - ); - - return _toRemoteAlbum(responseDto); - } - - Future update( - String albumId, { - String? name, - String? thumbnailAssetId, - String? description, - bool? activityEnabled, - SortOrder? sortOrder, - }) async { - AssetOrder? order; - if (sortOrder != null) { - order = sortOrder == SortOrder.asc ? AssetOrder.asc : AssetOrder.desc; - } - - final response = await checkNull( - _api.updateAlbumInfo( - albumId, - UpdateAlbumDto( - albumName: name, - albumThumbnailAssetId: thumbnailAssetId, - description: description, - isActivityEnabled: activityEnabled, - order: order, - ), - ), - ); - - return _toAlbum(response); - } - - Future delete(String albumId) { - return _api.deleteAlbum(albumId); - } - - Future<({List added, List duplicates})> addAssets(String albumId, Iterable assetIds) async { - final response = await checkNull(_api.addAssetsToAlbum(albumId, BulkIdsDto(ids: assetIds.toList()))); - - final List added = []; - final List duplicates = []; - - for (final result in response) { - if (result.success) { - added.add(result.id); - } else if (result.error == BulkIdErrorReason.duplicate) { - duplicates.add(result.id); - } - } - return (added: added, duplicates: duplicates); - } - - Future<({List removed, List failed})> removeAssets(String albumId, Iterable assetIds) async { - final response = await checkNull(_api.removeAssetFromAlbum(albumId, BulkIdsDto(ids: assetIds.toList()))); - final List removed = [], failed = []; - for (final dto in response) { - if (dto.success) { - removed.add(dto.id); - } else { - failed.add(dto.id); - } - } - return (removed: removed, failed: failed); - } - - Future addUsers(String albumId, Iterable userIds) async { - final albumUsers = userIds.map((userId) => AlbumUserAddDto(userId: userId)).toList(); - final response = await checkNull(_api.addUsersToAlbum(albumId, AddUsersDto(albumUsers: albumUsers))); - return _toAlbum(response); - } - - Future removeUser(String albumId, {required String userId}) { - return _api.removeUserFromAlbum(albumId, userId); - } - - static Album _toAlbum(AlbumResponseDto dto) { - final Album album = Album( - remoteId: dto.id, - name: dto.albumName, - createdAt: dto.createdAt, - modifiedAt: dto.updatedAt, - lastModifiedAssetTimestamp: dto.lastModifiedAssetTimestamp, - shared: dto.shared, - startDate: dto.startDate, - description: dto.description, - endDate: dto.endDate, - activityEnabled: dto.isActivityEnabled, - sortOrder: dto.order == AssetOrder.asc ? SortOrder.asc : SortOrder.desc, - ); - album.remoteAssetCount = dto.assetCount; - album.owner.value = entity.User.fromDto(UserConverter.fromSimpleUserDto(dto.owner)); - album.remoteThumbnailAssetId = dto.albumThumbnailAssetId; - final users = dto.albumUsers.map((albumUser) => UserConverter.fromSimpleUserDto(albumUser.user)); - album.sharedUsers.addAll(users.map(entity.User.fromDto)); - final assets = dto.assets.map(Asset.remote).toList(); - album.assets.addAll(assets); - - return album; - } - - static RemoteAlbum _toRemoteAlbum(AlbumResponseDto dto) { - return RemoteAlbum( - id: dto.id, - name: dto.albumName, - ownerId: dto.owner.id, - description: dto.description, - createdAt: dto.createdAt, - updatedAt: dto.updatedAt, - thumbnailAssetId: dto.albumThumbnailAssetId, - isActivityEnabled: dto.isActivityEnabled, - order: dto.order == AssetOrder.asc ? AlbumAssetOrder.asc : AlbumAssetOrder.desc, - assetCount: dto.assetCount, - ownerName: dto.owner.name, - isShared: dto.albumUsers.length > 2, - ); - } -} diff --git a/mobile/lib/repositories/album_media.repository.dart b/mobile/lib/repositories/album_media.repository.dart deleted file mode 100644 index 89860f4e75..0000000000 --- a/mobile/lib/repositories/album_media.repository.dart +++ /dev/null @@ -1,99 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/domain/models/store.model.dart'; -import 'package:immich_mobile/entities/album.entity.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/entities/store.entity.dart'; -import 'package:immich_mobile/infrastructure/entities/user.entity.dart'; -import 'package:immich_mobile/repositories/asset_media.repository.dart'; -import 'package:photo_manager/photo_manager.dart' hide AssetType; - -final albumMediaRepositoryProvider = Provider((ref) => const AlbumMediaRepository()); - -class AlbumMediaRepository { - const AlbumMediaRepository(); - - bool get useCustomFilter => Store.get(StoreKey.photoManagerCustomFilter, true); - - FilterOptionGroup? _getAlbumFilter({ - DateTimeCond? updateTimeCond, - bool? containsPathModified, - List? orderBy, - }) => useCustomFilter - ? FilterOptionGroup( - imageOption: const FilterOption(needTitle: true, sizeConstraint: SizeConstraint(ignoreSize: true)), - videoOption: const FilterOption( - needTitle: true, - sizeConstraint: SizeConstraint(ignoreSize: true), - durationConstraint: DurationConstraint(allowNullable: true), - ), - containsPathModified: containsPathModified ?? false, - createTimeCond: DateTimeCond.def().copyWith(ignore: true), - updateTimeCond: updateTimeCond ?? DateTimeCond.def().copyWith(ignore: true), - orders: orderBy ?? [], - ) - : null; - - Future> getAll() async { - final filter = useCustomFilter - ? CustomFilter.sql(where: '${CustomColumns.base.width} > 0') - : FilterOptionGroup(containsPathModified: true); - - final List assetPathEntities = await PhotoManager.getAssetPathList( - hasAll: true, - filterOption: filter, - ); - return assetPathEntities.map(_toAlbum).toList(); - } - - Future> getAssetIds(String albumId) async { - final album = await AssetPathEntity.fromId(albumId, filterOption: _getAlbumFilter()); - final List assets = await album.getAssetListRange(start: 0, end: 0x7fffffffffffffff); - return assets.map((e) => e.id).toList(); - } - - Future getAssetCount(String albumId) async { - final album = await AssetPathEntity.fromId(albumId, filterOption: _getAlbumFilter()); - return album.assetCountAsync; - } - - Future> getAssets( - String albumId, { - int start = 0, - int end = 0x7fffffffffffffff, - DateTime? modifiedFrom, - DateTime? modifiedUntil, - bool orderByModificationDate = false, - }) async { - final onDevice = await AssetPathEntity.fromId( - albumId, - filterOption: _getAlbumFilter( - updateTimeCond: modifiedFrom == null && modifiedUntil == null - ? null - : DateTimeCond(min: modifiedFrom ?? DateTime.utc(-271820), max: modifiedUntil ?? DateTime.utc(275760)), - orderBy: orderByModificationDate ? [const OrderOption(type: OrderOptionType.updateDate)] : [], - ), - ); - - final List assets = await onDevice.getAssetListRange(start: start, end: end); - return assets.map(AssetMediaRepository.toAsset).toList().cast(); - } - - Future get(String id) async { - final assetPathEntity = await AssetPathEntity.fromId(id, filterOption: _getAlbumFilter(containsPathModified: true)); - return _toAlbum(assetPathEntity); - } - - static Album _toAlbum(AssetPathEntity assetPathEntity) { - final Album album = Album( - name: assetPathEntity.name, - createdAt: assetPathEntity.lastModified?.toUtc() ?? DateTime.now().toUtc(), - modifiedAt: assetPathEntity.lastModified?.toUtc() ?? DateTime.now().toUtc(), - shared: false, - activityEnabled: false, - ); - album.owner.value = User.fromDto(Store.get(StoreKey.currentUser)); - album.localId = assetPathEntity.id; - album.isAll = assetPathEntity.isAll; - return album; - } -} diff --git a/mobile/lib/repositories/asset.repository.dart b/mobile/lib/repositories/asset.repository.dart deleted file mode 100644 index 79af8b4921..0000000000 --- a/mobile/lib/repositories/asset.repository.dart +++ /dev/null @@ -1,220 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/constants/enums.dart'; -import 'package:immich_mobile/entities/album.entity.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/entities/duplicated_asset.entity.dart'; -import 'package:immich_mobile/infrastructure/entities/exif.entity.dart'; -import 'package:immich_mobile/providers/db.provider.dart'; -import 'package:immich_mobile/repositories/database.repository.dart'; -import 'package:immich_mobile/utils/hash.dart'; -import 'package:isar/isar.dart'; - -enum AssetSort { checksum, ownerIdChecksum } - -final assetRepositoryProvider = Provider((ref) => AssetRepository(ref.watch(dbProvider))); - -class AssetRepository extends DatabaseRepository { - const AssetRepository(super.db); - - Future> getByAlbum( - Album album, { - Iterable notOwnedBy = const [], - String? ownerId, - AssetState? state, - AssetSort? sortBy, - }) { - var query = album.assets.filter(); - final isarUserIds = notOwnedBy.map(fastHash).toList(); - if (notOwnedBy.length == 1) { - query = query.not().ownerIdEqualTo(isarUserIds.first); - } else if (notOwnedBy.isNotEmpty) { - query = query.not().anyOf(isarUserIds, (q, int id) => q.ownerIdEqualTo(id)); - } - if (ownerId != null) { - query = query.ownerIdEqualTo(fastHash(ownerId)); - } - - if (state != null) { - query = switch (state) { - AssetState.local => query.remoteIdIsNull(), - AssetState.remote => query.localIdIsNull(), - AssetState.merged => query.localIdIsNotNull().remoteIdIsNotNull(), - }; - } - - final QueryBuilder sortedQuery = switch (sortBy) { - null => query.noOp(), - AssetSort.checksum => query.sortByChecksum(), - AssetSort.ownerIdChecksum => query.sortByOwnerId().thenByChecksum(), - }; - - return sortedQuery.findAll(); - } - - Future deleteByIds(List ids) => txn(() async { - await db.assets.deleteAll(ids); - await db.exifInfos.deleteAll(ids); - }); - - Future getByRemoteId(String id) => db.assets.getByRemoteId(id); - - Future> getAllByRemoteId(Iterable ids, {AssetState? state}) async { - if (ids.isEmpty) { - return []; - } - - return _getAllByRemoteIdImpl(ids, state).findAll(); - } - - QueryBuilder _getAllByRemoteIdImpl(Iterable ids, AssetState? state) { - final query = db.assets.remote(ids).filter(); - return switch (state) { - null => query.noOp(), - AssetState.local => query.remoteIdIsNull(), - AssetState.remote => query.localIdIsNull(), - AssetState.merged => query.localIdIsNotEmpty().remoteIdIsNotNull(), - }; - } - - Future> getAll({required String ownerId, AssetState? state, AssetSort? sortBy, int? limit}) { - final baseQuery = db.assets.where(); - final isarUserIds = fastHash(ownerId); - final QueryBuilder filteredQuery = switch (state) { - null => baseQuery.ownerIdEqualToAnyChecksum(isarUserIds).noOp(), - AssetState.local => baseQuery.remoteIdIsNull().filter().localIdIsNotNull().ownerIdEqualTo(isarUserIds), - AssetState.remote => baseQuery.localIdIsNull().filter().remoteIdIsNotNull().ownerIdEqualTo(isarUserIds), - AssetState.merged => - baseQuery.ownerIdEqualToAnyChecksum(isarUserIds).filter().remoteIdIsNotNull().localIdIsNotNull(), - }; - - final QueryBuilder query = switch (sortBy) { - null => filteredQuery.noOp(), - AssetSort.checksum => filteredQuery.sortByChecksum(), - AssetSort.ownerIdChecksum => filteredQuery.sortByOwnerId().thenByChecksum(), - }; - - return limit == null ? query.findAll() : query.limit(limit).findAll(); - } - - Future> updateAll(List assets) async { - await txn(() => db.assets.putAll(assets)); - return assets; - } - - Future> getMatches({ - required List assets, - required String ownerId, - AssetState? state, - int limit = 100, - }) { - final baseQuery = db.assets.where(); - final QueryBuilder query = switch (state) { - null => baseQuery.noOp(), - AssetState.local => baseQuery.remoteIdIsNull().filter().localIdIsNotNull(), - AssetState.remote => baseQuery.localIdIsNull().filter().remoteIdIsNotNull(), - AssetState.merged => baseQuery.localIdIsNotNull().filter().remoteIdIsNotNull(), - }; - return _getMatchesImpl(query, fastHash(ownerId), assets, limit); - } - - Future update(Asset asset) async { - await txn(() => asset.put(db)); - return asset; - } - - Future upsertDuplicatedAssets(Iterable duplicatedAssets) => - txn(() => db.duplicatedAssets.putAll(duplicatedAssets.map(DuplicatedAsset.new).toList())); - - Future> getAllDuplicatedAssetIds() => db.duplicatedAssets.where().idProperty().findAll(); - - Future getByOwnerIdChecksum(int ownerId, String checksum) => - db.assets.getByOwnerIdChecksum(ownerId, checksum); - - Future> getAllByOwnerIdChecksum(List ownerIds, List checksums) => - db.assets.getAllByOwnerIdChecksum(ownerIds, checksums); - - Future> getAllLocal() => db.assets.where().localIdIsNotNull().findAll(); - - Future deleteAllByRemoteId(List ids, {AssetState? state}) => - txn(() => _getAllByRemoteIdImpl(ids, state).deleteAll()); - - Future> getStackAssets(String stackId) { - return db.assets - .filter() - .isArchivedEqualTo(false) - .isTrashedEqualTo(false) - .stackIdEqualTo(stackId) - // orders primary asset first as its ID is null - .sortByStackPrimaryAssetId() - .thenByFileCreatedAtDesc() - .findAll(); - } - - Future clearTable() async { - await txn(() async { - await db.assets.clear(); - }); - } - - Stream watchAsset(int id, {bool fireImmediately = false}) { - return db.assets.watchObject(id, fireImmediately: fireImmediately); - } - - Future> getTrashAssets(String userId) { - return db.assets - .where() - .remoteIdIsNotNull() - .filter() - .ownerIdEqualTo(fastHash(userId)) - .isTrashedEqualTo(true) - .findAll(); - } - - Future> getRecentlyTakenAssets(String userId) { - return db.assets - .where() - .ownerIdEqualToAnyChecksum(fastHash(userId)) - .filter() - .visibilityEqualTo(AssetVisibilityEnum.timeline) - .sortByFileCreatedAtDesc() - .findAll(); - } - - Future> getMotionAssets(String userId) { - return db.assets - .where() - .ownerIdEqualToAnyChecksum(fastHash(userId)) - .filter() - .visibilityEqualTo(AssetVisibilityEnum.timeline) - .livePhotoVideoIdIsNotNull() - .findAll(); - } -} - -Future> _getMatchesImpl( - QueryBuilder query, - int ownerId, - List assets, - int limit, -) => query - .ownerIdEqualTo(ownerId) - .anyOf( - assets, - (q, Asset a) => q - .fileNameEqualTo(a.fileName) - .and() - .durationInSecondsEqualTo(a.durationInSeconds) - .and() - .fileCreatedAtBetween( - a.fileCreatedAt.subtract(const Duration(hours: 12)), - a.fileCreatedAt.add(const Duration(hours: 12)), - ) - .and() - .not() - .checksumEqualTo(a.checksum), - ) - .sortByFileName() - .thenByFileCreatedAt() - .thenByFileModifiedAt() - .limit(limit) - .findAll(); diff --git a/mobile/lib/repositories/asset_api.repository.dart b/mobile/lib/repositories/asset_api.repository.dart index d66b39ecde..2943177d60 100644 --- a/mobile/lib/repositories/asset_api.repository.dart +++ b/mobile/lib/repositories/asset_api.repository.dart @@ -3,7 +3,6 @@ import 'package:http/http.dart'; import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/domain/models/asset_edit.model.dart' hide AssetEditAction; import 'package:immich_mobile/domain/models/stack.model.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; import 'package:immich_mobile/providers/api.provider.dart'; import 'package:immich_mobile/repositories/api.repository.dart'; import 'package:maplibre_gl/maplibre_gl.dart'; @@ -12,7 +11,6 @@ import 'package:openapi/api.dart'; final assetApiRepositoryProvider = Provider( (ref) => AssetApiRepository( ref.watch(apiServiceProvider).assetsApi, - ref.watch(apiServiceProvider).searchApi, ref.watch(apiServiceProvider).stacksApi, ref.watch(apiServiceProvider).trashApi, ), @@ -20,32 +18,10 @@ final assetApiRepositoryProvider = Provider( class AssetApiRepository extends ApiRepository { final AssetsApi _api; - final SearchApi _searchApi; final StacksApi _stacksApi; final TrashApi _trashApi; - AssetApiRepository(this._api, this._searchApi, this._stacksApi, this._trashApi); - - Future update(String id, {String? description}) async { - final response = await checkNull(_api.updateAsset(id, UpdateAssetDto(description: description))); - return Asset.remote(response); - } - - Future> search({List personIds = const []}) async { - // TODO this always fetches all assets, change API and usage to actually do pagination - final List result = []; - bool hasNext = true; - int currentPage = 1; - while (hasNext) { - final response = await checkNull( - _searchApi.searchAssets(MetadataSearchDto(personIds: personIds, page: currentPage, size: 1000)), - ); - result.addAll(response.assets.items.map(Asset.remote)); - hasNext = response.assets.nextPage != null; - currentPage++; - } - return result; - } + AssetApiRepository(this._api, this._stacksApi, this._trashApi); Future delete(List ids, bool force) async { return _api.deleteAssets(AssetBulkDeleteDto(ids: ids, force: force)); diff --git a/mobile/lib/repositories/asset_media.repository.dart b/mobile/lib/repositories/asset_media.repository.dart index fecfe6df4d..a2d8bfe162 100644 --- a/mobile/lib/repositories/asset_media.repository.dart +++ b/mobile/lib/repositories/asset_media.repository.dart @@ -5,15 +5,10 @@ import 'package:device_info_plus/device_info_plus.dart'; import 'package:flutter/widgets.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; -import 'package:immich_mobile/domain/models/exif.model.dart'; -import 'package:immich_mobile/domain/models/store.model.dart'; -import 'package:immich_mobile/entities/asset.entity.dart' as asset_entity; -import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/platform_extensions.dart'; import 'package:immich_mobile/extensions/response_extensions.dart'; import 'package:immich_mobile/repositories/asset_api.repository.dart'; -import 'package:immich_mobile/utils/hash.dart'; import 'package:logging/logging.dart'; import 'package:path_provider/path_provider.dart'; import 'package:photo_manager/photo_manager.dart'; @@ -50,39 +45,9 @@ class AssetMediaRepository { return PhotoManager.editor.deleteWithIds(ids); } - Future get(String id) async { + Future get(String id) async { final entity = await AssetEntity.fromId(id); - return toAsset(entity); - } - - static asset_entity.Asset? toAsset(AssetEntity? local) { - if (local == null) return null; - - final asset_entity.Asset asset = asset_entity.Asset( - checksum: "", - localId: local.id, - ownerId: fastHash(Store.get(StoreKey.currentUser).id), - fileCreatedAt: local.createDateTime, - fileModifiedAt: local.modifiedDateTime, - updatedAt: local.modifiedDateTime, - durationInSeconds: local.duration, - type: asset_entity.AssetType.values[local.typeInt], - fileName: local.title!, - width: local.width, - height: local.height, - isFavorite: local.isFavorite, - ); - - if (asset.fileCreatedAt.year == 1970) { - asset.fileCreatedAt = asset.fileModifiedAt; - } - - if (local.latitude != null) { - asset.exifInfo = ExifInfo(latitude: local.latitude, longitude: local.longitude); - } - - asset.local = local; - return asset; + return entity; } Future getOriginalFilename(String id) async { diff --git a/mobile/lib/repositories/auth.repository.dart b/mobile/lib/repositories/auth.repository.dart index a8544ef6c0..c16b728ae5 100644 --- a/mobile/lib/repositories/auth.repository.dart +++ b/mobile/lib/repositories/auth.repository.dart @@ -2,40 +2,21 @@ import 'dart:convert'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/store.model.dart'; -import 'package:immich_mobile/entities/album.entity.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/entities/etag.entity.dart'; import 'package:immich_mobile/entities/store.entity.dart'; -import 'package:immich_mobile/infrastructure/entities/exif.entity.dart'; -import 'package:immich_mobile/infrastructure/entities/user.entity.dart'; import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/sync_stream.repository.dart'; import 'package:immich_mobile/models/auth/auxilary_endpoint.model.dart'; -import 'package:immich_mobile/providers/db.provider.dart'; import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; -import 'package:immich_mobile/repositories/database.repository.dart'; -final authRepositoryProvider = Provider( - (ref) => AuthRepository(ref.watch(dbProvider), ref.watch(driftProvider)), -); +final authRepositoryProvider = Provider((ref) => AuthRepository(ref.watch(driftProvider))); -class AuthRepository extends DatabaseRepository { +class AuthRepository { final Drift _drift; - const AuthRepository(super.db, this._drift); + const AuthRepository(this._drift); Future clearLocalData() async { await SyncStreamRepository(_drift).reset(); - - return db.writeTxn(() { - return Future.wait([ - db.assets.clear(), - db.exifInfos.clear(), - db.albums.clear(), - db.eTags.clear(), - db.users.clear(), - ]); - }); } bool getEndpointSwitchingFeature() { diff --git a/mobile/lib/repositories/backup.repository.dart b/mobile/lib/repositories/backup.repository.dart deleted file mode 100644 index 6cee6a4427..0000000000 --- a/mobile/lib/repositories/backup.repository.dart +++ /dev/null @@ -1,32 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/backup_album.entity.dart'; -import 'package:immich_mobile/providers/db.provider.dart'; -import 'package:immich_mobile/repositories/database.repository.dart'; -import 'package:isar/isar.dart'; - -enum BackupAlbumSort { id } - -final backupAlbumRepositoryProvider = Provider((ref) => BackupAlbumRepository(ref.watch(dbProvider))); - -class BackupAlbumRepository extends DatabaseRepository { - const BackupAlbumRepository(super.db); - - Future> getAll({BackupAlbumSort? sort}) { - final baseQuery = db.backupAlbums.where(); - final QueryBuilder query = switch (sort) { - null => baseQuery.noOp(), - BackupAlbumSort.id => baseQuery.sortById(), - }; - return query.findAll(); - } - - Future> getIdsBySelection(BackupSelection backup) => - db.backupAlbums.filter().selectionEqualTo(backup).idProperty().findAll(); - - Future> getAllBySelection(BackupSelection backup) => - db.backupAlbums.filter().selectionEqualTo(backup).findAll(); - - Future deleteAll(List ids) => txn(() => db.backupAlbums.deleteAll(ids)); - - Future updateAll(List backupAlbums) => txn(() => db.backupAlbums.putAll(backupAlbums)); -} diff --git a/mobile/lib/repositories/database.repository.dart b/mobile/lib/repositories/database.repository.dart deleted file mode 100644 index 71c15e1c40..0000000000 --- a/mobile/lib/repositories/database.repository.dart +++ /dev/null @@ -1,25 +0,0 @@ -import 'dart:async'; -import 'package:immich_mobile/interfaces/database.interface.dart'; -import 'package:isar/isar.dart'; - -/// copied from Isar; needed to check if an async transaction is already active -const Symbol _zoneTxn = #zoneTxn; - -abstract class DatabaseRepository implements IDatabaseRepository { - final Isar db; - const DatabaseRepository(this.db); - - bool get inTxn => Zone.current[_zoneTxn] != null; - - Future txn(Future Function() callback) => inTxn ? callback() : transaction(callback); - - @override - Future transaction(Future Function() callback) => db.writeTxn(callback); -} - -extension Asd on QueryBuilder { - QueryBuilder noOp() { - // ignore: invalid_use_of_protected_member - return QueryBuilder.apply(this, (query) => query); - } -} diff --git a/mobile/lib/repositories/etag.repository.dart b/mobile/lib/repositories/etag.repository.dart deleted file mode 100644 index 768d95b95c..0000000000 --- a/mobile/lib/repositories/etag.repository.dart +++ /dev/null @@ -1,27 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/etag.entity.dart'; -import 'package:immich_mobile/providers/db.provider.dart'; -import 'package:immich_mobile/repositories/database.repository.dart'; -import 'package:isar/isar.dart'; - -final etagRepositoryProvider = Provider((ref) => ETagRepository(ref.watch(dbProvider))); - -class ETagRepository extends DatabaseRepository { - const ETagRepository(super.db); - - Future> getAllIds() => db.eTags.where().idProperty().findAll(); - - Future get(String id) => db.eTags.getById(id); - - Future upsertAll(List etags) => txn(() => db.eTags.putAll(etags)); - - Future deleteByIds(List ids) => txn(() => db.eTags.deleteAllById(ids)); - - Future getById(String id) => db.eTags.getById(id); - - Future clearTable() async { - await txn(() async { - await db.eTags.clear(); - }); - } -} diff --git a/mobile/lib/repositories/file_media.repository.dart b/mobile/lib/repositories/file_media.repository.dart index f5cdb6d5c0..c54813a757 100644 --- a/mobile/lib/repositories/file_media.repository.dart +++ b/mobile/lib/repositories/file_media.repository.dart @@ -3,18 +3,12 @@ import 'dart:io'; import 'package:flutter/foundation.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; -import 'package:immich_mobile/entities/asset.entity.dart' hide AssetType; -import 'package:immich_mobile/repositories/asset_media.repository.dart'; import 'package:photo_manager/photo_manager.dart' hide AssetType; final fileMediaRepositoryProvider = Provider((ref) => const FileMediaRepository()); class FileMediaRepository { const FileMediaRepository(); - Future saveImage(Uint8List data, {required String title, String? relativePath}) async { - final entity = await PhotoManager.editor.saveImage(data, filename: title, title: title, relativePath: relativePath); - return AssetMediaRepository.toAsset(entity); - } Future saveLocalAsset(Uint8List data, {required String title, String? relativePath}) async { final entity = await PhotoManager.editor.saveImage(data, filename: title, title: title, relativePath: relativePath); @@ -30,24 +24,18 @@ class FileMediaRepository { ); } - Future saveImageWithFile(String filePath, {String? title, String? relativePath}) async { + Future saveImageWithFile(String filePath, {String? title, String? relativePath}) async { final entity = await PhotoManager.editor.saveImageWithPath(filePath, title: title, relativePath: relativePath); - return AssetMediaRepository.toAsset(entity); + return entity; } - Future saveLivePhoto({required File image, required File video, required String title}) async { + Future saveLivePhoto({required File image, required File video, required String title}) async { final entity = await PhotoManager.editor.darwin.saveLivePhoto(imageFile: image, videoFile: video, title: title); - return AssetMediaRepository.toAsset(entity); + return entity; } - Future saveVideo(File file, {required String title, String? relativePath}) async { + Future saveVideo(File file, {required String title, String? relativePath}) async { final entity = await PhotoManager.editor.saveVideo(file, title: title, relativePath: relativePath); - return AssetMediaRepository.toAsset(entity); + return entity; } - - Future clearFileCache() => PhotoManager.clearFileCache(); - - Future enableBackgroundAccess() => PhotoManager.setIgnorePermissionCheck(true); - - Future requestExtendedPermissions() => PhotoManager.requestPermissionExtend(); } diff --git a/mobile/lib/repositories/partner.repository.dart b/mobile/lib/repositories/partner.repository.dart deleted file mode 100644 index 7f5ce62e0c..0000000000 --- a/mobile/lib/repositories/partner.repository.dart +++ /dev/null @@ -1,34 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/domain/models/user.model.dart'; -import 'package:immich_mobile/infrastructure/entities/user.entity.dart' as entity; -import 'package:immich_mobile/providers/db.provider.dart'; -import 'package:immich_mobile/repositories/database.repository.dart'; -import 'package:isar/isar.dart'; - -final partnerRepositoryProvider = Provider((ref) => PartnerRepository(ref.watch(dbProvider))); - -class PartnerRepository extends DatabaseRepository { - const PartnerRepository(super.db); - - Future> getSharedBy() async { - return (await db.users.filter().isPartnerSharedByEqualTo(true).sortById().findAll()).map((u) => u.toDto()).toList(); - } - - Future> getSharedWith() async { - return (await db.users.filter().isPartnerSharedWithEqualTo(true).sortById().findAll()) - .map((u) => u.toDto()) - .toList(); - } - - Stream> watchSharedBy() { - return (db.users.filter().isPartnerSharedByEqualTo(true).sortById().watch()).map( - (users) => users.map((u) => u.toDto()).toList(), - ); - } - - Stream> watchSharedWith() { - return (db.users.filter().isPartnerSharedWithEqualTo(true).sortById().watch()).map( - (users) => users.map((u) => u.toDto()).toList(), - ); - } -} diff --git a/mobile/lib/repositories/timeline.repository.dart b/mobile/lib/repositories/timeline.repository.dart deleted file mode 100644 index c8c173b6f6..0000000000 --- a/mobile/lib/repositories/timeline.repository.dart +++ /dev/null @@ -1,146 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/constants/enums.dart'; -import 'package:immich_mobile/entities/album.entity.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/infrastructure/entities/user.entity.dart'; -import 'package:immich_mobile/providers/db.provider.dart'; -import 'package:immich_mobile/repositories/database.repository.dart'; -import 'package:immich_mobile/utils/hash.dart'; -import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart'; -import 'package:isar/isar.dart'; - -final timelineRepositoryProvider = Provider((ref) => TimelineRepository(ref.watch(dbProvider))); - -class TimelineRepository extends DatabaseRepository { - const TimelineRepository(super.db); - - Future> getTimelineUserIds(String id) { - return db.users.filter().inTimelineEqualTo(true).or().idEqualTo(id).idProperty().findAll(); - } - - Stream> watchTimelineUsers(String id) { - return db.users.filter().inTimelineEqualTo(true).or().idEqualTo(id).idProperty().watch(); - } - - Stream watchArchiveTimeline(String userId) { - final query = db.assets - .where() - .ownerIdEqualToAnyChecksum(fastHash(userId)) - .filter() - .isTrashedEqualTo(false) - .visibilityEqualTo(AssetVisibilityEnum.archive) - .sortByFileCreatedAtDesc(); - - return _watchRenderList(query, GroupAssetsBy.none); - } - - Stream watchFavoriteTimeline(String userId) { - final query = db.assets - .where() - .ownerIdEqualToAnyChecksum(fastHash(userId)) - .filter() - .isFavoriteEqualTo(true) - .not() - .visibilityEqualTo(AssetVisibilityEnum.locked) - .isTrashedEqualTo(false) - .sortByFileCreatedAtDesc(); - - return _watchRenderList(query, GroupAssetsBy.none); - } - - Stream watchAlbumTimeline(Album album, GroupAssetsBy groupAssetByOption) { - final query = album.assets.filter().isTrashedEqualTo(false).not().visibilityEqualTo(AssetVisibilityEnum.locked); - - final withSortedOption = switch (album.sortOrder) { - SortOrder.asc => query.sortByFileCreatedAt(), - SortOrder.desc => query.sortByFileCreatedAtDesc(), - }; - - return _watchRenderList(withSortedOption, groupAssetByOption); - } - - Stream watchTrashTimeline(String userId) { - final query = db.assets.filter().ownerIdEqualTo(fastHash(userId)).isTrashedEqualTo(true).sortByFileCreatedAtDesc(); - - return _watchRenderList(query, GroupAssetsBy.none); - } - - Stream watchAllVideosTimeline(String userId) { - final query = db.assets - .where() - .ownerIdEqualToAnyChecksum(fastHash(userId)) - .filter() - .isTrashedEqualTo(false) - .visibilityEqualTo(AssetVisibilityEnum.timeline) - .typeEqualTo(AssetType.video) - .sortByFileCreatedAtDesc(); - - return _watchRenderList(query, GroupAssetsBy.none); - } - - Stream watchHomeTimeline(String userId, GroupAssetsBy groupAssetByOption) { - final query = db.assets - .where() - .ownerIdEqualToAnyChecksum(fastHash(userId)) - .filter() - .isTrashedEqualTo(false) - .stackPrimaryAssetIdIsNull() - .visibilityEqualTo(AssetVisibilityEnum.timeline) - .sortByFileCreatedAtDesc(); - - return _watchRenderList(query, groupAssetByOption); - } - - Stream watchMultiUsersTimeline(List userIds, GroupAssetsBy groupAssetByOption) { - final isarUserIds = userIds.map(fastHash).toList(); - final query = db.assets - .where() - .anyOf(isarUserIds, (qb, id) => qb.ownerIdEqualToAnyChecksum(id)) - .filter() - .isTrashedEqualTo(false) - .visibilityEqualTo(AssetVisibilityEnum.timeline) - .stackPrimaryAssetIdIsNull() - .sortByFileCreatedAtDesc(); - return _watchRenderList(query, groupAssetByOption); - } - - Future getTimelineFromAssets(List assets, GroupAssetsBy getGroupByOption) { - return RenderList.fromAssets(assets, getGroupByOption); - } - - Stream watchAssetSelectionTimeline(String userId) { - final query = db.assets - .where() - .remoteIdIsNotNull() - .filter() - .ownerIdEqualTo(fastHash(userId)) - .visibilityEqualTo(AssetVisibilityEnum.timeline) - .isTrashedEqualTo(false) - .stackPrimaryAssetIdIsNull() - .sortByFileCreatedAtDesc(); - - return _watchRenderList(query, GroupAssetsBy.none); - } - - Stream watchLockedTimeline(String userId, GroupAssetsBy getGroupByOption) { - final query = db.assets - .where() - .ownerIdEqualToAnyChecksum(fastHash(userId)) - .filter() - .visibilityEqualTo(AssetVisibilityEnum.locked) - .isTrashedEqualTo(false) - .sortByFileCreatedAtDesc(); - - return _watchRenderList(query, getGroupByOption); - } - - Stream _watchRenderList( - QueryBuilder query, - GroupAssetsBy groupAssetsBy, - ) async* { - yield await RenderList.fromQuery(query, groupAssetsBy); - await for (final _ in query.watchLazy()) { - yield await RenderList.fromQuery(query, groupAssetsBy); - } - } -} diff --git a/mobile/lib/routing/app_navigation_observer.dart b/mobile/lib/routing/app_navigation_observer.dart index b05a28172d..b6b08d7831 100644 --- a/mobile/lib/routing/app_navigation_observer.dart +++ b/mobile/lib/routing/app_navigation_observer.dart @@ -19,7 +19,6 @@ class AppNavigationObserver extends AutoRouterObserver { @override void didPush(Route route, Route? previousRoute) { - _handleLockedViewState(route, previousRoute); _handleDriftLockedFolderState(route, previousRoute); Future(() { ref.read(currentRouteNameProvider.notifier).state = route.settings.name; @@ -28,21 +27,6 @@ class AppNavigationObserver extends AutoRouterObserver { }); } - _handleLockedViewState(Route route, Route? previousRoute) { - final isInLockedView = ref.read(inLockedViewProvider); - final isFromLockedViewToDetailView = - route.settings.name == GalleryViewerRoute.name && previousRoute?.settings.name == LockedRoute.name; - - final isFromDetailViewToInfoPanelView = - route.settings.name == null && previousRoute?.settings.name == GalleryViewerRoute.name && isInLockedView; - - if (route.settings.name == LockedRoute.name || isFromLockedViewToDetailView || isFromDetailViewToInfoPanelView) { - Future(() => ref.read(inLockedViewProvider.notifier).state = true); - } else { - Future(() => ref.read(inLockedViewProvider.notifier).state = false); - } - } - _handleDriftLockedFolderState(Route route, Route? previousRoute) { final isInLockedView = ref.read(inLockedViewProvider); final isFromLockedViewToDetailView = diff --git a/mobile/lib/routing/backup_permission_guard.dart b/mobile/lib/routing/backup_permission_guard.dart deleted file mode 100644 index f52516f2e5..0000000000 --- a/mobile/lib/routing/backup_permission_guard.dart +++ /dev/null @@ -1,21 +0,0 @@ -import 'dart:async'; - -import 'package:auto_route/auto_route.dart'; -import 'package:immich_mobile/providers/gallery_permission.provider.dart'; -import 'package:immich_mobile/routing/router.dart'; - -class BackupPermissionGuard extends AutoRouteGuard { - final GalleryPermissionNotifier _permission; - - const BackupPermissionGuard(this._permission); - - @override - void onNavigation(NavigationResolver resolver, StackRouter router) async { - final p = _permission.hasPermission; - if (p) { - resolver.next(true); - } else { - unawaited(router.push(const PermissionOnboardingRoute())); - } - } -} diff --git a/mobile/lib/routing/gallery_guard.dart b/mobile/lib/routing/gallery_guard.dart deleted file mode 100644 index 6a4b1bddab..0000000000 --- a/mobile/lib/routing/gallery_guard.dart +++ /dev/null @@ -1,34 +0,0 @@ -import 'dart:async'; - -import 'package:auto_route/auto_route.dart'; -import 'package:immich_mobile/routing/router.dart'; - -/// Handles duplicate navigation to this route (primarily for deep linking) -class GalleryGuard extends AutoRouteGuard { - const GalleryGuard(); - @override - void onNavigation(NavigationResolver resolver, StackRouter router) async { - final newRouteName = resolver.route.name; - final currentTopRouteName = router.stack.isNotEmpty ? router.stack.last.name : null; - - if (currentTopRouteName == newRouteName) { - // Replace instead of pushing duplicate - final args = resolver.route.args as GalleryViewerRouteArgs; - - unawaited( - router.replace( - GalleryViewerRoute( - renderList: args.renderList, - initialIndex: args.initialIndex, - heroOffset: args.heroOffset, - showStack: args.showStack, - ), - ), - ); - // Prevent further navigation since we replaced the route - resolver.next(false); - return; - } - resolver.next(true); - } -} diff --git a/mobile/lib/routing/router.dart b/mobile/lib/routing/router.dart index 90a17b1617..9c539a37a6 100644 --- a/mobile/lib/routing/router.dart +++ b/mobile/lib/routing/router.dart @@ -10,73 +10,28 @@ import 'package:immich_mobile/domain/models/memory.model.dart'; import 'package:immich_mobile/domain/models/person.model.dart'; import 'package:immich_mobile/domain/models/user.model.dart'; import 'package:immich_mobile/domain/services/timeline.service.dart'; -import 'package:immich_mobile/entities/album.entity.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; import 'package:immich_mobile/models/folder/recursive_folder.model.dart'; -import 'package:immich_mobile/models/memories/memory.model.dart'; -import 'package:immich_mobile/models/search/search_filter.model.dart'; import 'package:immich_mobile/models/shared_link/shared_link.model.dart'; import 'package:immich_mobile/models/upload/share_intent_attachment.model.dart'; -import 'package:immich_mobile/pages/album/album_additional_shared_user_selection.page.dart'; -import 'package:immich_mobile/pages/album/album_asset_selection.page.dart'; -import 'package:immich_mobile/pages/album/album_options.page.dart'; -import 'package:immich_mobile/pages/album/album_shared_user_selection.page.dart'; -import 'package:immich_mobile/pages/album/album_viewer.page.dart'; -import 'package:immich_mobile/pages/albums/albums.page.dart'; -import 'package:immich_mobile/pages/backup/album_preview.page.dart'; -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_options.page.dart'; import 'package:immich_mobile/pages/backup/drift_backup.page.dart'; import 'package:immich_mobile/pages/backup/drift_backup_album_selection.page.dart'; import 'package:immich_mobile/pages/backup/drift_backup_asset_detail.page.dart'; import 'package:immich_mobile/pages/backup/drift_backup_options.page.dart'; import 'package:immich_mobile/pages/backup/drift_upload_detail.page.dart'; -import 'package:immich_mobile/pages/backup/failed_backup_status.page.dart'; -import 'package:immich_mobile/pages/common/activities.page.dart'; import 'package:immich_mobile/pages/common/app_log.page.dart'; import 'package:immich_mobile/pages/common/app_log_detail.page.dart'; -import 'package:immich_mobile/pages/common/change_experience.page.dart'; -import 'package:immich_mobile/pages/common/create_album.page.dart'; -import 'package:immich_mobile/pages/common/gallery_viewer.page.dart'; import 'package:immich_mobile/pages/common/headers_settings.page.dart'; -import 'package:immich_mobile/pages/common/native_video_viewer.page.dart'; import 'package:immich_mobile/pages/common/settings.page.dart'; import 'package:immich_mobile/pages/common/splash_screen.page.dart'; -import 'package:immich_mobile/pages/common/tab_controller.page.dart'; import 'package:immich_mobile/pages/common/tab_shell.page.dart'; -import 'package:immich_mobile/pages/editing/crop.page.dart'; -import 'package:immich_mobile/pages/editing/edit.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/favorite.page.dart'; import 'package:immich_mobile/pages/library/folder/folder.page.dart'; -import 'package:immich_mobile/pages/library/library.page.dart'; -import 'package:immich_mobile/pages/library/local_albums.page.dart'; -import 'package:immich_mobile/pages/library/locked/locked.page.dart'; import 'package:immich_mobile/pages/library/locked/pin_auth.page.dart'; import 'package:immich_mobile/pages/library/partner/drift_partner.page.dart'; -import 'package:immich_mobile/pages/library/partner/partner.page.dart'; -import 'package:immich_mobile/pages/library/partner/partner_detail.page.dart'; -import 'package:immich_mobile/pages/library/people/people_collection.page.dart'; -import 'package:immich_mobile/pages/library/places/places_collection.page.dart'; import 'package:immich_mobile/pages/library/shared_link/shared_link.page.dart'; import 'package:immich_mobile/pages/library/shared_link/shared_link_edit.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/login.page.dart'; -import 'package:immich_mobile/pages/onboarding/permission_onboarding.page.dart'; -import 'package:immich_mobile/pages/photos/memory.page.dart'; -import 'package:immich_mobile/pages/photos/photos.page.dart'; -import 'package:immich_mobile/pages/search/all_motion_videos.page.dart'; -import 'package:immich_mobile/pages/search/all_people.page.dart'; -import 'package:immich_mobile/pages/search/all_places.page.dart'; -import 'package:immich_mobile/pages/search/all_videos.page.dart'; -import 'package:immich_mobile/pages/search/map/map.page.dart'; import 'package:immich_mobile/pages/search/map/map_location_picker.page.dart'; -import 'package:immich_mobile/pages/search/person_result.page.dart'; -import 'package:immich_mobile/pages/search/recently_taken.page.dart'; -import 'package:immich_mobile/pages/search/search.page.dart'; import 'package:immich_mobile/pages/settings/sync_status.page.dart'; import 'package:immich_mobile/pages/share_intent/share_intent.page.dart'; import 'package:immich_mobile/presentation/pages/cleanup_preview.page.dart'; @@ -114,15 +69,11 @@ import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.pag import 'package:immich_mobile/providers/api.provider.dart'; import 'package:immich_mobile/providers/gallery_permission.provider.dart'; import 'package:immich_mobile/routing/auth_guard.dart'; -import 'package:immich_mobile/routing/backup_permission_guard.dart'; -import 'package:immich_mobile/routing/custom_transition_builders.dart'; import 'package:immich_mobile/routing/duplicate_guard.dart'; -import 'package:immich_mobile/routing/gallery_guard.dart'; import 'package:immich_mobile/routing/locked_guard.dart'; import 'package:immich_mobile/services/api.service.dart'; import 'package:immich_mobile/services/local_auth.service.dart'; import 'package:immich_mobile/services/secure_storage.service.dart'; -import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart'; import 'package:maplibre_gl/maplibre_gl.dart'; part 'router.gr.dart'; @@ -140,9 +91,7 @@ final appRouterProvider = Provider( class AppRouter extends RootStackRouter { late final AuthGuard _authGuard; late final DuplicateGuard _duplicateGuard; - late final BackupPermissionGuard _backupPermissionGuard; late final LockedGuard _lockedGuard; - late final GalleryGuard _galleryGuard; AppRouter( ApiService apiService, @@ -153,8 +102,6 @@ class AppRouter extends RootStackRouter { _authGuard = AuthGuard(apiService); _duplicateGuard = const DuplicateGuard(); _lockedGuard = LockedGuard(apiService, secureStorageService, localAuthService); - _backupPermissionGuard = BackupPermissionGuard(galleryPermissionNotifier); - _galleryGuard = const GalleryGuard(); } @override @@ -163,20 +110,8 @@ class AppRouter extends RootStackRouter { @override late final List routes = [ AutoRoute(page: SplashScreenRoute.page, initial: true), - AutoRoute(page: PermissionOnboardingRoute.page, guards: [_authGuard, _duplicateGuard]), AutoRoute(page: LoginRoute.page), AutoRoute(page: ChangePasswordRoute.page), - AutoRoute(page: SearchRoute.page, guards: [_authGuard, _duplicateGuard], maintainState: false), - AutoRoute( - page: TabControllerRoute.page, - guards: [_authGuard, _duplicateGuard], - children: [ - AutoRoute(page: PhotosRoute.page, guards: [_authGuard, _duplicateGuard]), - AutoRoute(page: SearchRoute.page, guards: [_authGuard, _duplicateGuard], maintainState: false), - AutoRoute(page: LibraryRoute.page, guards: [_authGuard, _duplicateGuard]), - AutoRoute(page: AlbumsRoute.page, guards: [_authGuard, _duplicateGuard]), - ], - ), AutoRoute( page: TabShellRoute.page, guards: [_authGuard, _duplicateGuard], @@ -187,105 +122,17 @@ class AppRouter extends RootStackRouter { AutoRoute(page: DriftAlbumsRoute.page, guards: [_authGuard, _duplicateGuard]), ], ), - CustomRoute( - page: GalleryViewerRoute.page, - guards: [_authGuard, _galleryGuard], - transitionsBuilder: CustomTransitionsBuilders.zoomedPage, - ), - AutoRoute(page: BackupControllerRoute.page, guards: [_authGuard, _duplicateGuard, _backupPermissionGuard]), - AutoRoute(page: AllPlacesRoute.page, guards: [_authGuard, _duplicateGuard]), - AutoRoute(page: CreateAlbumRoute.page, guards: [_authGuard, _duplicateGuard]), - AutoRoute(page: EditImageRoute.page), - AutoRoute(page: CropImageRoute.page), - AutoRoute(page: FilterImageRoute.page), AutoRoute(page: ProfilePictureCropRoute.page), - CustomRoute( - page: FavoritesRoute.page, - guards: [_authGuard, _duplicateGuard], - transitionsBuilder: TransitionsBuilders.slideLeft, - ), - AutoRoute(page: AllVideosRoute.page, guards: [_authGuard, _duplicateGuard]), - AutoRoute(page: AllMotionPhotosRoute.page, guards: [_authGuard, _duplicateGuard]), - AutoRoute(page: RecentlyTakenRoute.page, guards: [_authGuard, _duplicateGuard]), - CustomRoute( - page: AlbumAssetSelectionRoute.page, - guards: [_authGuard, _duplicateGuard], - transitionsBuilder: TransitionsBuilders.slideBottom, - ), - CustomRoute( - page: AlbumSharedUserSelectionRoute.page, - guards: [_authGuard, _duplicateGuard], - transitionsBuilder: TransitionsBuilders.slideBottom, - ), - AutoRoute(page: AlbumViewerRoute.page, guards: [_authGuard, _duplicateGuard]), - CustomRoute( - page: AlbumAdditionalSharedUserSelectionRoute.page, - guards: [_authGuard, _duplicateGuard], - transitionsBuilder: TransitionsBuilders.slideBottom, - ), - AutoRoute(page: BackupAlbumSelectionRoute.page, guards: [_authGuard, _duplicateGuard]), - AutoRoute(page: AlbumPreviewRoute.page, guards: [_authGuard, _duplicateGuard]), - CustomRoute( - page: FailedBackupStatusRoute.page, - guards: [_authGuard, _duplicateGuard], - transitionsBuilder: TransitionsBuilders.slideBottom, - ), AutoRoute(page: SettingsRoute.page, guards: [_duplicateGuard]), AutoRoute(page: SettingsSubRoute.page, guards: [_duplicateGuard]), AutoRoute(page: AppLogRoute.page, guards: [_duplicateGuard]), AutoRoute(page: AppLogDetailRoute.page, guards: [_duplicateGuard]), - CustomRoute( - page: ArchiveRoute.page, - guards: [_authGuard, _duplicateGuard], - transitionsBuilder: TransitionsBuilders.slideLeft, - ), - CustomRoute( - page: PartnerRoute.page, - guards: [_authGuard, _duplicateGuard], - transitionsBuilder: TransitionsBuilders.slideLeft, - ), AutoRoute(page: FolderRoute.page, guards: [_authGuard]), - AutoRoute(page: PartnerDetailRoute.page, guards: [_authGuard, _duplicateGuard]), - AutoRoute(page: PersonResultRoute.page, guards: [_authGuard, _duplicateGuard]), - AutoRoute(page: AllPeopleRoute.page, guards: [_authGuard, _duplicateGuard]), - AutoRoute(page: MemoryRoute.page, guards: [_authGuard, _duplicateGuard]), - AutoRoute(page: MapRoute.page, guards: [_authGuard, _duplicateGuard]), - AutoRoute(page: AlbumOptionsRoute.page, guards: [_authGuard, _duplicateGuard]), - AutoRoute(page: TrashRoute.page, guards: [_authGuard, _duplicateGuard]), AutoRoute(page: SharedLinkRoute.page, guards: [_authGuard, _duplicateGuard]), AutoRoute(page: SharedLinkEditRoute.page, guards: [_authGuard, _duplicateGuard]), - CustomRoute( - page: ActivitiesRoute.page, - guards: [_authGuard, _duplicateGuard], - transitionsBuilder: TransitionsBuilders.slideLeft, - durationInMilliseconds: 200, - ), CustomRoute(page: MapLocationPickerRoute.page, guards: [_authGuard, _duplicateGuard]), - AutoRoute(page: BackupOptionsRoute.page, guards: [_authGuard, _duplicateGuard]), AutoRoute(page: HeaderSettingsRoute.page, 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, - ), - AutoRoute(page: NativeVideoViewerRoute.page, guards: [_authGuard, _duplicateGuard]), AutoRoute(page: ShareIntentRoute.page, guards: [_authGuard, _duplicateGuard]), - AutoRoute(page: LockedRoute.page, guards: [_authGuard, _lockedGuard, _duplicateGuard]), AutoRoute(page: PinAuthRoute.page, guards: [_authGuard, _duplicateGuard]), AutoRoute(page: LocalMediaSummaryRoute.page, guards: [_authGuard, _duplicateGuard]), AutoRoute(page: RemoteMediaSummaryRoute.page, guards: [_authGuard, _duplicateGuard]), @@ -322,7 +169,6 @@ class AppRouter extends RootStackRouter { AutoRoute(page: DriftPlaceRoute.page, guards: [_authGuard, _duplicateGuard]), AutoRoute(page: DriftPlaceDetailRoute.page, guards: [_authGuard, _duplicateGuard]), AutoRoute(page: DriftUserSelectionRoute.page, guards: [_authGuard, _duplicateGuard]), - AutoRoute(page: ChangeExperienceRoute.page, guards: [_authGuard, _duplicateGuard]), AutoRoute(page: DriftPartnerRoute.page, guards: [_authGuard, _duplicateGuard]), AutoRoute(page: DriftUploadDetailRoute.page, guards: [_authGuard, _duplicateGuard]), AutoRoute(page: SyncStatusRoute.page, guards: [_duplicateGuard]), diff --git a/mobile/lib/routing/router.gr.dart b/mobile/lib/routing/router.gr.dart index 18b16c23ef..07c3a52b49 100644 --- a/mobile/lib/routing/router.gr.dart +++ b/mobile/lib/routing/router.gr.dart @@ -10,330 +10,6 @@ part of 'router.dart'; -/// generated route for -/// [ActivitiesPage] -class ActivitiesRoute extends PageRouteInfo { - const ActivitiesRoute({List? children}) - : super(ActivitiesRoute.name, initialChildren: children); - - static const String name = 'ActivitiesRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - return const ActivitiesPage(); - }, - ); -} - -/// generated route for -/// [AlbumAdditionalSharedUserSelectionPage] -class AlbumAdditionalSharedUserSelectionRoute - extends PageRouteInfo { - AlbumAdditionalSharedUserSelectionRoute({ - Key? key, - required Album album, - List? children, - }) : super( - AlbumAdditionalSharedUserSelectionRoute.name, - args: AlbumAdditionalSharedUserSelectionRouteArgs( - key: key, - album: album, - ), - initialChildren: children, - ); - - static const String name = 'AlbumAdditionalSharedUserSelectionRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - final args = data.argsAs(); - return AlbumAdditionalSharedUserSelectionPage( - key: args.key, - album: args.album, - ); - }, - ); -} - -class AlbumAdditionalSharedUserSelectionRouteArgs { - const AlbumAdditionalSharedUserSelectionRouteArgs({ - this.key, - required this.album, - }); - - final Key? key; - - final Album album; - - @override - String toString() { - return 'AlbumAdditionalSharedUserSelectionRouteArgs{key: $key, album: $album}'; - } -} - -/// generated route for -/// [AlbumAssetSelectionPage] -class AlbumAssetSelectionRoute - extends PageRouteInfo { - AlbumAssetSelectionRoute({ - Key? key, - required Set existingAssets, - bool canDeselect = false, - List? children, - }) : super( - AlbumAssetSelectionRoute.name, - args: AlbumAssetSelectionRouteArgs( - key: key, - existingAssets: existingAssets, - canDeselect: canDeselect, - ), - initialChildren: children, - ); - - static const String name = 'AlbumAssetSelectionRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - final args = data.argsAs(); - return AlbumAssetSelectionPage( - key: args.key, - existingAssets: args.existingAssets, - canDeselect: args.canDeselect, - ); - }, - ); -} - -class AlbumAssetSelectionRouteArgs { - const AlbumAssetSelectionRouteArgs({ - this.key, - required this.existingAssets, - this.canDeselect = false, - }); - - final Key? key; - - final Set existingAssets; - - final bool canDeselect; - - @override - String toString() { - return 'AlbumAssetSelectionRouteArgs{key: $key, existingAssets: $existingAssets, canDeselect: $canDeselect}'; - } -} - -/// generated route for -/// [AlbumOptionsPage] -class AlbumOptionsRoute extends PageRouteInfo { - const AlbumOptionsRoute({List? children}) - : super(AlbumOptionsRoute.name, initialChildren: children); - - static const String name = 'AlbumOptionsRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - return const AlbumOptionsPage(); - }, - ); -} - -/// generated route for -/// [AlbumPreviewPage] -class AlbumPreviewRoute extends PageRouteInfo { - AlbumPreviewRoute({ - Key? key, - required Album album, - List? children, - }) : super( - AlbumPreviewRoute.name, - args: AlbumPreviewRouteArgs(key: key, album: album), - initialChildren: children, - ); - - static const String name = 'AlbumPreviewRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - final args = data.argsAs(); - return AlbumPreviewPage(key: args.key, album: args.album); - }, - ); -} - -class AlbumPreviewRouteArgs { - const AlbumPreviewRouteArgs({this.key, required this.album}); - - final Key? key; - - final Album album; - - @override - String toString() { - return 'AlbumPreviewRouteArgs{key: $key, album: $album}'; - } -} - -/// generated route for -/// [AlbumSharedUserSelectionPage] -class AlbumSharedUserSelectionRoute - extends PageRouteInfo { - AlbumSharedUserSelectionRoute({ - Key? key, - required Set assets, - List? children, - }) : super( - AlbumSharedUserSelectionRoute.name, - args: AlbumSharedUserSelectionRouteArgs(key: key, assets: assets), - initialChildren: children, - ); - - static const String name = 'AlbumSharedUserSelectionRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - final args = data.argsAs(); - return AlbumSharedUserSelectionPage(key: args.key, assets: args.assets); - }, - ); -} - -class AlbumSharedUserSelectionRouteArgs { - const AlbumSharedUserSelectionRouteArgs({this.key, required this.assets}); - - final Key? key; - - final Set assets; - - @override - String toString() { - return 'AlbumSharedUserSelectionRouteArgs{key: $key, assets: $assets}'; - } -} - -/// generated route for -/// [AlbumViewerPage] -class AlbumViewerRoute extends PageRouteInfo { - AlbumViewerRoute({ - Key? key, - required int albumId, - List? children, - }) : super( - AlbumViewerRoute.name, - args: AlbumViewerRouteArgs(key: key, albumId: albumId), - initialChildren: children, - ); - - static const String name = 'AlbumViewerRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - final args = data.argsAs(); - return AlbumViewerPage(key: args.key, albumId: args.albumId); - }, - ); -} - -class AlbumViewerRouteArgs { - const AlbumViewerRouteArgs({this.key, required this.albumId}); - - final Key? key; - - final int albumId; - - @override - String toString() { - return 'AlbumViewerRouteArgs{key: $key, albumId: $albumId}'; - } -} - -/// generated route for -/// [AlbumsPage] -class AlbumsRoute extends PageRouteInfo { - const AlbumsRoute({List? children}) - : super(AlbumsRoute.name, initialChildren: children); - - static const String name = 'AlbumsRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - return const AlbumsPage(); - }, - ); -} - -/// generated route for -/// [AllMotionPhotosPage] -class AllMotionPhotosRoute extends PageRouteInfo { - const AllMotionPhotosRoute({List? children}) - : super(AllMotionPhotosRoute.name, initialChildren: children); - - static const String name = 'AllMotionPhotosRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - return const AllMotionPhotosPage(); - }, - ); -} - -/// generated route for -/// [AllPeoplePage] -class AllPeopleRoute extends PageRouteInfo { - const AllPeopleRoute({List? children}) - : super(AllPeopleRoute.name, initialChildren: children); - - static const String name = 'AllPeopleRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - return const AllPeoplePage(); - }, - ); -} - -/// generated route for -/// [AllPlacesPage] -class AllPlacesRoute extends PageRouteInfo { - const AllPlacesRoute({List? children}) - : super(AllPlacesRoute.name, initialChildren: children); - - static const String name = 'AllPlacesRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - return const AllPlacesPage(); - }, - ); -} - -/// generated route for -/// [AllVideosPage] -class AllVideosRoute extends PageRouteInfo { - const AllVideosRoute({List? children}) - : super(AllVideosRoute.name, initialChildren: children); - - static const String name = 'AllVideosRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - return const AllVideosPage(); - }, - ); -} - /// generated route for /// [AppLogDetailPage] class AppLogDetailRoute extends PageRouteInfo { @@ -387,22 +63,6 @@ class AppLogRoute extends PageRouteInfo { ); } -/// generated route for -/// [ArchivePage] -class ArchiveRoute extends PageRouteInfo { - const ArchiveRoute({List? children}) - : super(ArchiveRoute.name, initialChildren: children); - - static const String name = 'ArchiveRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - return const ArchivePage(); - }, - ); -} - /// generated route for /// [AssetTroubleshootPage] class AssetTroubleshootRoute extends PageRouteInfo { @@ -504,97 +164,6 @@ class AssetViewerRouteArgs { } } -/// generated route for -/// [BackupAlbumSelectionPage] -class BackupAlbumSelectionRoute extends PageRouteInfo { - const BackupAlbumSelectionRoute({List? children}) - : super(BackupAlbumSelectionRoute.name, initialChildren: children); - - static const String name = 'BackupAlbumSelectionRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - return const BackupAlbumSelectionPage(); - }, - ); -} - -/// generated route for -/// [BackupControllerPage] -class BackupControllerRoute extends PageRouteInfo { - const BackupControllerRoute({List? children}) - : super(BackupControllerRoute.name, initialChildren: children); - - static const String name = 'BackupControllerRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - return const BackupControllerPage(); - }, - ); -} - -/// generated route for -/// [BackupOptionsPage] -class BackupOptionsRoute extends PageRouteInfo { - const BackupOptionsRoute({List? children}) - : super(BackupOptionsRoute.name, initialChildren: children); - - static const String name = 'BackupOptionsRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - return const BackupOptionsPage(); - }, - ); -} - -/// generated route for -/// [ChangeExperiencePage] -class ChangeExperienceRoute extends PageRouteInfo { - ChangeExperienceRoute({ - Key? key, - required bool switchingToBeta, - List? children, - }) : super( - ChangeExperienceRoute.name, - args: ChangeExperienceRouteArgs( - key: key, - switchingToBeta: switchingToBeta, - ), - initialChildren: children, - ); - - static const String name = 'ChangeExperienceRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - final args = data.argsAs(); - return ChangeExperiencePage( - key: args.key, - switchingToBeta: args.switchingToBeta, - ); - }, - ); -} - -class ChangeExperienceRouteArgs { - const ChangeExperienceRouteArgs({this.key, required this.switchingToBeta}); - - final Key? key; - - final bool switchingToBeta; - - @override - String toString() { - return 'ChangeExperienceRouteArgs{key: $key, switchingToBeta: $switchingToBeta}'; - } -} - /// generated route for /// [ChangePasswordPage] class ChangePasswordRoute extends PageRouteInfo { @@ -648,89 +217,6 @@ class CleanupPreviewRouteArgs { } } -/// generated route for -/// [CreateAlbumPage] -class CreateAlbumRoute extends PageRouteInfo { - CreateAlbumRoute({ - Key? key, - List? assets, - List? children, - }) : super( - CreateAlbumRoute.name, - args: CreateAlbumRouteArgs(key: key, assets: assets), - initialChildren: children, - ); - - static const String name = 'CreateAlbumRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - final args = data.argsAs( - orElse: () => const CreateAlbumRouteArgs(), - ); - return CreateAlbumPage(key: args.key, assets: args.assets); - }, - ); -} - -class CreateAlbumRouteArgs { - const CreateAlbumRouteArgs({this.key, this.assets}); - - final Key? key; - - final List? assets; - - @override - String toString() { - return 'CreateAlbumRouteArgs{key: $key, assets: $assets}'; - } -} - -/// generated route for -/// [CropImagePage] -class CropImageRoute extends PageRouteInfo { - CropImageRoute({ - Key? key, - required Image image, - required Asset asset, - List? children, - }) : super( - CropImageRoute.name, - args: CropImageRouteArgs(key: key, image: image, asset: asset), - initialChildren: children, - ); - - static const String name = 'CropImageRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - final args = data.argsAs(); - return CropImagePage(key: args.key, image: args.image, asset: args.asset); - }, - ); -} - -class CropImageRouteArgs { - const CropImageRouteArgs({ - this.key, - required this.image, - required this.asset, - }); - - final Key? key; - - final Image image; - - final Asset asset; - - @override - String toString() { - return 'CropImageRouteArgs{key: $key, image: $image, asset: $asset}'; - } -} - /// generated route for /// [DownloadInfoPage] class DownloadInfoRoute extends PageRouteInfo { @@ -1514,144 +1000,6 @@ class DriftVideoRoute extends PageRouteInfo { ); } -/// generated route for -/// [EditImagePage] -class EditImageRoute extends PageRouteInfo { - EditImageRoute({ - Key? key, - required Asset asset, - required Image image, - required bool isEdited, - List? children, - }) : super( - EditImageRoute.name, - args: EditImageRouteArgs( - key: key, - asset: asset, - image: image, - isEdited: isEdited, - ), - initialChildren: children, - ); - - static const String name = 'EditImageRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - final args = data.argsAs(); - return EditImagePage( - key: args.key, - asset: args.asset, - image: args.image, - isEdited: args.isEdited, - ); - }, - ); -} - -class EditImageRouteArgs { - const EditImageRouteArgs({ - this.key, - required this.asset, - required this.image, - required this.isEdited, - }); - - final Key? key; - - final Asset asset; - - final Image image; - - final bool isEdited; - - @override - String toString() { - return 'EditImageRouteArgs{key: $key, asset: $asset, image: $image, isEdited: $isEdited}'; - } -} - -/// generated route for -/// [FailedBackupStatusPage] -class FailedBackupStatusRoute extends PageRouteInfo { - const FailedBackupStatusRoute({List? children}) - : super(FailedBackupStatusRoute.name, initialChildren: children); - - static const String name = 'FailedBackupStatusRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - return const FailedBackupStatusPage(); - }, - ); -} - -/// generated route for -/// [FavoritesPage] -class FavoritesRoute extends PageRouteInfo { - const FavoritesRoute({List? children}) - : super(FavoritesRoute.name, initialChildren: children); - - static const String name = 'FavoritesRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - return const FavoritesPage(); - }, - ); -} - -/// generated route for -/// [FilterImagePage] -class FilterImageRoute extends PageRouteInfo { - FilterImageRoute({ - Key? key, - required Image image, - required Asset asset, - List? children, - }) : super( - FilterImageRoute.name, - args: FilterImageRouteArgs(key: key, image: image, asset: asset), - initialChildren: children, - ); - - static const String name = 'FilterImageRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - final args = data.argsAs(); - return FilterImagePage( - key: args.key, - image: args.image, - asset: args.asset, - ); - }, - ); -} - -class FilterImageRouteArgs { - const FilterImageRouteArgs({ - this.key, - required this.image, - required this.asset, - }); - - final Key? key; - - final Image image; - - final Asset asset; - - @override - String toString() { - return 'FilterImageRouteArgs{key: $key, image: $image, asset: $asset}'; - } -} - /// generated route for /// [FolderPage] class FolderRoute extends PageRouteInfo { @@ -1691,70 +1039,6 @@ class FolderRouteArgs { } } -/// generated route for -/// [GalleryViewerPage] -class GalleryViewerRoute extends PageRouteInfo { - GalleryViewerRoute({ - Key? key, - required RenderList renderList, - int initialIndex = 0, - int heroOffset = 0, - bool showStack = false, - List? children, - }) : super( - GalleryViewerRoute.name, - args: GalleryViewerRouteArgs( - key: key, - renderList: renderList, - initialIndex: initialIndex, - heroOffset: heroOffset, - showStack: showStack, - ), - initialChildren: children, - ); - - static const String name = 'GalleryViewerRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - final args = data.argsAs(); - return GalleryViewerPage( - key: args.key, - renderList: args.renderList, - initialIndex: args.initialIndex, - heroOffset: args.heroOffset, - showStack: args.showStack, - ); - }, - ); -} - -class GalleryViewerRouteArgs { - const GalleryViewerRouteArgs({ - this.key, - required this.renderList, - this.initialIndex = 0, - this.heroOffset = 0, - this.showStack = false, - }); - - final Key? key; - - final RenderList renderList; - - final int initialIndex; - - final int heroOffset; - - final bool showStack; - - @override - String toString() { - return 'GalleryViewerRouteArgs{key: $key, renderList: $renderList, initialIndex: $initialIndex, heroOffset: $heroOffset, showStack: $showStack}'; - } -} - /// generated route for /// [HeaderSettingsPage] class HeaderSettingsRoute extends PageRouteInfo { @@ -1771,38 +1055,6 @@ class HeaderSettingsRoute extends PageRouteInfo { ); } -/// generated route for -/// [LibraryPage] -class LibraryRoute extends PageRouteInfo { - const LibraryRoute({List? children}) - : super(LibraryRoute.name, initialChildren: children); - - static const String name = 'LibraryRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - return const LibraryPage(); - }, - ); -} - -/// generated route for -/// [LocalAlbumsPage] -class LocalAlbumsRoute extends PageRouteInfo { - const LocalAlbumsRoute({List? children}) - : super(LocalAlbumsRoute.name, initialChildren: children); - - static const String name = 'LocalAlbumsRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - return const LocalAlbumsPage(); - }, - ); -} - /// generated route for /// [LocalMediaSummaryPage] class LocalMediaSummaryRoute extends PageRouteInfo { @@ -1856,22 +1108,6 @@ class LocalTimelineRouteArgs { } } -/// generated route for -/// [LockedPage] -class LockedRoute extends PageRouteInfo { - const LockedRoute({List? children}) - : super(LockedRoute.name, initialChildren: children); - - static const String name = 'LockedRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - return const LockedPage(); - }, - ); -} - /// generated route for /// [LoginPage] class LoginRoute extends PageRouteInfo { @@ -1952,311 +1188,6 @@ class MapLocationPickerRouteArgs { } } -/// generated route for -/// [MapPage] -class MapRoute extends PageRouteInfo { - MapRoute({Key? key, LatLng? initialLocation, List? children}) - : super( - MapRoute.name, - args: MapRouteArgs(key: key, initialLocation: initialLocation), - initialChildren: children, - ); - - static const String name = 'MapRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - final args = data.argsAs( - orElse: () => const MapRouteArgs(), - ); - return MapPage(key: args.key, initialLocation: args.initialLocation); - }, - ); -} - -class MapRouteArgs { - const MapRouteArgs({this.key, this.initialLocation}); - - final Key? key; - - final LatLng? initialLocation; - - @override - String toString() { - return 'MapRouteArgs{key: $key, initialLocation: $initialLocation}'; - } -} - -/// generated route for -/// [MemoryPage] -class MemoryRoute extends PageRouteInfo { - MemoryRoute({ - required List memories, - required int memoryIndex, - Key? key, - List? children, - }) : super( - MemoryRoute.name, - args: MemoryRouteArgs( - memories: memories, - memoryIndex: memoryIndex, - key: key, - ), - initialChildren: children, - ); - - static const String name = 'MemoryRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - final args = data.argsAs(); - return MemoryPage( - memories: args.memories, - memoryIndex: args.memoryIndex, - key: args.key, - ); - }, - ); -} - -class MemoryRouteArgs { - const MemoryRouteArgs({ - required this.memories, - required this.memoryIndex, - this.key, - }); - - final List memories; - - final int memoryIndex; - - final Key? key; - - @override - String toString() { - return 'MemoryRouteArgs{memories: $memories, memoryIndex: $memoryIndex, key: $key}'; - } -} - -/// generated route for -/// [NativeVideoViewerPage] -class NativeVideoViewerRoute extends PageRouteInfo { - NativeVideoViewerRoute({ - Key? key, - required Asset asset, - required Widget image, - bool showControls = true, - int playbackDelayFactor = 1, - List? children, - }) : super( - NativeVideoViewerRoute.name, - args: NativeVideoViewerRouteArgs( - key: key, - asset: asset, - image: image, - showControls: showControls, - playbackDelayFactor: playbackDelayFactor, - ), - initialChildren: children, - ); - - static const String name = 'NativeVideoViewerRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - final args = data.argsAs(); - return NativeVideoViewerPage( - key: args.key, - asset: args.asset, - image: args.image, - showControls: args.showControls, - playbackDelayFactor: args.playbackDelayFactor, - ); - }, - ); -} - -class NativeVideoViewerRouteArgs { - const NativeVideoViewerRouteArgs({ - this.key, - required this.asset, - required this.image, - this.showControls = true, - this.playbackDelayFactor = 1, - }); - - final Key? key; - - final Asset asset; - - final Widget image; - - final bool showControls; - - final int playbackDelayFactor; - - @override - String toString() { - return 'NativeVideoViewerRouteArgs{key: $key, asset: $asset, image: $image, showControls: $showControls, playbackDelayFactor: $playbackDelayFactor}'; - } -} - -/// generated route for -/// [PartnerDetailPage] -class PartnerDetailRoute extends PageRouteInfo { - PartnerDetailRoute({ - Key? key, - required UserDto partner, - List? children, - }) : super( - PartnerDetailRoute.name, - args: PartnerDetailRouteArgs(key: key, partner: partner), - initialChildren: children, - ); - - static const String name = 'PartnerDetailRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - final args = data.argsAs(); - return PartnerDetailPage(key: args.key, partner: args.partner); - }, - ); -} - -class PartnerDetailRouteArgs { - const PartnerDetailRouteArgs({this.key, required this.partner}); - - final Key? key; - - final UserDto partner; - - @override - String toString() { - return 'PartnerDetailRouteArgs{key: $key, partner: $partner}'; - } -} - -/// generated route for -/// [PartnerPage] -class PartnerRoute extends PageRouteInfo { - const PartnerRoute({List? children}) - : super(PartnerRoute.name, initialChildren: children); - - static const String name = 'PartnerRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - return const PartnerPage(); - }, - ); -} - -/// generated route for -/// [PeopleCollectionPage] -class PeopleCollectionRoute extends PageRouteInfo { - const PeopleCollectionRoute({List? children}) - : super(PeopleCollectionRoute.name, initialChildren: children); - - static const String name = 'PeopleCollectionRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - return const PeopleCollectionPage(); - }, - ); -} - -/// generated route for -/// [PermissionOnboardingPage] -class PermissionOnboardingRoute extends PageRouteInfo { - const PermissionOnboardingRoute({List? children}) - : super(PermissionOnboardingRoute.name, initialChildren: children); - - static const String name = 'PermissionOnboardingRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - return const PermissionOnboardingPage(); - }, - ); -} - -/// generated route for -/// [PersonResultPage] -class PersonResultRoute extends PageRouteInfo { - PersonResultRoute({ - Key? key, - required String personId, - required String personName, - List? children, - }) : super( - PersonResultRoute.name, - args: PersonResultRouteArgs( - key: key, - personId: personId, - personName: personName, - ), - initialChildren: children, - ); - - static const String name = 'PersonResultRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - final args = data.argsAs(); - return PersonResultPage( - key: args.key, - personId: args.personId, - personName: args.personName, - ); - }, - ); -} - -class PersonResultRouteArgs { - const PersonResultRouteArgs({ - this.key, - required this.personId, - required this.personName, - }); - - final Key? key; - - final String personId; - - final String personName; - - @override - String toString() { - return 'PersonResultRouteArgs{key: $key, personId: $personId, personName: $personName}'; - } -} - -/// generated route for -/// [PhotosPage] -class PhotosRoute extends PageRouteInfo { - const PhotosRoute({List? children}) - : super(PhotosRoute.name, initialChildren: children); - - static const String name = 'PhotosRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - return const PhotosPage(); - }, - ); -} - /// generated route for /// [PinAuthPage] class PinAuthRoute extends PageRouteInfo { @@ -2296,51 +1227,6 @@ class PinAuthRouteArgs { } } -/// generated route for -/// [PlacesCollectionPage] -class PlacesCollectionRoute extends PageRouteInfo { - PlacesCollectionRoute({ - Key? key, - LatLng? currentLocation, - List? children, - }) : super( - PlacesCollectionRoute.name, - args: PlacesCollectionRouteArgs( - key: key, - currentLocation: currentLocation, - ), - initialChildren: children, - ); - - static const String name = 'PlacesCollectionRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - final args = data.argsAs( - orElse: () => const PlacesCollectionRouteArgs(), - ); - return PlacesCollectionPage( - key: args.key, - currentLocation: args.currentLocation, - ); - }, - ); -} - -class PlacesCollectionRouteArgs { - const PlacesCollectionRouteArgs({this.key, this.currentLocation}); - - final Key? key; - - final LatLng? currentLocation; - - @override - String toString() { - return 'PlacesCollectionRouteArgs{key: $key, currentLocation: $currentLocation}'; - } -} - /// generated route for /// [ProfilePictureCropPage] class ProfilePictureCropRoute @@ -2379,22 +1265,6 @@ class ProfilePictureCropRouteArgs { } } -/// generated route for -/// [RecentlyTakenPage] -class RecentlyTakenRoute extends PageRouteInfo { - const RecentlyTakenRoute({List? children}) - : super(RecentlyTakenRoute.name, initialChildren: children); - - static const String name = 'RecentlyTakenRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - return const RecentlyTakenPage(); - }, - ); -} - /// generated route for /// [RemoteAlbumPage] class RemoteAlbumRoute extends PageRouteInfo { @@ -2448,45 +1318,6 @@ class RemoteMediaSummaryRoute extends PageRouteInfo { ); } -/// generated route for -/// [SearchPage] -class SearchRoute extends PageRouteInfo { - SearchRoute({ - Key? key, - SearchFilter? prefilter, - List? children, - }) : super( - SearchRoute.name, - args: SearchRouteArgs(key: key, prefilter: prefilter), - initialChildren: children, - ); - - static const String name = 'SearchRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - final args = data.argsAs( - orElse: () => const SearchRouteArgs(), - ); - return SearchPage(key: args.key, prefilter: args.prefilter); - }, - ); -} - -class SearchRouteArgs { - const SearchRouteArgs({this.key, this.prefilter}); - - final Key? key; - - final SearchFilter? prefilter; - - @override - String toString() { - return 'SearchRouteArgs{key: $key, prefilter: $prefilter}'; - } -} - /// generated route for /// [SettingsPage] class SettingsRoute extends PageRouteInfo { @@ -2685,22 +1516,6 @@ class SyncStatusRoute extends PageRouteInfo { ); } -/// generated route for -/// [TabControllerPage] -class TabControllerRoute extends PageRouteInfo { - const TabControllerRoute({List? children}) - : super(TabControllerRoute.name, initialChildren: children); - - static const String name = 'TabControllerRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - return const TabControllerPage(); - }, - ); -} - /// generated route for /// [TabShellPage] class TabShellRoute extends PageRouteInfo { @@ -2716,19 +1531,3 @@ class TabShellRoute extends PageRouteInfo { }, ); } - -/// generated route for -/// [TrashPage] -class TrashRoute extends PageRouteInfo { - const TrashRoute({List? children}) - : super(TrashRoute.name, initialChildren: children); - - static const String name = 'TrashRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - return const TrashPage(); - }, - ); -} diff --git a/mobile/lib/services/activity.service.dart b/mobile/lib/services/activity.service.dart index 382a7fe107..0ef1badacb 100644 --- a/mobile/lib/services/activity.service.dart +++ b/mobile/lib/services/activity.service.dart @@ -9,7 +9,6 @@ import 'package:immich_mobile/providers/infrastructure/current_album.provider.da import 'package:immich_mobile/repositories/activity_api.repository.dart'; import 'package:immich_mobile/routing/router.dart'; import 'package:logging/logging.dart'; -import 'package:immich_mobile/entities/store.entity.dart' as immich_store; class ActivityService with ErrorLoggerMixin { final ActivityApiRepository _activityApiRepository; @@ -60,20 +59,16 @@ class ActivityService with ErrorLoggerMixin { } Future buildAssetViewerRoute(String assetId, WidgetRef ref) async { - if (immich_store.Store.isBetaTimelineEnabled) { - final asset = await _assetService.getRemoteAsset(assetId); - if (asset == null) { - return null; - } - - AssetViewer.setAsset(ref, asset); - return AssetViewerRoute( - initialIndex: 0, - timelineService: _timelineFactory.fromAssets([asset], TimelineOrigin.albumActivities), - currentAlbum: ref.read(currentRemoteAlbumProvider), - ); + final asset = await _assetService.getRemoteAsset(assetId); + if (asset == null) { + return null; } - return null; + AssetViewer.setAsset(ref, asset); + return AssetViewerRoute( + initialIndex: 0, + timelineService: _timelineFactory.fromAssets([asset], TimelineOrigin.albumActivities), + currentAlbum: ref.read(currentRemoteAlbumProvider), + ); } } diff --git a/mobile/lib/services/album.service.dart b/mobile/lib/services/album.service.dart deleted file mode 100644 index 8d77b569e6..0000000000 --- a/mobile/lib/services/album.service.dart +++ /dev/null @@ -1,425 +0,0 @@ -import 'dart:async'; -import 'dart:collection'; -import 'dart:io'; - -import 'package:collection/collection.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/constants/enums.dart'; -import 'package:immich_mobile/domain/models/user.model.dart'; -import 'package:immich_mobile/domain/services/user.service.dart'; -import 'package:immich_mobile/entities/album.entity.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/entities/backup_album.entity.dart'; -import 'package:immich_mobile/infrastructure/entities/user.entity.dart' as entity; -import 'package:immich_mobile/models/albums/album_add_asset_response.model.dart'; -import 'package:immich_mobile/models/albums/album_search.model.dart'; -import 'package:immich_mobile/providers/infrastructure/user.provider.dart'; -import 'package:immich_mobile/repositories/album.repository.dart'; -import 'package:immich_mobile/repositories/album_api.repository.dart'; -import 'package:immich_mobile/repositories/album_media.repository.dart'; -import 'package:immich_mobile/repositories/asset.repository.dart'; -import 'package:immich_mobile/repositories/backup.repository.dart'; -import 'package:immich_mobile/services/entity.service.dart'; -import 'package:immich_mobile/services/sync.service.dart'; -import 'package:immich_mobile/utils/hash.dart'; -import 'package:logging/logging.dart'; -import 'package:immich_mobile/utils/debug_print.dart'; - -final albumServiceProvider = Provider( - (ref) => AlbumService( - ref.watch(syncServiceProvider), - ref.watch(userServiceProvider), - ref.watch(entityServiceProvider), - ref.watch(albumRepositoryProvider), - ref.watch(assetRepositoryProvider), - ref.watch(backupAlbumRepositoryProvider), - ref.watch(albumMediaRepositoryProvider), - ref.watch(albumApiRepositoryProvider), - ), -); - -class AlbumService { - final SyncService _syncService; - final UserService _userService; - final EntityService _entityService; - final AlbumRepository _albumRepository; - final AssetRepository _assetRepository; - final BackupAlbumRepository _backupAlbumRepository; - final AlbumMediaRepository _albumMediaRepository; - final AlbumApiRepository _albumApiRepository; - final Logger _log = Logger('AlbumService'); - Completer _localCompleter = Completer()..complete(false); - Completer _remoteCompleter = Completer()..complete(false); - - AlbumService( - this._syncService, - this._userService, - this._entityService, - this._albumRepository, - this._assetRepository, - this._backupAlbumRepository, - this._albumMediaRepository, - this._albumApiRepository, - ); - - /// Checks all selected device albums for changes of albums and their assets - /// Updates the local database and returns `true` if there were any changes - Future refreshDeviceAlbums() async { - if (!_localCompleter.isCompleted) { - // guard against concurrent calls - _log.info("refreshDeviceAlbums is already in progress"); - return _localCompleter.future; - } - _localCompleter = Completer(); - final Stopwatch sw = Stopwatch()..start(); - bool changes = false; - try { - final (selectedIds, excludedIds, onDevice) = await ( - _backupAlbumRepository.getIdsBySelection(BackupSelection.select).then((value) => value.toSet()), - _backupAlbumRepository.getIdsBySelection(BackupSelection.exclude).then((value) => value.toSet()), - _albumMediaRepository.getAll(), - ).wait; - _log.info("Found ${onDevice.length} device albums"); - if (selectedIds.isEmpty) { - final numLocal = await _albumRepository.count(local: true); - if (numLocal > 0) { - await _syncService.removeAllLocalAlbumsAndAssets(); - } - return false; - } - Set? excludedAssets; - if (excludedIds.isNotEmpty) { - if (Platform.isIOS) { - // iOS and Android device album working principle differ significantly - // on iOS, an asset can be in multiple albums - // on Android, an asset can only be in exactly one album (folder!) at the same time - // thus, on Android, excluding an album can be done by ignoring that album - // however, on iOS, it it necessary to load the assets from all excluded - // albums and check every asset from any selected album against the set - // of excluded assets - excludedAssets = await _loadExcludedAssetIds(onDevice, excludedIds); - _log.info("Found ${excludedAssets.length} assets to exclude"); - } - // remove all excluded albums - onDevice.removeWhere((e) => excludedIds.contains(e.localId)); - _log.info("Ignoring ${excludedIds.length} excluded albums resulting in ${onDevice.length} device albums"); - } - - final allAlbum = onDevice.firstWhereOrNull((album) => album.isAll); - final hasAll = allAlbum != null && selectedIds.contains(allAlbum.localId); - if (hasAll) { - if (Platform.isAndroid) { - // remove the virtual "Recent" album and keep and individual albums - // on Android, the virtual "Recent" `lastModified` value is always null - onDevice.removeWhere((album) => album.isAll); - _log.info("'Recents' is selected, keeping all individual albums"); - } - } else { - // keep only the explicitly selected albums - onDevice.removeWhere((album) => !selectedIds.contains(album.localId)); - _log.info("'Recents' is not selected, keeping only selected albums"); - } - changes = await _syncService.syncLocalAlbumAssetsToDb(onDevice, excludedAssets); - _log.info("Syncing completed. Changes: $changes"); - } finally { - _localCompleter.complete(changes); - } - dPrint(() => "refreshDeviceAlbums took ${sw.elapsedMilliseconds}ms"); - return changes; - } - - Future> _loadExcludedAssetIds(List albums, Set excludedAlbumIds) async { - final Set result = HashSet(); - for (final batchAlbums in albums.where((album) => excludedAlbumIds.contains(album.localId)).slices(5)) { - await batchAlbums - .map((album) => _albumMediaRepository.getAssetIds(album.localId!).then((assetIds) => result.addAll(assetIds))) - .wait; - } - return result; - } - - /// Checks remote albums (owned if `isShared` is false) for changes, - /// updates the local database and returns `true` if there were any changes - Future refreshRemoteAlbums() async { - if (!_remoteCompleter.isCompleted) { - // guard against concurrent calls - return _remoteCompleter.future; - } - _remoteCompleter = Completer(); - final Stopwatch sw = Stopwatch()..start(); - bool changes = false; - try { - final users = await _syncService.getUsersFromServer(); - if (users != null) { - await _syncService.syncUsersFromServer(users); - } - final (sharedAlbum, ownedAlbum) = await ( - // Note: `shared: true` is required to get albums that don't belong to - // us due to unusual behaviour on the API but this will also return our - // own shared albums - _albumApiRepository.getAll(shared: true), - // Passing null (or nothing) for `shared` returns only albums that - // explicitly belong to us - _albumApiRepository.getAll(shared: null), - ).wait; - - final albums = HashSet(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 { - _remoteCompleter.complete(changes); - } - dPrint(() => "refreshRemoteAlbums took ${sw.elapsedMilliseconds}ms"); - return changes; - } - - Future createAlbum( - String albumName, - Iterable assets, [ - Iterable sharedUsers = const [], - ]) async { - final Album album = await _albumApiRepository.create( - albumName, - assetIds: assets.map((asset) => asset.remoteId!), - sharedUserIds: sharedUsers.map((user) => user.id), - ); - await _entityService.fillAlbumWithDatabaseEntities(album); - return _albumRepository.create(album); - } - - /* - * Creates names like Untitled, Untitled (1), Untitled (2), ... - */ - Future _getNextAlbumName() async { - const baseName = "Untitled"; - for (int round = 0; ; round++) { - final proposedName = "$baseName${round == 0 ? "" : " ($round)"}"; - - if (null == await _albumRepository.getByName(proposedName, owner: true)) { - return proposedName; - } - } - } - - Future createAlbumWithGeneratedName(Iterable assets) async { - return createAlbum(await _getNextAlbumName(), assets, []); - } - - Future addAssets(Album album, Iterable assets) async { - try { - final result = await _albumApiRepository.addAssets(album.remoteId!, assets.map((asset) => asset.remoteId!)); - - final List addedAssets = result.added - .map((id) => assets.firstWhere((asset) => asset.remoteId == id)) - .toList(); - - await _updateAssets(album.id, add: addedAssets); - - return AlbumAddAssetsResponse(alreadyInAlbum: result.duplicates, successfullyAdded: addedAssets.length); - } catch (e) { - dPrint(() => "Error addAssets ${e.toString()}"); - } - return null; - } - - Future _updateAssets(int albumId, {List add = const [], List remove = const []}) => - _albumRepository.transaction(() async { - final album = await _albumRepository.get(albumId); - if (album == null) return; - await _albumRepository.addAssets(album, add); - await _albumRepository.removeAssets(album, remove); - await _albumRepository.recalculateMetadata(album); - await _albumRepository.update(album); - }); - - Future setActivityStatus(Album album, bool enabled) async { - try { - final updatedAlbum = await _albumApiRepository.update(album.remoteId!, activityEnabled: enabled); - album.activityEnabled = updatedAlbum.activityEnabled; - await _albumRepository.update(album); - return true; - } catch (e) { - dPrint(() => "Error setActivityEnabled ${e.toString()}"); - } - return false; - } - - Future deleteAlbum(Album album) async { - try { - final userId = _userService.getMyUser().id; - if (album.owner.value?.isarId == fastHash(userId)) { - await _albumApiRepository.delete(album.remoteId!); - } - if (album.shared) { - final foreignAssets = await _assetRepository.getByAlbum(album, notOwnedBy: [userId]); - await _albumRepository.delete(album.id); - - final List albums = await _albumRepository.getAll(shared: true); - final List existing = []; - for (Album album in albums) { - existing.addAll(await _assetRepository.getByAlbum(album, notOwnedBy: [userId])); - } - final List idsToRemove = _syncService.sharedAssetsToRemove(foreignAssets, existing); - if (idsToRemove.isNotEmpty) { - await _assetRepository.deleteByIds(idsToRemove); - } - } else { - await _albumRepository.delete(album.id); - } - return true; - } catch (e) { - dPrint(() => "Error deleteAlbum ${e.toString()}"); - } - return false; - } - - Future leaveAlbum(Album album) async { - try { - await _albumApiRepository.removeUser(album.remoteId!, userId: "me"); - return true; - } catch (e) { - dPrint(() => "Error leaveAlbum ${e.toString()}"); - return false; - } - } - - Future removeAsset(Album album, Iterable assets) async { - try { - final result = await _albumApiRepository.removeAssets(album.remoteId!, assets.map((asset) => asset.remoteId!)); - final toRemove = result.removed.map((id) => assets.firstWhere((asset) => asset.remoteId == id)); - await _updateAssets(album.id, remove: toRemove.toList()); - return true; - } catch (e) { - dPrint(() => "Error removeAssetFromAlbum ${e.toString()}"); - } - return false; - } - - Future removeUser(Album album, UserDto user) async { - try { - await _albumApiRepository.removeUser(album.remoteId!, userId: user.id); - - album.sharedUsers.remove(entity.User.fromDto(user)); - await _albumRepository.removeUsers(album, [user]); - final a = await _albumRepository.get(album.id); - // trigger watcher - await _albumRepository.update(a!); - - return true; - } catch (error) { - dPrint(() => "Error removeUser ${error.toString()}"); - return false; - } - } - - Future addUsers(Album album, List 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.map((u) => u.toDto()).toList()); - await _albumRepository.update(album); - - return true; - } catch (error) { - dPrint(() => "Error addUsers ${error.toString()}"); - } - return false; - } - - Future changeTitleAlbum(Album album, String newAlbumTitle) async { - try { - final updatedAlbum = await _albumApiRepository.update(album.remoteId!, name: newAlbumTitle); - - album.name = updatedAlbum.name; - await _albumRepository.update(album); - return true; - } catch (e) { - dPrint(() => "Error changeTitleAlbum ${e.toString()}"); - return false; - } - } - - Future changeDescriptionAlbum(Album album, String newAlbumDescription) async { - try { - final updatedAlbum = await _albumApiRepository.update(album.remoteId!, description: newAlbumDescription); - - album.description = updatedAlbum.description; - await _albumRepository.update(album); - return true; - } catch (e) { - dPrint(() => "Error changeDescriptionAlbum ${e.toString()}"); - return false; - } - } - - Future getAlbumByName(String name, {bool? remote, bool? shared, bool? owner}) => - _albumRepository.getByName(name, remote: remote, shared: shared, owner: owner); - - /// - /// Add the uploaded asset to the selected albums - /// - Future syncUploadAlbums(List albumNames, List assetIds) async { - for (final albumName in albumNames) { - Album? album = await getAlbumByName(albumName, remote: true, owner: true); - album ??= await createAlbum(albumName, []); - if (album != null && album.remoteId != null) { - await _albumApiRepository.addAssets(album.remoteId!, assetIds); - } - } - } - - Future> getAllRemoteAlbums() async { - return _albumRepository.getAll(remote: true); - } - - Future> getAllLocalAlbums() async { - return _albumRepository.getAll(remote: false); - } - - Stream> watchRemoteAlbums() { - return _albumRepository.watchRemoteAlbums(); - } - - Stream> watchLocalAlbums() { - return _albumRepository.watchLocalAlbums(); - } - - /// Get album by Isar ID - Future getAlbumById(int id) { - return _albumRepository.get(id); - } - - Future getAlbumByRemoteId(String remoteId) { - return _albumRepository.getByRemoteId(remoteId); - } - - Stream watchAlbum(int id) { - return _albumRepository.watchAlbum(id); - } - - Future> search(String searchTerm, QuickFilterMode filterMode) async { - return _albumRepository.search(searchTerm, filterMode); - } - - Future updateSortOrder(Album album, SortOrder order) async { - try { - final updateAlbum = await _albumApiRepository.update(album.remoteId!, sortOrder: order); - album.sortOrder = updateAlbum.sortOrder; - - return _albumRepository.update(album); - } catch (error, stackTrace) { - _log.severe("Error updating album sort order", error, stackTrace); - } - return null; - } - - Future clearTable() async { - await _albumRepository.clearTable(); - } -} diff --git a/mobile/lib/services/asset.service.dart b/mobile/lib/services/asset.service.dart deleted file mode 100644 index b9fab35442..0000000000 --- a/mobile/lib/services/asset.service.dart +++ /dev/null @@ -1,465 +0,0 @@ -import 'dart:async'; - -import 'package:collection/collection.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/constants/enums.dart'; -import 'package:immich_mobile/domain/models/user.model.dart'; -import 'package:immich_mobile/domain/services/user.service.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/entities/backup_album.entity.dart'; -import 'package:immich_mobile/infrastructure/repositories/exif.repository.dart'; -import 'package:immich_mobile/infrastructure/repositories/user.repository.dart'; -import 'package:immich_mobile/models/backup/backup_candidate.model.dart'; -import 'package:immich_mobile/providers/api.provider.dart'; -import 'package:immich_mobile/providers/infrastructure/exif.provider.dart'; -import 'package:immich_mobile/providers/infrastructure/user.provider.dart'; -import 'package:immich_mobile/repositories/asset.repository.dart'; -import 'package:immich_mobile/repositories/asset_api.repository.dart'; -import 'package:immich_mobile/repositories/asset_media.repository.dart'; -import 'package:immich_mobile/repositories/backup.repository.dart'; -import 'package:immich_mobile/repositories/etag.repository.dart'; -import 'package:immich_mobile/services/album.service.dart'; -import 'package:immich_mobile/services/api.service.dart'; -import 'package:immich_mobile/services/backup.service.dart'; -import 'package:immich_mobile/services/sync.service.dart'; -import 'package:logging/logging.dart'; -import 'package:maplibre_gl/maplibre_gl.dart'; -import 'package:openapi/api.dart'; -import 'package:immich_mobile/utils/debug_print.dart'; - -final assetServiceProvider = Provider( - (ref) => AssetService( - ref.watch(assetApiRepositoryProvider), - ref.watch(assetRepositoryProvider), - ref.watch(exifRepositoryProvider), - ref.watch(userRepositoryProvider), - ref.watch(etagRepositoryProvider), - ref.watch(backupAlbumRepositoryProvider), - ref.watch(apiServiceProvider), - ref.watch(syncServiceProvider), - ref.watch(backupServiceProvider), - ref.watch(albumServiceProvider), - ref.watch(userServiceProvider), - ref.watch(assetMediaRepositoryProvider), - ), -); - -class AssetService { - final AssetApiRepository _assetApiRepository; - final AssetRepository _assetRepository; - final IsarExifRepository _exifInfoRepository; - final IsarUserRepository _isarUserRepository; - final ETagRepository _etagRepository; - final BackupAlbumRepository _backupRepository; - final ApiService _apiService; - final SyncService _syncService; - final BackupService _backupService; - final AlbumService _albumService; - final UserService _userService; - final AssetMediaRepository _assetMediaRepository; - final log = Logger('AssetService'); - - AssetService( - this._assetApiRepository, - this._assetRepository, - this._exifInfoRepository, - this._isarUserRepository, - this._etagRepository, - this._backupRepository, - this._apiService, - this._syncService, - this._backupService, - this._albumService, - this._userService, - this._assetMediaRepository, - ); - - /// Checks the server for updated assets and updates the local database if - /// required. Returns `true` if there were any changes. - Future refreshRemoteAssets() async { - final syncedUserIds = await _etagRepository.getAllIds(); - final List syncedUsers = syncedUserIds.isEmpty - ? [] - : (await _isarUserRepository.getByUserIds(syncedUserIds)).nonNulls.toList(); - final Stopwatch sw = Stopwatch()..start(); - final bool changes = await _syncService.syncRemoteAssetsToDb( - users: syncedUsers, - getChangedAssets: _getRemoteAssetChanges, - loadAssets: _getRemoteAssets, - ); - dPrint(() => "refreshRemoteAssets full took ${sw.elapsedMilliseconds}ms"); - return changes; - } - - /// Returns `(null, null)` if changes are invalid -> requires full sync - Future<(List? toUpsert, List? toDelete)> _getRemoteAssetChanges( - List users, - DateTime since, - ) async { - final dto = AssetDeltaSyncDto(updatedAfter: since, userIds: users.map((e) => e.id).toList()); - final changes = await _apiService.syncApi.getDeltaSync(dto); - return changes == null || changes.needsFullSync - ? (null, null) - : (changes.upserted.map(Asset.remote).toList(), changes.deleted); - } - - /// Returns the list of people of the given asset id. - // If the server is not reachable `null` is returned. - Future?> getRemotePeopleOfAsset(String remoteId) async { - try { - final AssetResponseDto? dto = await _apiService.assetsApi.getAssetInfo(remoteId); - - return dto?.people; - } catch (error, stack) { - log.severe('Error while getting remote asset info: ${error.toString()}', error, stack); - - return null; - } - } - - /// Returns `null` if the server state did not change, else list of assets - Future?> _getRemoteAssets(UserDto user, DateTime until) async { - const int chunkSize = 10000; - try { - final List allAssets = []; - String? lastId; - // will break on error or once all assets are loaded - while (true) { - final dto = AssetFullSyncDto(limit: chunkSize, updatedUntil: until, lastId: lastId, userId: user.id); - log.fine("Requesting $chunkSize assets from $lastId"); - final List? assets = await _apiService.syncApi.getFullSyncForUser(dto); - if (assets == null) return null; - log.fine("Received ${assets.length} assets from ${assets.firstOrNull?.id} to ${assets.lastOrNull?.id}"); - allAssets.addAll(assets.map(Asset.remote)); - if (assets.length != chunkSize) break; - lastId = assets.last.id; - } - return allAssets; - } catch (error, stack) { - log.severe('Error while getting remote assets', error, stack); - return null; - } - } - - /// Loads the exif information from the database. If there is none, loads - /// the exif info from the server (remote assets only) - Future loadExif(Asset a) async { - a.exifInfo ??= (await _exifInfoRepository.get(a.id)); - // fileSize is always filled on the server but not set on client - if (a.exifInfo?.fileSize == null) { - if (a.isRemote) { - final dto = await _apiService.assetsApi.getAssetInfo(a.remoteId!); - if (dto != null && dto.exifInfo != null) { - final newExif = Asset.remote(dto).exifInfo!.copyWith(assetId: a.id); - a.exifInfo = newExif; - if (newExif != a.exifInfo) { - if (a.isInDb) { - await _assetRepository.transaction(() => _assetRepository.update(a)); - } else { - dPrint(() => "[loadExif] parameter Asset is not from DB!"); - } - } - } - } else { - // TODO implement local exif info parsing - } - } - return a; - } - - Future updateAssets(List assets, UpdateAssetDto updateAssetDto) async { - return await _apiService.assetsApi.updateAssets( - AssetBulkUpdateDto( - ids: assets.map((e) => e.remoteId!).toList(), - dateTimeOriginal: updateAssetDto.dateTimeOriginal, - isFavorite: updateAssetDto.isFavorite, - visibility: updateAssetDto.visibility, - latitude: updateAssetDto.latitude, - longitude: updateAssetDto.longitude, - ), - ); - } - - Future> changeFavoriteStatus(List assets, bool isFavorite) async { - try { - await updateAssets(assets, UpdateAssetDto(isFavorite: isFavorite)); - - for (var element in assets) { - element.isFavorite = isFavorite; - } - - await _syncService.upsertAssetsWithExif(assets); - - return assets; - } catch (error, stack) { - log.severe("Error while changing favorite status", error, stack); - return []; - } - } - - Future> changeArchiveStatus(List assets, bool isArchived) async { - try { - await updateAssets( - assets, - UpdateAssetDto(visibility: isArchived ? AssetVisibility.archive : AssetVisibility.timeline), - ); - - for (var element in assets) { - element.isArchived = isArchived; - element.visibility = isArchived ? AssetVisibilityEnum.archive : AssetVisibilityEnum.timeline; - } - - await _syncService.upsertAssetsWithExif(assets); - - return assets; - } catch (error, stack) { - log.severe("Error while changing archive status", error, stack); - return []; - } - } - - Future?> changeDateTime(List assets, String updatedDt) async { - try { - await updateAssets(assets, UpdateAssetDto(dateTimeOriginal: updatedDt)); - - for (var element in assets) { - element.fileCreatedAt = DateTime.parse(updatedDt); - element.exifInfo = element.exifInfo?.copyWith(dateTimeOriginal: DateTime.parse(updatedDt)); - } - - await _syncService.upsertAssetsWithExif(assets); - - return assets; - } catch (error, stack) { - log.severe("Error while changing date/time status", error, stack); - return Future.value(null); - } - } - - Future?> changeLocation(List assets, LatLng location) async { - try { - await updateAssets(assets, UpdateAssetDto(latitude: location.latitude, longitude: location.longitude)); - - for (var element in assets) { - element.exifInfo = element.exifInfo?.copyWith(latitude: location.latitude, longitude: location.longitude); - } - - await _syncService.upsertAssetsWithExif(assets); - - return assets; - } catch (error, stack) { - log.severe("Error while changing location status", error, stack); - return Future.value(null); - } - } - - Future syncUploadedAssetToAlbums() async { - try { - final selectedAlbums = await _backupRepository.getAllBySelection(BackupSelection.select); - final excludedAlbums = await _backupRepository.getAllBySelection(BackupSelection.exclude); - - final candidates = await _backupService.buildUploadCandidates( - selectedAlbums, - excludedAlbums, - useTimeFilter: false, - ); - - await refreshRemoteAssets(); - final owner = _userService.getMyUser(); - final remoteAssets = await _assetRepository.getAll(ownerId: owner.id, state: AssetState.merged); - - /// Map - Map> assetToAlbums = {}; - - for (BackupCandidate candidate in candidates) { - final asset = remoteAssets.firstWhereOrNull((a) => a.localId == candidate.asset.localId); - - if (asset != null) { - for (final albumName in candidate.albumNames) { - assetToAlbums.putIfAbsent(albumName, () => []).add(asset.remoteId!); - } - } - } - - // Upload assets to albums - for (final entry in assetToAlbums.entries) { - final albumName = entry.key; - final assetIds = entry.value; - - await _albumService.syncUploadAlbums([albumName], assetIds); - } - } catch (error, stack) { - log.severe("Error while syncing uploaded asset to albums", error, stack); - } - } - - Future setDescription(Asset asset, String newDescription) async { - final remoteAssetId = asset.remoteId; - final localExifId = asset.exifInfo?.assetId; - - // Guard [remoteAssetId] and [localExifId] null - if (remoteAssetId == null || localExifId == null) { - return; - } - - final result = await _assetApiRepository.update(remoteAssetId, description: newDescription); - - final description = result.exifInfo?.description; - - if (description != null) { - var exifInfo = await _exifInfoRepository.get(localExifId); - - if (exifInfo != null) { - await _exifInfoRepository.update(exifInfo.copyWith(description: description)); - } - } - } - - Future getDescription(Asset asset) async { - final localExifId = asset.exifInfo?.assetId; - - // Guard [remoteAssetId] and [localExifId] null - if (localExifId == null) { - return ""; - } - - final exifInfo = await _exifInfoRepository.get(localExifId); - - return exifInfo?.description ?? ""; - } - - Future getAspectRatio(Asset asset) async { - if (asset.isRemote) { - asset = await loadExif(asset); - } else if (asset.isLocal) { - await asset.localAsync; - } - - final aspectRatio = asset.aspectRatio; - if (aspectRatio != null) { - return aspectRatio; - } - - final width = asset.width; - final height = asset.height; - if (width != null && height != null) { - // we don't know the orientation, so assume it's normal - return width / height; - } - - return 1.0; - } - - Future> getStackAssets(String stackId) { - return _assetRepository.getStackAssets(stackId); - } - - Future clearTable() { - return _assetRepository.clearTable(); - } - - /// Delete assets from local file system and unreference from the database - Future deleteLocalAssets(Iterable assets) async { - // Delete files from local gallery - final candidates = assets.where((asset) => asset.isLocal); - - final deletedIds = await _assetMediaRepository.deleteAll(candidates.map((asset) => asset.localId!).toList()); - - // Modify local database by removing the reference to the local assets - if (deletedIds.isNotEmpty) { - // Delete records from local database - final isarIds = assets.where((asset) => asset.storage == AssetState.local).map((asset) => asset.id).toList(); - await _assetRepository.deleteByIds(isarIds); - - // Modify Merged asset to be remote only - final updatedAssets = assets.where((asset) => asset.storage == AssetState.merged).map((asset) { - asset.localId = null; - return asset; - }).toList(); - - await _assetRepository.updateAll(updatedAssets); - } - } - - /// Delete assets from the server and unreference from the database - Future deleteRemoteAssets(Iterable assets, {bool shouldDeletePermanently = false}) async { - final candidates = assets.where((a) => a.isRemote); - - if (candidates.isEmpty) { - return; - } - - await _apiService.assetsApi.deleteAssets( - AssetBulkDeleteDto(ids: candidates.map((a) => a.remoteId!).toList(), force: shouldDeletePermanently), - ); - - /// Update asset info bassed on the deletion type. - final payload = shouldDeletePermanently - ? assets.where((asset) => asset.storage == AssetState.merged).map((asset) { - asset.remoteId = null; - asset.visibility = AssetVisibilityEnum.timeline; - return asset; - }) - : assets.where((asset) => asset.isRemote).map((asset) { - asset.isTrashed = true; - return asset; - }); - - await _assetRepository.transaction(() async { - await _assetRepository.updateAll(payload.toList()); - - if (shouldDeletePermanently) { - final remoteAssetIds = assets - .where((asset) => asset.storage == AssetState.remote) - .map((asset) => asset.id) - .toList(); - await _assetRepository.deleteByIds(remoteAssetIds); - } - }); - } - - /// Delete assets on both local file system and the server. - /// Unreference from the database. - Future deleteAssets(Iterable assets, {bool shouldDeletePermanently = false}) async { - final hasLocal = assets.any((asset) => asset.isLocal); - final hasRemote = assets.any((asset) => asset.isRemote); - - if (hasLocal) { - await deleteLocalAssets(assets); - } - - if (hasRemote) { - await deleteRemoteAssets(assets, shouldDeletePermanently: shouldDeletePermanently); - } - } - - Stream watchAsset(int id, {bool fireImmediately = false}) { - return _assetRepository.watchAsset(id, fireImmediately: fireImmediately); - } - - Future> getRecentlyTakenAssets() { - final me = _userService.getMyUser(); - return _assetRepository.getRecentlyTakenAssets(me.id); - } - - Future> getMotionAssets() { - final me = _userService.getMyUser(); - return _assetRepository.getMotionAssets(me.id); - } - - Future setVisibility(List assets, AssetVisibilityEnum visibility) async { - await _assetApiRepository.updateVisibility(assets.map((asset) => asset.remoteId!).toList(), visibility); - - final updatedAssets = assets.map((asset) { - asset.visibility = visibility; - return asset; - }).toList(); - - await _assetRepository.updateAll(updatedAssets); - } - - Future getAssetByRemoteId(String remoteId) async { - final assets = await _assetRepository.getAllByRemoteId([remoteId]); - return assets.isNotEmpty ? assets.first : null; - } -} diff --git a/mobile/lib/services/background.service.dart b/mobile/lib/services/background.service.dart deleted file mode 100644 index 03278d25fc..0000000000 --- a/mobile/lib/services/background.service.dart +++ /dev/null @@ -1,595 +0,0 @@ -import 'dart:async'; -import 'dart:developer'; -import 'dart:io'; -import 'dart:isolate'; -import 'dart:ui' show DartPluginRegistrant, IsolateNameServer, PluginUtilities; - -import 'package:collection/collection.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter/widgets.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/domain/models/store.model.dart'; -import 'package:immich_mobile/entities/backup_album.entity.dart'; -import 'package:immich_mobile/entities/store.entity.dart'; -import 'package:immich_mobile/models/backup/backup_candidate.model.dart'; -import 'package:immich_mobile/models/backup/current_upload_asset.model.dart'; -import 'package:immich_mobile/models/backup/error_upload_asset.model.dart'; -import 'package:immich_mobile/providers/api.provider.dart'; -import 'package:immich_mobile/providers/app_settings.provider.dart'; -import 'package:immich_mobile/providers/db.provider.dart'; -import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; -import 'package:immich_mobile/repositories/backup.repository.dart'; -import 'package:immich_mobile/repositories/file_media.repository.dart'; -import 'package:immich_mobile/services/app_settings.service.dart'; -import 'package:immich_mobile/services/auth.service.dart'; -import 'package:immich_mobile/services/backup.service.dart'; -import 'package:immich_mobile/services/localization.service.dart'; -import 'package:immich_mobile/utils/backup_progress.dart'; -import 'package:immich_mobile/utils/bootstrap.dart'; -import 'package:immich_mobile/utils/debug_print.dart'; -import 'package:immich_mobile/utils/diff.dart'; -import 'package:path_provider_foundation/path_provider_foundation.dart'; -import 'package:photo_manager/photo_manager.dart' show PMProgressHandler; - -final backgroundServiceProvider = Provider((ref) => BackgroundService()); - -/// Background backup service -class BackgroundService { - static const String _portNameLock = "immichLock"; - static const MethodChannel _foregroundChannel = MethodChannel('immich/foregroundChannel'); - static const MethodChannel _backgroundChannel = MethodChannel('immich/backgroundChannel'); - static const notifyInterval = Duration(milliseconds: 400); - bool _isBackgroundInitialized = false; - Completer? _cancellationToken; - bool _canceledBySystem = false; - int _wantsLockTime = 0; - bool _hasLock = false; - SendPort? _waitingIsolate; - ReceivePort? _rp; - bool _errorGracePeriodExceeded = true; - int _uploadedAssetsCount = 0; - int _assetsToUploadCount = 0; - String _lastPrintedDetailContent = ""; - String? _lastPrintedDetailTitle; - late final ThrottleProgressUpdate _throttledNotifiy = ThrottleProgressUpdate(_updateProgress, notifyInterval); - late final ThrottleProgressUpdate _throttledDetailNotify = ThrottleProgressUpdate( - _updateDetailProgress, - notifyInterval, - ); - - bool get isBackgroundInitialized { - return _isBackgroundInitialized; - } - - /// Ensures that the background service is enqueued if enabled in settings - Future resumeServiceIfEnabled() async { - return await isBackgroundBackupEnabled() && await enableService(); - } - - /// Enqueues the background service - Future enableService({bool immediate = false}) async { - try { - final callback = PluginUtilities.getCallbackHandle(_nativeEntry)!; - final String title = "backup_background_service_default_notification".tr(); - final bool ok = await _foregroundChannel.invokeMethod('enable', [callback.toRawHandle(), title, immediate]); - return ok; - } catch (error) { - return false; - } - } - - /// Configures the background service - Future configureService({ - bool requireUnmetered = true, - bool requireCharging = false, - int triggerUpdateDelay = 5000, - int triggerMaxDelay = 50000, - }) async { - try { - final bool ok = await _foregroundChannel.invokeMethod('configure', [ - requireUnmetered, - requireCharging, - triggerUpdateDelay, - triggerMaxDelay, - ]); - return ok; - } catch (error) { - return false; - } - } - - /// Cancels the background service (if currently running) and removes it from work queue - Future disableService() async { - try { - final ok = await _foregroundChannel.invokeMethod('disable'); - return ok; - } catch (error) { - return false; - } - } - - /// Returns `true` if the background service is enabled - Future isBackgroundBackupEnabled() async { - try { - return await _foregroundChannel.invokeMethod("isEnabled"); - } catch (error) { - return false; - } - } - - /// Returns `true` if battery optimizations are disabled - Future isIgnoringBatteryOptimizations() async { - // iOS does not need battery optimizations enabled - if (Platform.isIOS) { - return true; - } - try { - return await _foregroundChannel.invokeMethod('isIgnoringBatteryOptimizations'); - } catch (error) { - return false; - } - } - - // Yet to be implemented - Future digestFile(String path) { - return _foregroundChannel.invokeMethod("digestFile", [path]); - } - - Future?> digestFiles(List paths) { - return _foregroundChannel.invokeListMethod("digestFiles", paths); - } - - /// Updates the notification shown by the background service - Future _updateNotification({ - String? title, - String? content, - int progress = 0, - int max = 0, - bool indeterminate = false, - bool isDetail = false, - bool onlyIfFG = false, - }) async { - try { - if (_isBackgroundInitialized) { - return _backgroundChannel.invokeMethod('updateNotification', [ - title, - content, - progress, - max, - indeterminate, - isDetail, - onlyIfFG, - ]); - } - } catch (error) { - dPrint(() => "[_updateNotification] failed to communicate with plugin"); - } - return false; - } - - /// Shows a new priority notification - Future _showErrorNotification({required String title, String? content, String? individualTag}) async { - try { - if (_isBackgroundInitialized && _errorGracePeriodExceeded) { - return await _backgroundChannel.invokeMethod('showError', [title, content, individualTag]); - } - } catch (error) { - dPrint(() => "[_showErrorNotification] failed to communicate with plugin"); - } - return false; - } - - Future _clearErrorNotifications() async { - try { - if (_isBackgroundInitialized) { - return await _backgroundChannel.invokeMethod('clearErrorNotifications'); - } - } catch (error) { - dPrint(() => "[_clearErrorNotifications] failed to communicate with plugin"); - } - return false; - } - - /// await to ensure this thread (foreground or background) has exclusive access - Future acquireLock() async { - if (_hasLock) { - dPrint(() => "WARNING: [acquireLock] called more than once"); - return true; - } - final int lockTime = Timeline.now; - _wantsLockTime = lockTime; - final ReceivePort rp = ReceivePort(_portNameLock); - _rp = rp; - final SendPort sp = rp.sendPort; - - while (!IsolateNameServer.registerPortWithName(sp, _portNameLock)) { - try { - await _checkLockReleasedWithHeartbeat(lockTime); - } catch (error) { - return false; - } - if (_wantsLockTime != lockTime) { - return false; - } - } - _hasLock = true; - rp.listen(_heartbeatListener); - return true; - } - - Future _checkLockReleasedWithHeartbeat(final int lockTime) async { - SendPort? other = IsolateNameServer.lookupPortByName(_portNameLock); - if (other != null) { - final ReceivePort tempRp = ReceivePort(); - final SendPort tempSp = tempRp.sendPort; - final bs = tempRp.asBroadcastStream(); - while (_wantsLockTime == lockTime) { - other.send(tempSp); - final dynamic answer = await bs.first.timeout(const Duration(seconds: 3), onTimeout: () => null); - if (_wantsLockTime != lockTime) { - break; - } - if (answer == null) { - // other isolate failed to answer, assuming it exited without releasing the lock - if (other == IsolateNameServer.lookupPortByName(_portNameLock)) { - IsolateNameServer.removePortNameMapping(_portNameLock); - } - break; - } else if (answer == true) { - // other isolate released the lock - break; - } else if (answer == false) { - // other isolate is still active - } - final dynamic isFinished = await bs.first.timeout(const Duration(seconds: 3), onTimeout: () => false); - if (isFinished == true) { - break; - } - } - tempRp.close(); - } - } - - void _heartbeatListener(dynamic msg) { - if (msg is SendPort) { - _waitingIsolate = msg; - msg.send(false); - } - } - - /// releases the exclusive access lock - void releaseLock() { - _wantsLockTime = 0; - if (_hasLock) { - IsolateNameServer.removePortNameMapping(_portNameLock); - _waitingIsolate?.send(true); - _waitingIsolate = null; - _hasLock = false; - } - _rp?.close(); - _rp = null; - } - - void _setupBackgroundCallHandler() { - _backgroundChannel.setMethodCallHandler(_callHandler); - _isBackgroundInitialized = true; - _backgroundChannel.invokeMethod('initialized'); - } - - Future _callHandler(MethodCall call) async { - DartPluginRegistrant.ensureInitialized(); - if (Platform.isIOS) { - // NOTE: I'm not sure this is strictly necessary anymore, but - // out of an abundance of caution, we will keep it in until someone - // can say for sure - PathProviderFoundation.registerWith(); - } - switch (call.method) { - case "backgroundProcessing": - case "onAssetsChanged": - try { - unawaited(_clearErrorNotifications()); - - // iOS should time out after some threshold so it doesn't wait - // indefinitely and can run later - // Android is fine to wait here until the lock releases - final waitForLock = Platform.isIOS - ? acquireLock().timeout(const Duration(seconds: 5), onTimeout: () => false) - : acquireLock(); - - final bool hasAccess = await waitForLock; - if (!hasAccess) { - dPrint(() => "[_callHandler] could not acquire lock, exiting"); - return false; - } - - final translationsOk = await loadTranslations(); - if (!translationsOk) { - dPrint(() => "[_callHandler] could not load translations"); - } - - final bool ok = await _onAssetsChanged(); - return ok; - } catch (error) { - dPrint(() => error.toString()); - return false; - } finally { - releaseLock(); - } - case "systemStop": - _canceledBySystem = true; - _cancellationToken?.complete(); - _cancellationToken = null; - return true; - default: - dPrint(() => "Unknown method ${call.method}"); - return false; - } - } - - Future _onAssetsChanged() async { - final (isar, drift, logDb) = await Bootstrap.initDB(); - await Bootstrap.initDomain(isar, drift, logDb, shouldBufferLogs: false, listenStoreUpdates: false); - - final ref = ProviderContainer( - overrides: [ - dbProvider.overrideWithValue(isar), - isarProvider.overrideWithValue(isar), - driftProvider.overrideWith(driftOverride(drift)), - ], - ); - - await ref.read(authServiceProvider).setOpenApiServiceEndpoint(); - dPrint(() => "[BG UPLOAD] Using endpoint: ${ref.read(apiServiceProvider).apiClient.basePath}"); - - final selectedAlbums = await ref.read(backupAlbumRepositoryProvider).getAllBySelection(BackupSelection.select); - final excludedAlbums = await ref.read(backupAlbumRepositoryProvider).getAllBySelection(BackupSelection.exclude); - if (selectedAlbums.isEmpty) { - return true; - } - - await ref.read(fileMediaRepositoryProvider).enableBackgroundAccess(); - - do { - final bool backupOk = await _runBackup( - ref.read(backupServiceProvider), - ref.read(appSettingsServiceProvider), - selectedAlbums, - excludedAlbums, - ); - if (backupOk) { - await Store.delete(StoreKey.backupFailedSince); - final backupAlbums = [...selectedAlbums, ...excludedAlbums]; - backupAlbums.sortBy((e) => e.id); - - final dbAlbums = await ref.read(backupAlbumRepositoryProvider).getAll(sort: BackupAlbumSort.id); - final List toDelete = []; - final List toUpsert = []; - // stores the most recent `lastBackup` per album but always keeps the `selection` from the most recent DB state - diffSortedListsSync( - dbAlbums, - backupAlbums, - compare: (BackupAlbum a, BackupAlbum b) => a.id.compareTo(b.id), - both: (BackupAlbum a, BackupAlbum b) { - a.lastBackup = a.lastBackup.isAfter(b.lastBackup) ? a.lastBackup : b.lastBackup; - toUpsert.add(a); - return true; - }, - onlyFirst: (BackupAlbum a) => toUpsert.add(a), - onlySecond: (BackupAlbum b) => toDelete.add(b.isarId), - ); - await ref.read(backupAlbumRepositoryProvider).deleteAll(toDelete); - await ref.read(backupAlbumRepositoryProvider).updateAll(toUpsert); - } else if (Store.tryGet(StoreKey.backupFailedSince) == null) { - await Store.put(StoreKey.backupFailedSince, DateTime.now()); - return false; - } - // Android should check for new assets added while performing backup - } while (Platform.isAndroid && true == await _backgroundChannel.invokeMethod("hasContentChanged")); - return true; - } - - Future _runBackup( - BackupService backupService, - AppSettingsService settingsService, - List selectedAlbums, - List excludedAlbums, - ) async { - _errorGracePeriodExceeded = _isErrorGracePeriodExceeded(settingsService); - final bool notifyTotalProgress = settingsService.getSetting(AppSettingsEnum.backgroundBackupTotalProgress); - final bool notifySingleProgress = settingsService.getSetting(AppSettingsEnum.backgroundBackupSingleProgress); - - if (_canceledBySystem) { - return false; - } - - Set toUpload = await backupService.buildUploadCandidates(selectedAlbums, excludedAlbums); - - try { - toUpload = await backupService.removeAlreadyUploadedAssets(toUpload); - } catch (e) { - unawaited( - _showErrorNotification( - title: "backup_background_service_error_title".tr(), - content: "backup_background_service_connection_failed_message".tr(), - ), - ); - return false; - } - - if (_canceledBySystem) { - return false; - } - - if (toUpload.isEmpty) { - return true; - } - _assetsToUploadCount = toUpload.length; - _uploadedAssetsCount = 0; - unawaited( - _updateNotification( - title: "backup_background_service_in_progress_notification".tr(), - content: notifyTotalProgress ? formatAssetBackupProgress(_uploadedAssetsCount, _assetsToUploadCount) : null, - progress: 0, - max: notifyTotalProgress ? _assetsToUploadCount : 0, - indeterminate: !notifyTotalProgress, - onlyIfFG: !notifyTotalProgress, - ), - ); - - _cancellationToken?.complete(); - _cancellationToken = Completer(); - final pmProgressHandler = Platform.isIOS ? PMProgressHandler() : null; - - final bool ok = await backupService.backupAsset( - toUpload, - _cancellationToken!, - pmProgressHandler: pmProgressHandler, - onSuccess: (result) => _onAssetUploaded(shouldNotify: notifyTotalProgress), - onProgress: (bytes, totalBytes) => _onProgress(bytes, totalBytes, shouldNotify: notifySingleProgress), - onCurrentAsset: (asset) => _onSetCurrentBackupAsset(asset, shouldNotify: notifySingleProgress), - onError: _onBackupError, - isBackground: true, - ); - - if (!ok && !_cancellationToken!.isCompleted) { - unawaited( - _showErrorNotification( - title: "backup_background_service_error_title".tr(), - content: "backup_background_service_backup_failed_message".tr(), - ), - ); - } - - return ok; - } - - void _onAssetUploaded({bool shouldNotify = false}) { - if (!shouldNotify) { - return; - } - - _uploadedAssetsCount++; - _throttledNotifiy(); - } - - void _onProgress(int bytes, int totalBytes, {bool shouldNotify = false}) { - if (!shouldNotify) { - return; - } - - _throttledDetailNotify(progress: bytes, total: totalBytes); - } - - void _updateDetailProgress(String? title, int progress, int total) { - final String msg = total > 0 ? humanReadableBytesProgress(progress, total) : ""; - // only update if message actually differs (to stop many useless notification updates on large assets or slow connections) - if (msg != _lastPrintedDetailContent || _lastPrintedDetailTitle != title) { - _lastPrintedDetailContent = msg; - _lastPrintedDetailTitle = title; - _updateNotification( - progress: total > 0 ? (progress * 1000) ~/ total : 0, - max: 1000, - isDetail: true, - title: title, - content: msg, - ); - } - } - - void _updateProgress(String? title, int progress, int total) { - _updateNotification( - progress: _uploadedAssetsCount, - max: _assetsToUploadCount, - title: title, - content: formatAssetBackupProgress(_uploadedAssetsCount, _assetsToUploadCount), - ); - } - - void _onBackupError(ErrorUploadAsset errorAssetInfo) { - _showErrorNotification( - title: "backup_background_service_upload_failure_notification".tr( - namedArgs: {'filename': errorAssetInfo.fileName}, - ), - individualTag: errorAssetInfo.id, - ); - } - - void _onSetCurrentBackupAsset(CurrentUploadAsset currentUploadAsset, {bool shouldNotify = false}) { - if (!shouldNotify) { - return; - } - - _throttledDetailNotify.title = "backup_background_service_current_upload_notification".tr( - namedArgs: {'filename': currentUploadAsset.fileName}, - ); - _throttledDetailNotify.progress = 0; - _throttledDetailNotify.total = 0; - } - - bool _isErrorGracePeriodExceeded(AppSettingsService appSettingsService) { - final int value = appSettingsService.getSetting(AppSettingsEnum.uploadErrorNotificationGracePeriod); - if (value == 0) { - return true; - } else if (value == 5) { - return false; - } - final DateTime? failedSince = Store.tryGet(StoreKey.backupFailedSince); - if (failedSince == null) { - return false; - } - final Duration duration = DateTime.now().difference(failedSince); - if (value == 1) { - return duration > const Duration(minutes: 30); - } else if (value == 2) { - return duration > const Duration(hours: 2); - } else if (value == 3) { - return duration > const Duration(hours: 8); - } else if (value == 4) { - return duration > const Duration(hours: 24); - } - assert(false, "Invalid value"); - return true; - } - - Future getIOSBackupLastRun(IosBackgroundTask task) async { - if (!Platform.isIOS) { - return null; - } - // Seconds since last run - final double? lastRun = task == IosBackgroundTask.fetch - ? await _foregroundChannel.invokeMethod('lastBackgroundFetchTime') - : await _foregroundChannel.invokeMethod('lastBackgroundProcessingTime'); - if (lastRun == null) { - return null; - } - final time = DateTime.fromMillisecondsSinceEpoch(lastRun.toInt() * 1000); - return time; - } - - Future getIOSBackupNumberOfProcesses() async { - if (!Platform.isIOS) { - return 0; - } - return await _foregroundChannel.invokeMethod('numberOfBackgroundProcesses'); - } - - Future getIOSBackgroundAppRefreshEnabled() async { - if (!Platform.isIOS) { - return false; - } - return await _foregroundChannel.invokeMethod('backgroundAppRefreshEnabled'); - } -} - -enum IosBackgroundTask { fetch, processing } - -/// entry point called by Kotlin/Java code; needs to be a top-level function -@pragma('vm:entry-point') -void _nativeEntry() { - WidgetsFlutterBinding.ensureInitialized(); - DartPluginRegistrant.ensureInitialized(); - BackgroundService backgroundService = BackgroundService(); - backgroundService._setupBackgroundCallHandler(); -} diff --git a/mobile/lib/services/backup.service.dart b/mobile/lib/services/backup.service.dart deleted file mode 100644 index 9b6a26be03..0000000000 --- a/mobile/lib/services/backup.service.dart +++ /dev/null @@ -1,473 +0,0 @@ -import 'dart:async'; -import 'dart:convert'; -import 'dart:io'; - -import 'package:collection/collection.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:http/http.dart'; -import 'package:immich_mobile/domain/models/store.model.dart'; -import 'package:immich_mobile/entities/album.entity.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/entities/backup_album.entity.dart'; -import 'package:immich_mobile/entities/store.entity.dart'; -import 'package:immich_mobile/infrastructure/repositories/network.repository.dart'; -import 'package:immich_mobile/repositories/upload.repository.dart'; -import 'package:immich_mobile/models/backup/backup_candidate.model.dart'; -import 'package:immich_mobile/models/backup/current_upload_asset.model.dart'; -import 'package:immich_mobile/models/backup/error_upload_asset.model.dart'; -import 'package:immich_mobile/models/backup/success_upload_asset.model.dart'; -import 'package:immich_mobile/providers/api.provider.dart'; -import 'package:immich_mobile/providers/app_settings.provider.dart'; -import 'package:immich_mobile/repositories/album_media.repository.dart'; -import 'package:immich_mobile/repositories/asset.repository.dart'; -import 'package:immich_mobile/repositories/asset_media.repository.dart'; -import 'package:immich_mobile/repositories/file_media.repository.dart'; -import 'package:immich_mobile/services/album.service.dart'; -import 'package:immich_mobile/services/api.service.dart'; -import 'package:immich_mobile/services/app_settings.service.dart'; -import 'package:logging/logging.dart'; -import 'package:openapi/api.dart'; -import 'package:path/path.dart' as p; -import 'package:permission_handler/permission_handler.dart' as pm; -import 'package:photo_manager/photo_manager.dart' show PMProgressHandler; -import 'package:immich_mobile/utils/debug_print.dart'; - -final backupServiceProvider = Provider( - (ref) => BackupService( - ref.watch(apiServiceProvider), - ref.watch(appSettingsServiceProvider), - ref.watch(albumServiceProvider), - ref.watch(albumMediaRepositoryProvider), - ref.watch(fileMediaRepositoryProvider), - ref.watch(assetRepositoryProvider), - ref.watch(assetMediaRepositoryProvider), - ), -); - -class BackupService { - final ApiService _apiService; - final Logger _log = Logger("BackupService"); - final AppSettingsService _appSetting; - final AlbumService _albumService; - final AlbumMediaRepository _albumMediaRepository; - final FileMediaRepository _fileMediaRepository; - final AssetRepository _assetRepository; - final AssetMediaRepository _assetMediaRepository; - - BackupService( - this._apiService, - this._appSetting, - this._albumService, - this._albumMediaRepository, - this._fileMediaRepository, - this._assetRepository, - this._assetMediaRepository, - ); - - Future?> getDeviceBackupAsset() async { - final String deviceId = Store.get(StoreKey.deviceId); - - try { - return await _apiService.assetsApi.getAllUserAssetsByDeviceId(deviceId); - } catch (e) { - dPrint(() => 'Error [getDeviceBackupAsset] ${e.toString()}'); - return null; - } - } - - Future _saveDuplicatedAssetIds(List deviceAssetIds) => - _assetRepository.transaction(() => _assetRepository.upsertDuplicatedAssets(deviceAssetIds)); - - /// Get duplicated asset id from database - Future> getDuplicatedAssetIds() async { - final duplicates = await _assetRepository.getAllDuplicatedAssetIds(); - return duplicates.toSet(); - } - - /// Returns all assets newer than the last successful backup per album - /// if `useTimeFilter` is set to true, all assets will be returned - Future> buildUploadCandidates( - List selectedBackupAlbums, - List excludedBackupAlbums, { - bool useTimeFilter = true, - }) async { - final now = DateTime.now(); - - final Set toAdd = await _fetchAssetsAndUpdateLastBackup( - selectedBackupAlbums, - now, - useTimeFilter: useTimeFilter, - ); - - if (toAdd.isEmpty) return {}; - - final Set toRemove = await _fetchAssetsAndUpdateLastBackup( - excludedBackupAlbums, - now, - useTimeFilter: useTimeFilter, - ); - - return toAdd.difference(toRemove); - } - - Future> _fetchAssetsAndUpdateLastBackup( - List backupAlbums, - DateTime now, { - bool useTimeFilter = true, - }) async { - Set candidates = {}; - - for (final BackupAlbum backupAlbum in backupAlbums) { - final Album localAlbum; - try { - localAlbum = await _albumMediaRepository.get(backupAlbum.id); - } on StateError { - // the album no longer exists - continue; - } - - if (useTimeFilter && localAlbum.modifiedAt.isBefore(backupAlbum.lastBackup)) { - continue; - } - final List assets; - try { - assets = await _albumMediaRepository.getAssets( - backupAlbum.id, - modifiedFrom: useTimeFilter - ? - // subtract 2 seconds to prevent missing assets due to rounding issues - backupAlbum.lastBackup.subtract(const Duration(seconds: 2)) - : null, - modifiedUntil: useTimeFilter ? now : null, - ); - } on StateError { - // either there are no assets matching the filter criteria OR the album no longer exists - continue; - } - - // Add album's name to the asset info - for (final asset in assets) { - List albumNames = [localAlbum.name]; - - final existingAsset = candidates.firstWhereOrNull((candidate) => candidate.asset.localId == asset.localId); - - if (existingAsset != null) { - albumNames.addAll(existingAsset.albumNames); - candidates.remove(existingAsset); - } - - candidates.add(BackupCandidate(asset: asset, albumNames: albumNames)); - } - - backupAlbum.lastBackup = now; - } - - return candidates; - } - - /// Returns a new list of assets not yet uploaded - Future> removeAlreadyUploadedAssets(Set candidates) async { - if (candidates.isEmpty) { - return candidates; - } - - final Set duplicatedAssetIds = await getDuplicatedAssetIds(); - candidates.removeWhere((candidate) => duplicatedAssetIds.contains(candidate.asset.localId)); - - if (candidates.isEmpty) { - return candidates; - } - - final Set existing = {}; - try { - final String deviceId = Store.get(StoreKey.deviceId); - final CheckExistingAssetsResponseDto? duplicates = await _apiService.assetsApi.checkExistingAssets( - CheckExistingAssetsDto(deviceAssetIds: candidates.map((c) => c.asset.localId!).toList(), deviceId: deviceId), - ); - if (duplicates != null) { - existing.addAll(duplicates.existingIds); - } - } on ApiException { - // workaround for older server versions or when checking for too many assets at once - final List? allAssetsInDatabase = await getDeviceBackupAsset(); - if (allAssetsInDatabase != null) { - existing.addAll(allAssetsInDatabase); - } - } - - if (existing.isNotEmpty) { - candidates.removeWhere((c) => existing.contains(c.asset.localId)); - } - - return candidates; - } - - Future _checkPermissions() async { - if (Platform.isAndroid && !(await pm.Permission.accessMediaLocation.status).isGranted) { - // double check that permission is granted here, to guard against - // uploading corrupt assets without EXIF information - _log.warning( - "Media location permission is not granted. " - "Cannot access original assets for backup.", - ); - - return false; - } - - // DON'T KNOW WHY BUT THIS HELPS BACKGROUND BACKUP TO WORK ON IOS - if (Platform.isIOS) { - await _fileMediaRepository.requestExtendedPermissions(); - } - - return true; - } - - /// Upload images before video assets for background tasks - /// these are further sorted by using their creation date - List _sortPhotosFirst(List candidates) { - return candidates.sorted((a, b) { - final cmp = a.asset.type.index - b.asset.type.index; - if (cmp != 0) return cmp; - return a.asset.fileCreatedAt.compareTo(b.asset.fileCreatedAt); - }); - } - - Future backupAsset( - Iterable assets, - Completer cancelToken, { - bool isBackground = false, - PMProgressHandler? pmProgressHandler, - required void Function(SuccessUploadAsset result) onSuccess, - required void Function(int bytes, int totalBytes) onProgress, - required void Function(CurrentUploadAsset asset) onCurrentAsset, - required void Function(ErrorUploadAsset error) onError, - }) async { - final bool isIgnoreIcloudAssets = _appSetting.getSetting(AppSettingsEnum.ignoreIcloudAssets); - final shouldSyncAlbums = _appSetting.getSetting(AppSettingsEnum.syncAlbums); - final String deviceId = Store.get(StoreKey.deviceId); - final String savedEndpoint = Store.get(StoreKey.serverEndpoint); - final List duplicatedAssetIds = []; - bool anyErrors = false; - - final hasPermission = await _checkPermissions(); - if (!hasPermission) { - return false; - } - - List candidates = assets.toList(); - if (isBackground) { - candidates = _sortPhotosFirst(candidates); - } - - for (final candidate in candidates) { - final Asset asset = candidate.asset; - File? file; - File? livePhotoFile; - - try { - final isAvailableLocally = await asset.local!.isLocallyAvailable(isOrigin: true); - - // Handle getting files from iCloud - if (!isAvailableLocally && Platform.isIOS) { - // Skip iCloud assets if the user has disabled this feature - if (isIgnoreIcloudAssets) { - continue; - } - - onCurrentAsset( - CurrentUploadAsset( - id: asset.localId!, - fileCreatedAt: asset.fileCreatedAt.year == 1970 ? asset.fileModifiedAt : asset.fileCreatedAt, - fileName: asset.fileName, - fileType: _getAssetType(asset.type), - iCloudAsset: true, - ), - ); - - file = await asset.local!.loadFile(progressHandler: pmProgressHandler); - if (asset.local!.isLivePhoto) { - livePhotoFile = await asset.local!.loadFile(withSubtype: true, progressHandler: pmProgressHandler); - } - } else { - file = await asset.local!.originFile.timeout(const Duration(seconds: 5)); - - if (asset.local!.isLivePhoto) { - livePhotoFile = await asset.local!.originFileWithSubtype.timeout(const Duration(seconds: 5)); - } - } - - if (file != null) { - String? originalFileName = await _assetMediaRepository.getOriginalFilename(asset.localId!); - originalFileName ??= asset.fileName; - - if (asset.local!.isLivePhoto) { - if (livePhotoFile == null) { - _log.warning("Failed to obtain motion part of the livePhoto - $originalFileName"); - } - } - - final fileStream = file.openRead(); - final assetRawUploadData = MultipartFile( - "assetData", - fileStream, - file.lengthSync(), - filename: originalFileName, - ); - - final baseRequest = ProgressMultipartRequest( - 'POST', - Uri.parse('$savedEndpoint/assets'), - abortTrigger: cancelToken.future, - onProgress: ((bytes, totalBytes) => onProgress(bytes, totalBytes)), - ); - - baseRequest.fields['deviceAssetId'] = asset.localId!; - baseRequest.fields['deviceId'] = deviceId; - baseRequest.fields['fileCreatedAt'] = asset.fileCreatedAt.toUtc().toIso8601String(); - baseRequest.fields['fileModifiedAt'] = asset.fileModifiedAt.toUtc().toIso8601String(); - baseRequest.fields['isFavorite'] = asset.isFavorite.toString(); - baseRequest.fields['duration'] = asset.duration.toString(); - baseRequest.files.add(assetRawUploadData); - - onCurrentAsset( - CurrentUploadAsset( - id: asset.localId!, - fileCreatedAt: asset.fileCreatedAt.year == 1970 ? asset.fileModifiedAt : asset.fileCreatedAt, - fileName: originalFileName, - fileType: _getAssetType(asset.type), - fileSize: file.lengthSync(), - iCloudAsset: false, - ), - ); - - String? livePhotoVideoId; - if (asset.local!.isLivePhoto && livePhotoFile != null) { - livePhotoVideoId = await uploadLivePhotoVideo(originalFileName, livePhotoFile, baseRequest, cancelToken); - } - - if (livePhotoVideoId != null) { - baseRequest.fields['livePhotoVideoId'] = livePhotoVideoId; - } - - final response = await NetworkRepository.client.send(baseRequest); - - final responseBody = jsonDecode(await response.stream.bytesToString()); - - if (![200, 201].contains(response.statusCode)) { - final error = responseBody; - final errorMessage = error['message'] ?? error['error']; - - dPrint( - () => - "Error(${error['statusCode']}) uploading ${asset.localId} | $originalFileName | Created on ${asset.fileCreatedAt} | ${error['error']}", - ); - - onError( - ErrorUploadAsset( - asset: asset, - id: asset.localId!, - fileCreatedAt: asset.fileCreatedAt, - fileName: originalFileName, - fileType: _getAssetType(candidate.asset.type), - errorMessage: errorMessage, - ), - ); - - if (errorMessage == "Quota has been exceeded!") { - anyErrors = true; - break; - } - - continue; - } - - bool isDuplicate = false; - if (response.statusCode == 200) { - isDuplicate = true; - duplicatedAssetIds.add(asset.localId!); - } - - onSuccess( - SuccessUploadAsset( - candidate: candidate, - remoteAssetId: responseBody['id'] as String, - isDuplicate: isDuplicate, - ), - ); - - if (shouldSyncAlbums) { - await _albumService.syncUploadAlbums(candidate.albumNames, [responseBody['id'] as String]); - } - } - } on RequestAbortedException { - dPrint(() => "Backup was cancelled by the user"); - anyErrors = true; - break; - } catch (error, stackTrace) { - dPrint(() => "Error backup asset: ${error.toString()}: $stackTrace"); - anyErrors = true; - continue; - } finally { - if (Platform.isIOS) { - try { - await file?.delete(); - await livePhotoFile?.delete(); - } catch (e) { - dPrint(() => "ERROR deleting file: ${e.toString()}"); - } - } - } - } - - if (duplicatedAssetIds.isNotEmpty) { - await _saveDuplicatedAssetIds(duplicatedAssetIds); - } - - return !anyErrors; - } - - Future uploadLivePhotoVideo( - String originalFileName, - File? livePhotoVideoFile, - MultipartRequest baseRequest, - Completer cancelToken, - ) async { - if (livePhotoVideoFile == null) { - return null; - } - final livePhotoTitle = p.setExtension(originalFileName, p.extension(livePhotoVideoFile.path)); - final fileStream = livePhotoVideoFile.openRead(); - final livePhotoRawUploadData = MultipartFile( - "assetData", - fileStream, - livePhotoVideoFile.lengthSync(), - filename: livePhotoTitle, - ); - final livePhotoReq = ProgressMultipartRequest(baseRequest.method, baseRequest.url, abortTrigger: cancelToken.future) - ..headers.addAll(baseRequest.headers) - ..fields.addAll(baseRequest.fields); - - livePhotoReq.files.add(livePhotoRawUploadData); - - var response = await NetworkRepository.client.send(livePhotoReq); - - var responseBody = jsonDecode(await response.stream.bytesToString()); - - if (![200, 201].contains(response.statusCode)) { - var error = responseBody; - - dPrint( - () => "Error(${error['statusCode']}) uploading livePhoto for assetId | $livePhotoTitle | ${error['error']}", - ); - } - - return responseBody.containsKey('id') ? responseBody['id'] : null; - } - - String _getAssetType(AssetType assetType) => switch (assetType) { - AssetType.audio => "AUDIO", - AssetType.image => "IMAGE", - AssetType.video => "VIDEO", - AssetType.other => "OTHER", - }; -} diff --git a/mobile/lib/services/backup_album.service.dart b/mobile/lib/services/backup_album.service.dart deleted file mode 100644 index ef9d1031de..0000000000 --- a/mobile/lib/services/backup_album.service.dart +++ /dev/null @@ -1,33 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/backup_album.entity.dart'; -import 'package:immich_mobile/repositories/backup.repository.dart'; - -final backupAlbumServiceProvider = Provider((ref) { - return BackupAlbumService(ref.watch(backupAlbumRepositoryProvider)); -}); - -class BackupAlbumService { - final BackupAlbumRepository _backupAlbumRepository; - - const BackupAlbumService(this._backupAlbumRepository); - - Future> getAll({BackupAlbumSort? sort}) { - return _backupAlbumRepository.getAll(sort: sort); - } - - Future> getIdsBySelection(BackupSelection backup) { - return _backupAlbumRepository.getIdsBySelection(backup); - } - - Future> getAllBySelection(BackupSelection backup) { - return _backupAlbumRepository.getAllBySelection(backup); - } - - Future deleteAll(List ids) { - return _backupAlbumRepository.deleteAll(ids); - } - - Future updateAll(List backupAlbums) { - return _backupAlbumRepository.updateAll(backupAlbums); - } -} diff --git a/mobile/lib/services/backup_verification.service.dart b/mobile/lib/services/backup_verification.service.dart deleted file mode 100644 index 2efd52cc81..0000000000 --- a/mobile/lib/services/backup_verification.service.dart +++ /dev/null @@ -1,192 +0,0 @@ -import 'dart:async'; -import 'dart:typed_data'; - -import 'package:collection/collection.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/services.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/domain/models/exif.model.dart'; -import 'package:immich_mobile/domain/models/store.model.dart'; -import 'package:immich_mobile/domain/services/user.service.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/entities/store.entity.dart'; -import 'package:immich_mobile/infrastructure/repositories/exif.repository.dart'; -import 'package:immich_mobile/infrastructure/utils/exif.converter.dart'; -import 'package:immich_mobile/providers/infrastructure/exif.provider.dart'; -import 'package:immich_mobile/providers/infrastructure/user.provider.dart'; -import 'package:immich_mobile/repositories/asset.repository.dart'; -import 'package:immich_mobile/repositories/file_media.repository.dart'; -import 'package:immich_mobile/services/api.service.dart'; -import 'package:immich_mobile/utils/bootstrap.dart'; -import 'package:immich_mobile/utils/diff.dart'; - -/// Finds duplicates originating from missing EXIF information -class BackupVerificationService { - final UserService _userService; - final FileMediaRepository _fileMediaRepository; - final AssetRepository _assetRepository; - final IsarExifRepository _exifInfoRepository; - - const BackupVerificationService( - this._userService, - this._fileMediaRepository, - this._assetRepository, - this._exifInfoRepository, - ); - - /// Returns at most [limit] assets that were backed up without exif - Future> findWronglyBackedUpAssets({int limit = 100}) async { - final owner = _userService.getMyUser().id; - final List onlyLocal = await _assetRepository.getAll(ownerId: owner, state: AssetState.local, limit: limit); - final List remoteMatches = await _assetRepository.getMatches( - assets: onlyLocal, - ownerId: owner, - state: AssetState.remote, - limit: limit, - ); - final List localMatches = await _assetRepository.getMatches( - assets: remoteMatches, - ownerId: owner, - state: AssetState.local, - limit: limit, - ); - - final List deleteCandidates = [], originals = []; - - await diffSortedLists( - remoteMatches, - localMatches, - compare: (a, b) => a.fileName.compareTo(b.fileName), - both: (a, b) async { - a.exifInfo = await _exifInfoRepository.get(a.id); - deleteCandidates.add(a); - originals.add(b); - return false; - }, - onlyFirst: (a) {}, - onlySecond: (b) {}, - ); - final isolateToken = ServicesBinding.rootIsolateToken!; - final List toDelete; - if (deleteCandidates.length > 10) { - // performs 2 checks in parallel for a nice speedup - final half = deleteCandidates.length ~/ 2; - final lower = compute(_computeSaveToDelete, ( - deleteCandidates: deleteCandidates.slice(0, half), - originals: originals.slice(0, half), - endpoint: Store.get(StoreKey.serverEndpoint), - rootIsolateToken: isolateToken, - fileMediaRepository: _fileMediaRepository, - )); - final upper = compute(_computeSaveToDelete, ( - deleteCandidates: deleteCandidates.slice(half), - originals: originals.slice(half), - endpoint: Store.get(StoreKey.serverEndpoint), - rootIsolateToken: isolateToken, - fileMediaRepository: _fileMediaRepository, - )); - toDelete = await lower + await upper; - } else { - toDelete = await compute(_computeSaveToDelete, ( - deleteCandidates: deleteCandidates, - originals: originals, - endpoint: Store.get(StoreKey.serverEndpoint), - rootIsolateToken: isolateToken, - fileMediaRepository: _fileMediaRepository, - )); - } - return toDelete; - } - - static Future> _computeSaveToDelete( - ({ - List deleteCandidates, - List originals, - String endpoint, - RootIsolateToken rootIsolateToken, - FileMediaRepository fileMediaRepository, - }) - tuple, - ) async { - assert(tuple.deleteCandidates.length == tuple.originals.length); - final List result = []; - BackgroundIsolateBinaryMessenger.ensureInitialized(tuple.rootIsolateToken); - final (isar, drift, logDb) = await Bootstrap.initDB(); - await Bootstrap.initDomain(isar, drift, logDb); - await tuple.fileMediaRepository.enableBackgroundAccess(); - final ApiService apiService = ApiService(); - apiService.setEndpoint(tuple.endpoint); - for (int i = 0; i < tuple.deleteCandidates.length; i++) { - if (await _compareAssets(tuple.deleteCandidates[i], tuple.originals[i], apiService)) { - result.add(tuple.deleteCandidates[i]); - } - } - return result; - } - - static Future _compareAssets(Asset remote, Asset local, ApiService apiService) async { - if (remote.checksum == local.checksum) return false; - ExifInfo? exif = remote.exifInfo; - if (exif != null && exif.latitude != null) return false; - if (exif == null || exif.fileSize == null) { - final dto = await apiService.assetsApi.getAssetInfo(remote.remoteId!); - if (dto != null && dto.exifInfo != null) { - exif = ExifDtoConverter.fromDto(dto.exifInfo!); - } - } - final file = await local.local!.originFile; - if (exif != null && file != null && exif.fileSize != null) { - final origSize = await file.length(); - if (exif.fileSize! == origSize || exif.fileSize! != origSize) { - final latLng = await local.local!.latlngAsync(); - - if (exif.latitude == null && - latLng.latitude != null && - (remote.fileCreatedAt.isAtSameMomentAs(local.fileCreatedAt) || - remote.fileModifiedAt.isAtSameMomentAs(local.fileModifiedAt) || - _sameExceptTimeZone(remote.fileCreatedAt, local.fileCreatedAt))) { - if (remote.type == AssetType.video) { - // it's very unlikely that a video of same length, filesize, name - // and date is wrong match. Cannot easily compare videos anyway - return true; - } - - // for images: make sure they are pixel-wise identical - // (skip first few KBs containing metadata) - final Uint64List localImage = _fakeDecodeImg(await file.readAsBytes()); - final res = await apiService.assetsApi.downloadAssetWithHttpInfo(remote.remoteId!); - final Uint64List remoteImage = _fakeDecodeImg(res.bodyBytes); - - final eq = const ListEquality().equals(remoteImage, localImage); - return eq; - } - } - } - - return false; - } - - static Uint64List _fakeDecodeImg(Uint8List bytes) { - const headerLength = 131072; // assume header is at most 128 KB - final start = bytes.length < headerLength * 2 ? (bytes.length ~/ (4 * 8)) * 8 : headerLength; - return bytes.buffer.asUint64List(start); - } - - static bool _sameExceptTimeZone(DateTime a, DateTime b) { - final ms = a.isAfter(b) - ? a.millisecondsSinceEpoch - b.millisecondsSinceEpoch - : b.millisecondsSinceEpoch - a.microsecondsSinceEpoch; - final x = ms / (1000 * 60 * 30); - final y = ms ~/ (1000 * 60 * 30); - return y.toDouble() == x && y < 24; - } -} - -final backupVerificationServiceProvider = Provider( - (ref) => BackupVerificationService( - ref.watch(userServiceProvider), - ref.watch(fileMediaRepositoryProvider), - ref.watch(assetRepositoryProvider), - ref.watch(exifRepositoryProvider), - ), -); diff --git a/mobile/lib/services/deep_link.service.dart b/mobile/lib/services/deep_link.service.dart index 9d2bdbe4a0..5ff0fa8a4d 100644 --- a/mobile/lib/services/deep_link.service.dart +++ b/mobile/lib/services/deep_link.service.dart @@ -7,10 +7,7 @@ import 'package:immich_mobile/domain/services/memory.service.dart'; import 'package:immich_mobile/domain/services/people.service.dart'; import 'package:immich_mobile/domain/services/remote_album.service.dart'; import 'package:immich_mobile/domain/services/timeline.service.dart'; -import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.page.dart'; -import 'package:immich_mobile/providers/album/current_album.provider.dart'; -import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart'; import 'package:immich_mobile/providers/infrastructure/album.provider.dart'; import 'package:immich_mobile/providers/infrastructure/asset.provider.dart' as beta_asset_provider; import 'package:immich_mobile/providers/infrastructure/memory.provider.dart'; @@ -18,19 +15,9 @@ import 'package:immich_mobile/providers/infrastructure/people.provider.dart'; import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart'; import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/services/album.service.dart'; -import 'package:immich_mobile/services/asset.service.dart'; -import 'package:immich_mobile/services/memory.service.dart'; -import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart'; final deepLinkServiceProvider = Provider( (ref) => DeepLinkService( - ref.watch(memoryServiceProvider), - ref.watch(assetServiceProvider), - ref.watch(albumServiceProvider), - ref.watch(currentAssetProvider.notifier), - ref.watch(currentAlbumProvider.notifier), - // Below is used for beta timeline ref.watch(timelineFactoryProvider), ref.watch(beta_asset_provider.assetServiceProvider), ref.watch(remoteAlbumServiceProvider), @@ -41,14 +28,6 @@ final deepLinkServiceProvider = Provider( ); class DeepLinkService { - /// TODO: Remove this when beta is default - final MemoryService _memoryService; - final AssetService _assetService; - final AlbumService _albumService; - final CurrentAsset _currentAsset; - final CurrentAlbum _currentAlbum; - - /// Used for beta timeline final TimelineFactory _betaTimelineFactory; final beta_asset_service.AssetService _betaAssetService; final RemoteAlbumService _betaRemoteAlbumService; @@ -58,11 +37,6 @@ class DeepLinkService { final UserDto? _currentUser; const DeepLinkService( - this._memoryService, - this._assetService, - this._albumService, - this._currentAsset, - this._currentAlbum, this._betaTimelineFactory, this._betaAssetService, this._betaRemoteAlbumService, @@ -75,7 +49,7 @@ class DeepLinkService { return DeepLink([ // we need something to segue back to if the app was cold started // TODO: use MainTimelineRoute this when beta is default - if (isColdStart) (Store.isBetaTimelineEnabled) ? const TabShellRoute() : const PhotosRoute(), + if (isColdStart) const TabShellRoute(), route, ]); } @@ -138,95 +112,52 @@ class DeepLinkService { } Future _buildMemoryDeepLink(String? memoryId) async { - if (Store.isBetaTimelineEnabled) { - List memories = []; + List memories = []; - if (memoryId == null) { - if (_currentUser == null) { - return null; - } - - memories = await _betaMemoryService.getMemoryLane(_currentUser.id); - } else { - final memory = await _betaMemoryService.get(memoryId); - if (memory != null) { - memories = [memory]; - } - } - - if (memories.isEmpty) { + if (memoryId == null) { + if (_currentUser == null) { return null; } - return DriftMemoryRoute(memories: memories, memoryIndex: 0); + memories = await _betaMemoryService.getMemoryLane(_currentUser.id); } else { - // TODO: Remove this when beta is default - if (memoryId == null) { - return null; + final memory = await _betaMemoryService.get(memoryId); + if (memory != null) { + memories = [memory]; } - final memory = await _memoryService.getMemoryById(memoryId); - - if (memory == null) { - return null; - } - - return MemoryRoute(memories: [memory], memoryIndex: 0); } - } - Future _buildAssetDeepLink(String assetId, WidgetRef ref) async { - if (Store.isBetaTimelineEnabled) { - final asset = await _betaAssetService.getRemoteAsset(assetId); - if (asset == null) { - return null; - } - - AssetViewer.setAsset(ref, asset); - return AssetViewerRoute( - initialIndex: 0, - timelineService: _betaTimelineFactory.fromAssets([asset], TimelineOrigin.deepLink), - ); - } else { - // TODO: Remove this when beta is default - final asset = await _assetService.getAssetByRemoteId(assetId); - if (asset == null) { - return null; - } - - _currentAsset.set(asset); - final renderList = await RenderList.fromAssets([asset], GroupAssetsBy.auto); - - return GalleryViewerRoute(renderList: renderList, initialIndex: 0, heroOffset: 0, showStack: true); - } - } - - Future _buildAlbumDeepLink(String albumId) async { - if (Store.isBetaTimelineEnabled) { - final album = await _betaRemoteAlbumService.get(albumId); - - if (album == null) { - return null; - } - - return RemoteAlbumRoute(album: album); - } else { - // TODO: Remove this when beta is default - final album = await _albumService.getAlbumByRemoteId(albumId); - - if (album == null) { - return null; - } - - _currentAlbum.set(album); - return AlbumViewerRoute(albumId: album.id); - } - } - - Future _buildActivityDeepLink(String albumId) async { - if (Store.isBetaTimelineEnabled == false) { + if (memories.isEmpty) { return null; } + return DriftMemoryRoute(memories: memories, memoryIndex: 0); + } + + Future _buildAssetDeepLink(String assetId, WidgetRef ref) async { + final asset = await _betaAssetService.getRemoteAsset(assetId); + if (asset == null) { + return null; + } + + AssetViewer.setAsset(ref, asset); + return AssetViewerRoute( + initialIndex: 0, + timelineService: _betaTimelineFactory.fromAssets([asset], TimelineOrigin.deepLink), + ); + } + + Future _buildAlbumDeepLink(String albumId) async { + final album = await _betaRemoteAlbumService.get(albumId); + + if (album == null) { + return null; + } + + return RemoteAlbumRoute(album: album); + } + + Future _buildActivityDeepLink(String albumId) async { final album = await _betaRemoteAlbumService.get(albumId); if (album == null || album.isActivityEnabled == false) { @@ -237,10 +168,6 @@ class DeepLinkService { } Future _buildPeopleDeepLink(String personId) async { - if (Store.isBetaTimelineEnabled == false) { - return null; - } - final person = await _betaPeopleService.get(personId); if (person == null) { diff --git a/mobile/lib/services/device.service.dart b/mobile/lib/services/device.service.dart deleted file mode 100644 index 50a0d93b24..0000000000 --- a/mobile/lib/services/device.service.dart +++ /dev/null @@ -1,25 +0,0 @@ -import 'package:flutter_udid/flutter_udid.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/domain/models/store.model.dart'; -import 'package:immich_mobile/entities/store.entity.dart'; - -final deviceServiceProvider = Provider((ref) => const DeviceService()); - -class DeviceService { - const DeviceService(); - - createDeviceId() { - return FlutterUdid.consistentUdid; - } - - /// Returns the device ID from local storage or creates a new one if not found. - /// - /// This method first attempts to retrieve the device ID from the local store using - /// [StoreKey.deviceId]. If no device ID is found (returns null), it generates a - /// new device ID by calling [createDeviceId]. - /// - /// Returns a [String] representing the device's unique identifier. - String getDeviceId() { - return Store.tryGet(StoreKey.deviceId) ?? createDeviceId(); - } -} diff --git a/mobile/lib/services/download.service.dart b/mobile/lib/services/download.service.dart index 8e810ced2a..3f2c36fa7e 100644 --- a/mobile/lib/services/download.service.dart +++ b/mobile/lib/services/download.service.dart @@ -3,14 +3,9 @@ import 'dart:io'; import 'package:background_downloader/background_downloader.dart'; import 'package:flutter/services.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/constants/constants.dart'; -import 'package:immich_mobile/domain/models/store.model.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/models/download/livephotos_medatada.model.dart'; import 'package:immich_mobile/repositories/download.repository.dart'; import 'package:immich_mobile/repositories/file_media.repository.dart'; -import 'package:immich_mobile/services/api.service.dart'; import 'package:logging/logging.dart'; final downloadServiceProvider = Provider( @@ -54,7 +49,7 @@ class DownloadService { final title = task.filename; final relativePath = Platform.isAndroid ? 'DCIM/Immich' : null; try { - final Asset? resultAsset = await _fileMediaRepository.saveImageWithFile( + final resultAsset = await _fileMediaRepository.saveImageWithFile( filePath, title: title, relativePath: relativePath, @@ -76,7 +71,7 @@ class DownloadService { final relativePath = Platform.isAndroid ? 'DCIM/Immich' : null; final file = File(filePath); try { - final Asset? resultAsset = await _fileMediaRepository.saveVideo(file, title: title, relativePath: relativePath); + final resultAsset = await _fileMediaRepository.saveVideo(file, title: title, relativePath: relativePath); return resultAsset != null; } catch (error, stack) { _log.severe("Error saving video", error, stack); @@ -136,62 +131,6 @@ class DownloadService { Future cancelDownload(String id) async { return await FileDownloader().cancelTaskWithId(id); } - - Future> downloadAll(List assets) async { - return await _downloadRepository.downloadAll(assets.expand(_createDownloadTasks).toList()); - } - - Future download(Asset asset) async { - final tasks = _createDownloadTasks(asset); - await _downloadRepository.downloadAll(tasks); - } - - List _createDownloadTasks(Asset asset) { - if (asset.isImage && asset.livePhotoVideoId != null && Platform.isIOS) { - return [ - _buildDownloadTask( - asset.remoteId!, - asset.fileName, - group: kDownloadGroupLivePhoto, - metadata: LivePhotosMetadata(part: LivePhotosPart.image, id: asset.remoteId!).toJson(), - ), - _buildDownloadTask( - asset.livePhotoVideoId!, - asset.fileName.toUpperCase().replaceAll(RegExp(r"\.(JPG|HEIC)$"), '.MOV'), - group: kDownloadGroupLivePhoto, - metadata: LivePhotosMetadata(part: LivePhotosPart.video, id: asset.remoteId!).toJson(), - ), - ]; - } - - if (asset.remoteId == null) { - return []; - } - - return [ - _buildDownloadTask( - asset.remoteId!, - asset.fileName, - group: asset.isImage ? kDownloadGroupImage : kDownloadGroupVideo, - ), - ]; - } - - DownloadTask _buildDownloadTask(String id, String filename, {String? group, String? metadata}) { - final path = r'/assets/{id}/original'.replaceAll('{id}', id); - final serverEndpoint = Store.get(StoreKey.serverEndpoint); - final headers = ApiService.getRequestHeaders(); - - return DownloadTask( - taskId: id, - url: serverEndpoint + path, - headers: headers, - filename: filename, - updates: Updates.statusAndProgress, - group: group ?? '', - metaData: metadata ?? '', - ); - } } TaskRecord _findTaskRecord(List records, String livePhotosId, LivePhotosPart part) { diff --git a/mobile/lib/services/entity.service.dart b/mobile/lib/services/entity.service.dart deleted file mode 100644 index fe7358fce6..0000000000 --- a/mobile/lib/services/entity.service.dart +++ /dev/null @@ -1,44 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/album.entity.dart'; -import 'package:immich_mobile/infrastructure/entities/user.entity.dart'; -import 'package:immich_mobile/infrastructure/repositories/user.repository.dart'; -import 'package:immich_mobile/providers/infrastructure/user.provider.dart'; -import 'package:immich_mobile/repositories/asset.repository.dart'; - -class EntityService { - final AssetRepository _assetRepository; - final IsarUserRepository _isarUserRepository; - const EntityService(this._assetRepository, this._isarUserRepository); - - Future fillAlbumWithDatabaseEntities(Album album) async { - final ownerId = album.ownerId; - if (ownerId != null) { - // replace owner with user from database - final user = await _isarUserRepository.getByUserId(ownerId); - album.owner.value = user == null ? null : User.fromDto(user); - } - final thumbnailAssetId = album.remoteThumbnailAssetId ?? album.thumbnail.value?.remoteId; - if (thumbnailAssetId != null) { - // set thumbnail with asset from database - album.thumbnail.value = await _assetRepository.getByRemoteId(thumbnailAssetId); - } - if (album.remoteUsers.isNotEmpty) { - // replace all users with users from database - final users = await _isarUserRepository.getByUserIds(album.remoteUsers.map((user) => user.id).toList()); - album.sharedUsers.clear(); - album.sharedUsers.addAll(users.nonNulls.map(User.fromDto)); - album.shared = true; - } - if (album.remoteAssets.isNotEmpty) { - // replace all assets with assets from database - final assets = await _assetRepository.getAllByRemoteId(album.remoteAssets.map((asset) => asset.remoteId!)); - album.assets.clear(); - album.assets.addAll(assets); - } - return album; - } -} - -final entityServiceProvider = Provider( - (ref) => EntityService(ref.watch(assetRepositoryProvider), ref.watch(userRepositoryProvider)), -); diff --git a/mobile/lib/services/etag.service.dart b/mobile/lib/services/etag.service.dart deleted file mode 100644 index 00eb83fcea..0000000000 --- a/mobile/lib/services/etag.service.dart +++ /dev/null @@ -1,14 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/repositories/etag.repository.dart'; - -final etagServiceProvider = Provider((ref) => ETagService(ref.watch(etagRepositoryProvider))); - -class ETagService { - final ETagRepository _eTagRepository; - - const ETagService(this._eTagRepository); - - Future clearTable() { - return _eTagRepository.clearTable(); - } -} diff --git a/mobile/lib/services/exif.service.dart b/mobile/lib/services/exif.service.dart deleted file mode 100644 index 57f793b21e..0000000000 --- a/mobile/lib/services/exif.service.dart +++ /dev/null @@ -1,15 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/infrastructure/repositories/exif.repository.dart'; -import 'package:immich_mobile/providers/infrastructure/exif.provider.dart'; - -final exifServiceProvider = Provider((ref) => ExifService(ref.watch(exifRepositoryProvider))); - -class ExifService { - final IsarExifRepository _exifInfoRepository; - - const ExifService(this._exifInfoRepository); - - Future clearTable() { - return _exifInfoRepository.deleteAll(); - } -} diff --git a/mobile/lib/services/hash.service.dart b/mobile/lib/services/hash.service.dart deleted file mode 100644 index 9d1f4e51e8..0000000000 --- a/mobile/lib/services/hash.service.dart +++ /dev/null @@ -1,191 +0,0 @@ -import 'dart:convert'; -import 'dart:io'; - -import 'package:flutter/foundation.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/constants/constants.dart'; -import 'package:immich_mobile/domain/models/device_asset.model.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/infrastructure/repositories/device_asset.repository.dart'; -import 'package:immich_mobile/providers/infrastructure/device_asset.provider.dart'; -import 'package:immich_mobile/services/background.service.dart'; -import 'package:logging/logging.dart'; - -class HashService { - HashService({ - required IsarDeviceAssetRepository deviceAssetRepository, - required BackgroundService backgroundService, - this.batchSizeLimit = kBatchHashSizeLimit, - int? batchFileLimit, - }) : _deviceAssetRepository = deviceAssetRepository, - _backgroundService = backgroundService, - batchFileLimit = batchFileLimit ?? kBatchHashFileLimit; - - final IsarDeviceAssetRepository _deviceAssetRepository; - final BackgroundService _backgroundService; - final int batchSizeLimit; - final int batchFileLimit; - final _log = Logger('HashService'); - - /// Processes a list of local [Asset]s, storing their hash and returning only those - /// that were successfully hashed. Hashes are looked up in a DB table - /// [DeviceAsset] by local id. Only missing entries are newly hashed and added to the DB table. - Future> hashAssets(List assets) async { - assets.sort(Asset.compareByLocalId); - - // Get and sort DB entries - guaranteed to be a subset of assets - final hashesInDB = await _deviceAssetRepository.getByIds(assets.map((a) => a.localId!).toList()); - hashesInDB.sort((a, b) => a.assetId.compareTo(b.assetId)); - - int dbIndex = 0; - int bytesProcessed = 0; - final hashedAssets = []; - final toBeHashed = <_AssetPath>[]; - final toBeDeleted = []; - - for (int assetIndex = 0; assetIndex < assets.length; assetIndex++) { - final asset = assets[assetIndex]; - DeviceAsset? matchingDbEntry; - - if (dbIndex < hashesInDB.length) { - final deviceAsset = hashesInDB[dbIndex]; - if (deviceAsset.assetId == asset.localId) { - matchingDbEntry = deviceAsset; - dbIndex++; - } - } - - if (matchingDbEntry != null && - matchingDbEntry.hash.isNotEmpty && - matchingDbEntry.modifiedTime.isAtSameMomentAs(asset.fileModifiedAt)) { - // Reuse the existing hash - hashedAssets.add(asset.copyWith(checksum: base64.encode(matchingDbEntry.hash))); - continue; - } - - final file = await _tryGetAssetFile(asset); - if (file == null) { - // Can't access file, delete any DB entry - if (matchingDbEntry != null) { - toBeDeleted.add(matchingDbEntry.assetId); - } - continue; - } - - bytesProcessed += await file.length(); - toBeHashed.add(_AssetPath(asset: asset, path: file.path)); - - if (_shouldProcessBatch(toBeHashed.length, bytesProcessed)) { - hashedAssets.addAll(await _processBatch(toBeHashed, toBeDeleted)); - toBeHashed.clear(); - toBeDeleted.clear(); - bytesProcessed = 0; - } - } - assert(dbIndex == hashesInDB.length, "All hashes should've been processed"); - - // Process any remaining files - if (toBeHashed.isNotEmpty) { - hashedAssets.addAll(await _processBatch(toBeHashed, toBeDeleted)); - } - - // Clean up deleted references - if (toBeDeleted.isNotEmpty) { - await _deviceAssetRepository.deleteIds(toBeDeleted); - } - - return hashedAssets; - } - - bool _shouldProcessBatch(int assetCount, int bytesProcessed) => - assetCount >= batchFileLimit || bytesProcessed >= batchSizeLimit; - - Future _tryGetAssetFile(Asset asset) async { - try { - final file = await asset.local!.originFile; - if (file == null) { - _log.warning( - "Failed to get file for asset ${asset.localId ?? ''}, name: ${asset.fileName}, created on: ${asset.fileCreatedAt}, skipping", - ); - return null; - } - return file; - } catch (error, stackTrace) { - _log.warning( - "Error getting file to hash for asset ${asset.localId ?? ''}, name: ${asset.fileName}, created on: ${asset.fileCreatedAt}, skipping", - error, - stackTrace, - ); - return null; - } - } - - /// Processes a batch of files and returns a list of successfully hashed assets after saving - /// them in [DeviceAssetToHash] for future retrieval - Future> _processBatch(List<_AssetPath> toBeHashed, List toBeDeleted) async { - _log.info("Hashing ${toBeHashed.length} files"); - final hashes = await _hashFiles(toBeHashed.map((e) => e.path).toList()); - assert( - hashes.length == toBeHashed.length, - "Number of Hashes returned from platform should be the same as the input", - ); - - final hashedAssets = []; - final toBeAdded = []; - - for (final (index, hash) in hashes.indexed) { - final asset = toBeHashed.elementAtOrNull(index)?.asset; - if (asset != null && hash?.length == 20) { - hashedAssets.add(asset.copyWith(checksum: base64.encode(hash!))); - toBeAdded.add(DeviceAsset(assetId: asset.localId!, hash: hash, modifiedTime: asset.fileModifiedAt)); - } else { - _log.warning("Failed to hash file ${asset?.localId ?? ''}"); - if (asset != null) { - toBeDeleted.add(asset.localId!); - } - } - } - - // Update the DB for future retrieval - await _deviceAssetRepository.transaction(() async { - await _deviceAssetRepository.updateAll(toBeAdded); - await _deviceAssetRepository.deleteIds(toBeDeleted); - }); - - _log.fine("Hashed ${hashedAssets.length}/${toBeHashed.length} assets"); - return hashedAssets; - } - - /// Hashes the given files and returns a list of the same length. - /// Files that could not be hashed will have a `null` value - Future> _hashFiles(List paths) async { - try { - final hashes = await _backgroundService.digestFiles(paths); - if (hashes != null) { - return hashes; - } - _log.severe("Hashing ${paths.length} files failed"); - } catch (e, s) { - _log.severe("Error occurred while hashing assets", e, s); - } - return List.filled(paths.length, null); - } -} - -class _AssetPath { - final Asset asset; - final String path; - - const _AssetPath({required this.asset, required this.path}); - - _AssetPath copyWith({Asset? asset, String? path}) { - return _AssetPath(asset: asset ?? this.asset, path: path ?? this.path); - } -} - -final hashServiceProvider = Provider( - (ref) => HashService( - deviceAssetRepository: ref.watch(deviceAssetRepositoryProvider), - backgroundService: ref.watch(backgroundServiceProvider), - ), -); diff --git a/mobile/lib/services/local_notification.service.dart b/mobile/lib/services/local_notification.service.dart deleted file mode 100644 index bf85f4a9a9..0000000000 --- a/mobile/lib/services/local_notification.service.dart +++ /dev/null @@ -1,118 +0,0 @@ -import 'package:flutter_local_notifications/flutter_local_notifications.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/providers/backup/manual_upload.provider.dart'; -import 'package:immich_mobile/providers/notification_permission.provider.dart'; -import 'package:permission_handler/permission_handler.dart'; -import 'package:immich_mobile/utils/debug_print.dart'; - -final localNotificationService = Provider( - (ref) => LocalNotificationService(ref.watch(notificationPermissionProvider), ref), -); - -class LocalNotificationService { - final FlutterLocalNotificationsPlugin _localNotificationPlugin = FlutterLocalNotificationsPlugin(); - final PermissionStatus _permissionStatus; - final Ref ref; - - LocalNotificationService(this._permissionStatus, this.ref); - - static const manualUploadNotificationID = 4; - static const manualUploadDetailedNotificationID = 5; - static const manualUploadChannelName = 'Manual Asset Upload'; - static const manualUploadChannelID = 'immich/manualUpload'; - static const manualUploadChannelNameDetailed = 'Manual Asset Upload Detailed'; - static const manualUploadDetailedChannelID = 'immich/manualUploadDetailed'; - static const cancelUploadActionID = 'cancel_upload'; - - Future setup() async { - const androidSetting = AndroidInitializationSettings('@drawable/notification_icon'); - const iosSetting = DarwinInitializationSettings(); - - const initSettings = InitializationSettings(android: androidSetting, iOS: iosSetting); - - await _localNotificationPlugin.initialize( - initSettings, - onDidReceiveNotificationResponse: _onDidReceiveForegroundNotificationResponse, - ); - } - - Future _showOrUpdateNotification( - int id, - String title, - String body, - AndroidNotificationDetails androidNotificationDetails, - DarwinNotificationDetails iosNotificationDetails, - ) async { - final notificationDetails = NotificationDetails(android: androidNotificationDetails, iOS: iosNotificationDetails); - - if (_permissionStatus == PermissionStatus.granted) { - await _localNotificationPlugin.show(id, title, body, notificationDetails); - } - } - - Future closeNotification(int id) { - return _localNotificationPlugin.cancel(id); - } - - Future showOrUpdateManualUploadStatus( - String title, - String body, { - bool? isDetailed, - bool? presentBanner, - bool? showActions, - int? maxProgress, - int? progress, - }) { - var notificationlId = manualUploadNotificationID; - var androidChannelID = manualUploadChannelID; - var androidChannelName = manualUploadChannelName; - // Separate Notification for Info/Alerts and Progress - if (isDetailed != null && isDetailed) { - notificationlId = manualUploadDetailedNotificationID; - androidChannelID = manualUploadDetailedChannelID; - androidChannelName = manualUploadChannelNameDetailed; - } - // Progress notification - final androidNotificationDetails = (maxProgress != null && progress != null) - ? AndroidNotificationDetails( - androidChannelID, - androidChannelName, - ticker: title, - showProgress: true, - onlyAlertOnce: true, - maxProgress: maxProgress, - progress: progress, - indeterminate: false, - playSound: false, - priority: Priority.low, - importance: Importance.low, - ongoing: true, - actions: (showActions ?? false) - ? [ - const AndroidNotificationAction(cancelUploadActionID, 'Cancel', showsUserInterface: true), - ] - : null, - ) - // Non-progress notification - : AndroidNotificationDetails(androidChannelID, androidChannelName, playSound: false); - - final iosNotificationDetails = DarwinNotificationDetails( - presentBadge: true, - presentList: true, - presentBanner: presentBanner, - ); - - return _showOrUpdateNotification(notificationlId, title, body, androidNotificationDetails, iosNotificationDetails); - } - - void _onDidReceiveForegroundNotificationResponse(NotificationResponse notificationResponse) { - // Handle notification actions - switch (notificationResponse.actionId) { - case cancelUploadActionID: - { - dPrint(() => "User cancelled manual upload operation"); - ref.read(manualUploadProvider.notifier).cancelBackup(); - } - } - } -} diff --git a/mobile/lib/services/memory.service.dart b/mobile/lib/services/memory.service.dart deleted file mode 100644 index e485bb0957..0000000000 --- a/mobile/lib/services/memory.service.dart +++ /dev/null @@ -1,71 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; -import 'package:immich_mobile/models/memories/memory.model.dart'; -import 'package:immich_mobile/providers/api.provider.dart'; -import 'package:immich_mobile/repositories/asset.repository.dart'; -import 'package:immich_mobile/services/api.service.dart'; -import 'package:logging/logging.dart'; - -final memoryServiceProvider = StateProvider((ref) { - return MemoryService(ref.watch(apiServiceProvider), ref.watch(assetRepositoryProvider)); -}); - -class MemoryService { - final log = Logger("MemoryService"); - - final ApiService _apiService; - final AssetRepository _assetRepository; - - MemoryService(this._apiService, this._assetRepository); - - Future?> getMemoryLane() async { - try { - final now = DateTime.now(); - final data = await _apiService.memoriesApi.searchMemories( - for_: DateTime.utc(now.year, now.month, now.day, 0, 0, 0), - ); - - if (data == null) { - return null; - } - - List memories = []; - - for (final memory in data) { - final dbAssets = await _assetRepository.getAllByRemoteId(memory.assets.map((e) => e.id)); - final yearsAgo = now.year - memory.data.year; - if (dbAssets.isNotEmpty) { - final String title = 'years_ago'.t(args: {'years': yearsAgo.toString()}); - memories.add(Memory(title: title, assets: dbAssets)); - } - } - - return memories.isNotEmpty ? memories : null; - } catch (error, stack) { - log.severe("Cannot get memories", error, stack); - return null; - } - } - - Future getMemoryById(String id) async { - try { - final memoryResponse = await _apiService.memoriesApi.getMemory(id); - - if (memoryResponse == null) { - return null; - } - final dbAssets = await _assetRepository.getAllByRemoteId(memoryResponse.assets.map((e) => e.id)); - if (dbAssets.isEmpty) { - log.warning("No assets found for memory with ID: $id"); - return null; - } - final yearsAgo = DateTime.now().year - memoryResponse.data.year; - final String title = 'years_ago'.t(args: {'years': yearsAgo.toString()}); - - return Memory(title: title, assets: dbAssets); - } catch (error, stack) { - log.severe("Cannot get memory with ID: $id", error, stack); - return null; - } - } -} diff --git a/mobile/lib/services/partner.service.dart b/mobile/lib/services/partner.service.dart deleted file mode 100644 index b8e5ae9a4d..0000000000 --- a/mobile/lib/services/partner.service.dart +++ /dev/null @@ -1,73 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/domain/models/user.model.dart'; -import 'package:immich_mobile/infrastructure/repositories/user.repository.dart'; -import 'package:immich_mobile/providers/infrastructure/user.provider.dart'; -import 'package:immich_mobile/repositories/partner.repository.dart'; -import 'package:immich_mobile/repositories/partner_api.repository.dart'; -import 'package:logging/logging.dart'; - -final partnerServiceProvider = Provider( - (ref) => PartnerService( - ref.watch(partnerApiRepositoryProvider), - ref.watch(userRepositoryProvider), - ref.watch(partnerRepositoryProvider), - ), -); - -class PartnerService { - final PartnerApiRepository _partnerApiRepository; - final PartnerRepository _partnerRepository; - final IsarUserRepository _isarUserRepository; - final Logger _log = Logger("PartnerService"); - - PartnerService(this._partnerApiRepository, this._isarUserRepository, this._partnerRepository); - - Future> getSharedWith() async { - return _partnerRepository.getSharedWith(); - } - - Future> getSharedBy() async { - return _partnerRepository.getSharedBy(); - } - - Stream> watchSharedWith() { - return _partnerRepository.watchSharedWith(); - } - - Stream> watchSharedBy() { - return _partnerRepository.watchSharedBy(); - } - - Future removePartner(UserDto partner) async { - try { - await _partnerApiRepository.delete(partner.id); - await _isarUserRepository.update(partner.copyWith(isPartnerSharedBy: false)); - } catch (e) { - _log.warning("Failed to remove partner ${partner.id}", e); - return false; - } - return true; - } - - Future addPartner(UserDto partner) async { - try { - await _partnerApiRepository.create(partner.id); - await _isarUserRepository.update(partner.copyWith(isPartnerSharedBy: true)); - return true; - } catch (e) { - _log.warning("Failed to add partner ${partner.id}", e); - } - return false; - } - - Future updatePartner(UserDto partner, {required bool inTimeline}) async { - try { - final dto = await _partnerApiRepository.update(partner.id, inTimeline: inTimeline); - await _isarUserRepository.update(partner.copyWith(inTimeline: dto.inTimeline)); - return true; - } catch (e) { - _log.warning("Failed to update partner ${partner.id}", e); - } - return false; - } -} diff --git a/mobile/lib/services/person.service.dart b/mobile/lib/services/person.service.dart index 37b16a8d29..023c62ed78 100644 --- a/mobile/lib/services/person.service.dart +++ b/mobile/lib/services/person.service.dart @@ -1,8 +1,5 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/person.model.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/repositories/asset.repository.dart'; -import 'package:immich_mobile/repositories/asset_api.repository.dart'; import 'package:immich_mobile/repositories/person_api.repository.dart'; import 'package:logging/logging.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; @@ -10,19 +7,12 @@ import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'person.service.g.dart'; @riverpod -PersonService personService(Ref ref) => PersonService( - ref.watch(personApiRepositoryProvider), - ref.watch(assetApiRepositoryProvider), - ref.read(assetRepositoryProvider), -); +PersonService personService(Ref ref) => PersonService(ref.watch(personApiRepositoryProvider)); class PersonService { final Logger _log = Logger("PersonService"); final PersonApiRepository _personApiRepository; - final AssetApiRepository _assetApiRepository; - final AssetRepository _assetRepository; - - PersonService(this._personApiRepository, this._assetApiRepository, this._assetRepository); + PersonService(this._personApiRepository); Future> getAllPeople() async { try { @@ -33,16 +23,6 @@ class PersonService { } } - Future> getPersonAssets(String id) async { - try { - final assets = await _assetApiRepository.search(personIds: [id]); - return await _assetRepository.getAllByRemoteId(assets.map((a) => a.remoteId!)); - } catch (error, stack) { - _log.severe("Error while fetching person assets", error, stack); - } - return []; - } - Future updateName(String id, String name) async { try { return await _personApiRepository.update(id, name: name); diff --git a/mobile/lib/services/person.service.g.dart b/mobile/lib/services/person.service.g.dart index 8c2d46b3bd..4caf1ea434 100644 --- a/mobile/lib/services/person.service.g.dart +++ b/mobile/lib/services/person.service.g.dart @@ -6,7 +6,7 @@ part of 'person.service.dart'; // RiverpodGenerator // ************************************************************************** -String _$personServiceHash() => r'10883bccc6c402205e6785cf9ee6cd7142cd0983'; +String _$personServiceHash() => r'646e38d764c52e63d9fca86992e440f34196d519'; /// See also [personService]. @ProviderFor(personService) diff --git a/mobile/lib/services/search.service.dart b/mobile/lib/services/search.service.dart index f33adf80f9..0330c8485c 100644 --- a/mobile/lib/services/search.service.dart +++ b/mobile/lib/services/search.service.dart @@ -1,31 +1,22 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/string_extensions.dart'; import 'package:immich_mobile/infrastructure/repositories/search_api.repository.dart'; -import 'package:immich_mobile/models/search/search_filter.model.dart'; -import 'package:immich_mobile/models/search/search_result.model.dart'; import 'package:immich_mobile/providers/api.provider.dart'; import 'package:immich_mobile/providers/infrastructure/search.provider.dart'; -import 'package:immich_mobile/repositories/asset.repository.dart'; import 'package:immich_mobile/services/api.service.dart'; +import 'package:immich_mobile/utils/debug_print.dart'; import 'package:logging/logging.dart'; import 'package:openapi/api.dart'; -import 'package:immich_mobile/utils/debug_print.dart'; final searchServiceProvider = Provider( - (ref) => SearchService( - ref.watch(apiServiceProvider), - ref.watch(assetRepositoryProvider), - ref.watch(searchApiRepositoryProvider), - ), + (ref) => SearchService(ref.watch(apiServiceProvider), ref.watch(searchApiRepositoryProvider)), ); class SearchService { final ApiService _apiService; - final AssetRepository _assetRepository; final SearchApiRepository _searchApiRepository; final _log = Logger("SearchService"); - SearchService(this._apiService, this._assetRepository, this._searchApiRepository); + SearchService(this._apiService, this._searchApiRepository); Future?> getSearchSuggestions( SearchSuggestionType type, { @@ -48,24 +39,6 @@ class SearchService { } } - Future search(SearchFilter filter, int page) async { - try { - final response = await _searchApiRepository.search(filter, page); - - if (response == null || response.assets.items.isEmpty) { - return null; - } - - return SearchResult( - assets: await _assetRepository.getAllByRemoteId(response.assets.items.map((e) => e.id)), - nextPage: response.assets.nextPage?.toInt(), - ); - } catch (error, stackTrace) { - _log.severe("Failed to search for assets", error, stackTrace); - } - return null; - } - Future?> getExploreData() async { try { return await _apiService.searchApi.getExploreData(); diff --git a/mobile/lib/services/share.service.dart b/mobile/lib/services/share.service.dart deleted file mode 100644 index a0998d6d3d..0000000000 --- a/mobile/lib/services/share.service.dart +++ /dev/null @@ -1,74 +0,0 @@ -import 'dart:async'; -import 'dart:io'; - -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/extensions/response_extensions.dart'; -import 'package:immich_mobile/providers/api.provider.dart'; -import 'package:immich_mobile/services/api.service.dart'; -import 'package:logging/logging.dart'; -import 'package:path_provider/path_provider.dart'; -import 'package:share_plus/share_plus.dart'; - -final shareServiceProvider = Provider((ref) => ShareService(ref.watch(apiServiceProvider))); - -class ShareService { - final ApiService _apiService; - final Logger _log = Logger("ShareService"); - - ShareService(this._apiService); - - Future shareAsset(Asset asset, BuildContext context) async { - return await shareAssets([asset], context); - } - - Future shareAssets(List assets, BuildContext context) async { - try { - final downloadedXFiles = []; - - for (var asset in assets) { - if (asset.isLocal) { - // Prefer local assets to share - File? f = await asset.local!.originFile; - downloadedXFiles.add(XFile(f!.path)); - } else if (asset.isRemote) { - // Download remote asset otherwise - final tempDir = await getTemporaryDirectory(); - final fileName = asset.fileName; - final tempFile = await File('${tempDir.path}/$fileName').create(); - final res = await _apiService.assetsApi.downloadAssetWithHttpInfo(asset.remoteId!); - - if (res.statusCode != 200) { - _log.severe("Asset download for ${asset.fileName} failed", res.toLoggerString()); - continue; - } - - tempFile.writeAsBytesSync(res.bodyBytes); - downloadedXFiles.add(XFile(tempFile.path)); - } - } - - if (downloadedXFiles.isEmpty) { - _log.warning("No asset can be retrieved for share"); - return false; - } - - if (downloadedXFiles.length != assets.length) { - _log.warning("Partial share - Requested: ${assets.length}, Sharing: ${downloadedXFiles.length}"); - } - - final size = MediaQuery.of(context).size; - unawaited( - Share.shareXFiles( - downloadedXFiles, - sharePositionOrigin: Rect.fromPoints(Offset.zero, Offset(size.width / 3, size.height)), - ), - ); - return true; - } catch (error) { - _log.severe("Share failed", error); - } - return false; - } -} diff --git a/mobile/lib/services/stack.service.dart b/mobile/lib/services/stack.service.dart deleted file mode 100644 index 88189c6bcd..0000000000 --- a/mobile/lib/services/stack.service.dart +++ /dev/null @@ -1,64 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/providers/api.provider.dart'; -import 'package:immich_mobile/repositories/asset.repository.dart'; -import 'package:immich_mobile/services/api.service.dart'; -import 'package:openapi/api.dart'; -import 'package:immich_mobile/utils/debug_print.dart'; - -class StackService { - const StackService(this._api, this._assetRepository); - - final ApiService _api; - final AssetRepository _assetRepository; - - Future getStack(String stackId) async { - try { - return _api.stacksApi.getStack(stackId); - } catch (error) { - dPrint(() => "Error while fetching stack: $error"); - } - return null; - } - - Future createStack(List assetIds) async { - try { - return _api.stacksApi.createStack(StackCreateDto(assetIds: assetIds)); - } catch (error) { - dPrint(() => "Error while creating stack: $error"); - } - return null; - } - - Future updateStack(String stackId, String primaryAssetId) async { - try { - return await _api.stacksApi.updateStack(stackId, StackUpdateDto(primaryAssetId: primaryAssetId)); - } catch (error) { - dPrint(() => "Error while updating stack children: $error"); - } - return null; - } - - Future deleteStack(String stackId, List assets) async { - try { - await _api.stacksApi.deleteStack(stackId); - - // Update local database to trigger rerendering - final List removeAssets = []; - for (final asset in assets) { - asset.stackId = null; - asset.stackPrimaryAssetId = null; - asset.stackCount = 0; - - removeAssets.add(asset); - } - await _assetRepository.transaction(() => _assetRepository.updateAll(removeAssets)); - } catch (error) { - dPrint(() => "Error while deleting stack: $error"); - } - } -} - -final stackServiceProvider = Provider( - (ref) => StackService(ref.watch(apiServiceProvider), ref.watch(assetRepositoryProvider)), -); diff --git a/mobile/lib/services/sync.service.dart b/mobile/lib/services/sync.service.dart deleted file mode 100644 index f5b55f36eb..0000000000 --- a/mobile/lib/services/sync.service.dart +++ /dev/null @@ -1,945 +0,0 @@ -import 'dart:async'; -import 'dart:io'; - -import 'package:collection/collection.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/constants/enums.dart'; -import 'package:immich_mobile/domain/models/user.model.dart'; -import 'package:immich_mobile/domain/services/user.service.dart'; -import 'package:immich_mobile/entities/album.entity.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/entities/etag.entity.dart'; -import 'package:immich_mobile/extensions/collection_extensions.dart'; -import 'package:immich_mobile/infrastructure/repositories/exif.repository.dart'; -import 'package:immich_mobile/infrastructure/repositories/user.repository.dart'; -import 'package:immich_mobile/infrastructure/repositories/user_api.repository.dart'; -import 'package:immich_mobile/providers/app_settings.provider.dart'; -import 'package:immich_mobile/providers/infrastructure/exif.provider.dart'; -import 'package:immich_mobile/providers/infrastructure/user.provider.dart'; -import 'package:immich_mobile/repositories/album.repository.dart'; -import 'package:immich_mobile/repositories/album_api.repository.dart'; -import 'package:immich_mobile/repositories/album_media.repository.dart'; -import 'package:immich_mobile/repositories/asset.repository.dart'; -import 'package:immich_mobile/repositories/etag.repository.dart'; -import 'package:immich_mobile/repositories/local_files_manager.repository.dart'; -import 'package:immich_mobile/repositories/partner.repository.dart'; -import 'package:immich_mobile/repositories/partner_api.repository.dart'; -import 'package:immich_mobile/services/app_settings.service.dart'; -import 'package:immich_mobile/services/entity.service.dart'; -import 'package:immich_mobile/services/hash.service.dart'; -import 'package:immich_mobile/utils/async_mutex.dart'; -import 'package:immich_mobile/utils/datetime_comparison.dart'; -import 'package:immich_mobile/utils/diff.dart'; -import 'package:immich_mobile/utils/hash.dart'; -import 'package:logging/logging.dart'; - -final syncServiceProvider = Provider( - (ref) => SyncService( - ref.watch(hashServiceProvider), - ref.watch(entityServiceProvider), - ref.watch(albumMediaRepositoryProvider), - ref.watch(albumApiRepositoryProvider), - ref.watch(albumRepositoryProvider), - ref.watch(assetRepositoryProvider), - ref.watch(exifRepositoryProvider), - ref.watch(partnerRepositoryProvider), - ref.watch(userRepositoryProvider), - ref.watch(userServiceProvider), - ref.watch(etagRepositoryProvider), - ref.watch(appSettingsServiceProvider), - ref.watch(localFilesManagerRepositoryProvider), - ref.watch(partnerApiRepositoryProvider), - ref.watch(userApiRepositoryProvider), - ), -); - -class SyncService { - final HashService _hashService; - final EntityService _entityService; - final AlbumMediaRepository _albumMediaRepository; - final AlbumApiRepository _albumApiRepository; - final AlbumRepository _albumRepository; - final AssetRepository _assetRepository; - final IsarExifRepository _exifInfoRepository; - final IsarUserRepository _isarUserRepository; - final UserService _userService; - final PartnerRepository _partnerRepository; - final ETagRepository _eTagRepository; - final PartnerApiRepository _partnerApiRepository; - final UserApiRepository _userApiRepository; - final AsyncMutex _lock = AsyncMutex(); - final Logger _log = Logger('SyncService'); - final AppSettingsService _appSettingsService; - final LocalFilesManagerRepository _localFilesManager; - - SyncService( - this._hashService, - this._entityService, - this._albumMediaRepository, - this._albumApiRepository, - this._albumRepository, - this._assetRepository, - this._exifInfoRepository, - this._partnerRepository, - this._isarUserRepository, - this._userService, - this._eTagRepository, - this._appSettingsService, - this._localFilesManager, - this._partnerApiRepository, - this._userApiRepository, - ); - - // public methods: - - /// Syncs users from the server to the local database - /// Returns `true`if there were any changes - Future syncUsersFromServer(List users) => _lock.run(() => _syncUsersFromServer(users)); - - /// Syncs remote assets owned by the logged-in user to the DB - /// Returns `true` if there were any changes - Future syncRemoteAssetsToDb({ - required List users, - required Future<(List? toUpsert, List? toDelete)> Function(List users, DateTime since) - getChangedAssets, - required FutureOr?> Function(UserDto user, DateTime until) loadAssets, - }) => _lock.run( - () async => - await _syncRemoteAssetChanges(users, getChangedAssets) ?? - await _syncRemoteAssetsFull(getUsersFromServer, loadAssets), - ); - - /// Syncs remote albums to the database - /// returns `true` if there were any changes - Future syncRemoteAlbumsToDb(List remote) => _lock.run(() => _syncRemoteAlbumsToDb(remote)); - - /// Syncs all device albums and their assets to the database - /// Returns `true` if there were any changes - Future syncLocalAlbumAssetsToDb(List onDevice, [Set? excludedAssets]) => - _lock.run(() => _syncLocalAlbumAssetsToDb(onDevice, excludedAssets)); - - /// returns all Asset IDs that are not contained in the existing list - List sharedAssetsToRemove(List deleteCandidates, List existing) { - if (deleteCandidates.isEmpty) { - return []; - } - deleteCandidates.sort(Asset.compareById); - existing.sort(Asset.compareById); - return _diffAssets(existing, deleteCandidates, compare: Asset.compareById).$3.map((e) => e.id).toList(); - } - - /// Syncs a new asset to the db. Returns `true` if successful - Future syncNewAssetToDb(Asset newAsset) => _lock.run(() => _syncNewAssetToDb(newAsset)); - - Future removeAllLocalAlbumsAndAssets() => _lock.run(_removeAllLocalAlbumsAndAssets); - - // private methods: - - /// Syncs users from the server to the local database - /// Returns `true`if there were any changes - Future _syncUsersFromServer(List users) async { - users.sortBy((u) => u.id); - final dbUsers = await _isarUserRepository.getAll(sortBy: SortUserBy.id); - final List toDelete = []; - final List toUpsert = []; - final changes = diffSortedListsSync( - users, - dbUsers, - compare: (UserDto a, UserDto b) => a.id.compareTo(b.id), - both: (UserDto a, UserDto b) { - if ((a.updatedAt == null && b.updatedAt != null) || - (a.updatedAt != null && b.updatedAt == null) || - (a.updatedAt != null && b.updatedAt != null && !a.updatedAt!.isAtSameMomentAs(b.updatedAt!)) || - a.isPartnerSharedBy != b.isPartnerSharedBy || - a.isPartnerSharedWith != b.isPartnerSharedWith || - a.inTimeline != b.inTimeline) { - toUpsert.add(a); - return true; - } - return false; - }, - onlyFirst: (UserDto a) => toUpsert.add(a), - onlySecond: (UserDto b) => toDelete.add(b.id), - ); - if (changes) { - await _isarUserRepository.transaction(() async { - await _isarUserRepository.delete(toDelete); - await _isarUserRepository.updateAll(toUpsert); - }); - } - return changes; - } - - /// Syncs a new asset to the db. Returns `true` if successful - Future _syncNewAssetToDb(Asset a) async { - final Asset? inDb = await _assetRepository.getByOwnerIdChecksum(a.ownerId, a.checksum); - if (inDb != null) { - // unify local/remote assets by replacing the - // local-only asset in the DB with a local&remote asset - a = inDb.updatedCopy(a); - } - try { - await _assetRepository.update(a); - } catch (e) { - _log.severe("Failed to put new asset into db", e); - return false; - } - return true; - } - - /// Efficiently syncs assets via changes. Returns `null` when a full sync is required. - Future _syncRemoteAssetChanges( - List users, - Future<(List? toUpsert, List? toDelete)> Function(List users, DateTime since) - getChangedAssets, - ) async { - final currentUser = _userService.getMyUser(); - final DateTime? since = (await _eTagRepository.get(currentUser.id))?.time?.toUtc(); - if (since == null) return null; - final DateTime now = DateTime.now(); - final (toUpsert, toDelete) = await getChangedAssets(users, since); - if (toUpsert == null || toDelete == null) { - await _clearUserAssetsETag(users); - return null; - } - try { - if (toDelete.isNotEmpty) { - await handleRemoteAssetRemoval(toDelete); - } - if (toUpsert.isNotEmpty) { - final (_, updated) = await _linkWithExistingFromDb(toUpsert); - await upsertAssetsWithExif(updated); - } - if (toUpsert.isNotEmpty || toDelete.isNotEmpty) { - await _updateUserAssetsETag(users, now); - return true; - } - return false; - } catch (e) { - _log.severe("Failed to sync remote assets to db", e); - } - return null; - } - - Future _moveToTrashMatchedAssets(Iterable idsToDelete) async { - final List localAssets = await _assetRepository.getAllLocal(); - final List matchedAssets = localAssets.where((asset) => idsToDelete.contains(asset.remoteId)).toList(); - - final mediaUrls = await Future.wait(matchedAssets.map((asset) => asset.local?.getMediaUrl() ?? Future.value(null))); - - await _localFilesManager.moveToTrash(mediaUrls.nonNulls.toList()); - } - - /// Deletes remote-only assets, updates merged assets to be local-only - Future handleRemoteAssetRemoval(List idsToDelete) async { - return _assetRepository.transaction(() async { - await _assetRepository.deleteAllByRemoteId(idsToDelete, state: AssetState.remote); - final merged = await _assetRepository.getAllByRemoteId(idsToDelete, state: AssetState.merged); - if (Platform.isAndroid && _appSettingsService.getSetting(AppSettingsEnum.manageLocalMediaAndroid)) { - await _moveToTrashMatchedAssets(idsToDelete); - } - if (merged.isEmpty) return; - for (final Asset asset in merged) { - asset.remoteId = null; - asset.isTrashed = false; - } - await _assetRepository.updateAll(merged); - }); - } - - Future> _getAllAccessibleUsers() async { - final sharedWith = (await _partnerRepository.getSharedWith()).toSet(); - sharedWith.add(_userService.getMyUser()); - return sharedWith.toList(); - } - - /// Syncs assets by loading and comparing all assets from the server. - Future _syncRemoteAssetsFull( - FutureOr?> Function() refreshUsers, - FutureOr?> Function(UserDto user, DateTime until) loadAssets, - ) async { - final serverUsers = await refreshUsers(); - if (serverUsers == null) { - _log.warning("_syncRemoteAssetsFull aborted because user refresh failed"); - return false; - } - await _syncUsersFromServer(serverUsers); - final List users = await _getAllAccessibleUsers(); - bool changes = false; - for (UserDto u in users) { - changes |= await _syncRemoteAssetsForUser(u, loadAssets); - } - return changes; - } - - Future _syncRemoteAssetsForUser( - UserDto user, - FutureOr?> Function(UserDto user, DateTime until) loadAssets, - ) async { - final DateTime now = DateTime.now().toUtc(); - final List? remote = await loadAssets(user, now); - if (remote == null) { - return false; - } - final List inDb = await _assetRepository.getAll(ownerId: user.id, sortBy: AssetSort.checksum); - assert(inDb.isSorted(Asset.compareByChecksum), "inDb not sorted!"); - - remote.sort(Asset.compareByChecksum); - - // filter our duplicates that might be introduced by the chunked retrieval - remote.uniqueConsecutive(compare: Asset.compareByChecksum); - - final (toAdd, toUpdate, toRemove) = _diffAssets(remote, inDb, remote: true); - if (toAdd.isEmpty && toUpdate.isEmpty && toRemove.isEmpty) { - await _updateUserAssetsETag([user], now); - return false; - } - final idsToDelete = toRemove.map((e) => e.id).toList(); - try { - await _assetRepository.deleteByIds(idsToDelete); - await upsertAssetsWithExif(toAdd + toUpdate); - } catch (e) { - _log.severe("Failed to sync remote assets to db", e); - } - await _updateUserAssetsETag([user], now); - return true; - } - - Future _updateUserAssetsETag(List users, DateTime time) { - final etags = users.map((u) => ETag(id: u.id, time: time)).toList(); - return _eTagRepository.upsertAll(etags); - } - - Future _clearUserAssetsETag(List users) { - final ids = users.map((u) => u.id).toList(); - return _eTagRepository.deleteByIds(ids); - } - - /// Syncs remote albums to the database - /// returns `true` if there were any changes - Future _syncRemoteAlbumsToDb(List remoteAlbums) async { - remoteAlbums.sortBy((e) => e.remoteId!); - - final List dbAlbums = await _albumRepository.getAll(remote: true, sortBy: AlbumSort.remoteId); - - final List toDelete = []; - final List existing = []; - - final bool changes = await diffSortedLists( - remoteAlbums, - dbAlbums, - compare: (remoteAlbum, dbAlbum) => remoteAlbum.remoteId!.compareTo(dbAlbum.remoteId!), - both: (remoteAlbum, dbAlbum) => _syncRemoteAlbum(remoteAlbum, dbAlbum, toDelete, existing), - onlyFirst: (remoteAlbum) => _addAlbumFromServer(remoteAlbum, existing), - onlySecond: (dbAlbum) => _removeAlbumFromDb(dbAlbum, toDelete), - ); - - if (toDelete.isNotEmpty) { - final List idsToRemove = sharedAssetsToRemove(toDelete, existing); - if (idsToRemove.isNotEmpty) { - await _assetRepository.deleteByIds(idsToRemove); - } - } else { - assert(toDelete.isEmpty); - } - return changes; - } - - /// syncs albums from the server to the local database (does not support - /// syncing changes from local back to server) - /// accumulates - Future _syncRemoteAlbum(Album dto, Album album, List deleteCandidates, List existing) async { - if (!_hasRemoteAlbumChanged(dto, album)) { - return false; - } - // loadDetails (/api/album/:id) will not include lastModifiedAssetTimestamp, - // i.e. it will always be null. Save it here. - final originalDto = dto; - dto = await _albumApiRepository.get(dto.remoteId!); - - final assetsInDb = await _assetRepository.getByAlbum(album, sortBy: AssetSort.ownerIdChecksum); - assert(assetsInDb.isSorted(Asset.compareByOwnerChecksum), "inDb unsorted!"); - final List assetsOnRemote = dto.remoteAssets.toList(); - assetsOnRemote.sort(Asset.compareByOwnerChecksum); - final (toAdd, toUpdate, toUnlink) = _diffAssets(assetsOnRemote, assetsInDb, compare: Asset.compareByOwnerChecksum); - - // update shared users - final List sharedUsers = album.sharedUsers.map((u) => u.toDto()).toList(growable: false); - sharedUsers.sort((a, b) => a.id.compareTo(b.id)); - final List users = dto.remoteUsers.map((u) => u.toDto()).toList()..sort((a, b) => a.id.compareTo(b.id)); - final List userIdsToAdd = []; - final List usersToUnlink = []; - diffSortedListsSync( - users, - sharedUsers, - compare: (UserDto a, UserDto b) => a.id.compareTo(b.id), - both: (a, b) => false, - onlyFirst: (UserDto a) => userIdsToAdd.add(a.id), - onlySecond: (UserDto a) => usersToUnlink.add(a), - ); - - // for shared album: put missing album assets into local DB - final (existingInDb, updated) = await _linkWithExistingFromDb(toAdd); - await upsertAssetsWithExif(updated); - final assetsToLink = existingInDb + updated; - final usersToLink = await _isarUserRepository.getByUserIds(userIdsToAdd); - - album.name = dto.name; - album.description = dto.description; - album.shared = dto.shared; - album.createdAt = dto.createdAt; - album.modifiedAt = dto.modifiedAt; - album.startDate = dto.startDate; - album.endDate = dto.endDate; - album.lastModifiedAssetTimestamp = originalDto.lastModifiedAssetTimestamp; - album.shared = dto.shared; - album.activityEnabled = dto.activityEnabled; - album.sortOrder = dto.sortOrder; - - final remoteThumbnailAssetId = dto.remoteThumbnailAssetId; - if (remoteThumbnailAssetId != null && album.thumbnail.value?.remoteId != remoteThumbnailAssetId) { - album.thumbnail.value = await _assetRepository.getByRemoteId(remoteThumbnailAssetId); - } - - // write & commit all changes to DB - try { - await _assetRepository.transaction(() async { - await _assetRepository.updateAll(toUpdate); - await _albumRepository.addUsers(album, usersToLink.nonNulls.toList()); - await _albumRepository.removeUsers(album, usersToUnlink); - await _albumRepository.addAssets(album, assetsToLink); - await _albumRepository.removeAssets(album, toUnlink); - await _albumRepository.recalculateMetadata(album); - await _albumRepository.update(album); - }); - _log.info("Synced changes of remote album ${album.name} to DB"); - } catch (e) { - _log.severe("Failed to sync remote album to database", e); - } - - if (album.shared || dto.shared) { - final userId = (_userService.getMyUser()).id; - final foreign = await _assetRepository.getByAlbum(album, notOwnedBy: [userId]); - existing.addAll(foreign); - - // delete assets in DB unless they belong to this user or part of some other shared album - final isarUserId = fastHash(userId); - deleteCandidates.addAll(toUnlink.where((a) => a.ownerId != isarUserId)); - } - - return true; - } - - /// Adds a remote album to the database while making sure to add any foreign - /// (shared) assets to the database beforehand - /// accumulates assets already existing in the database - Future _addAlbumFromServer(Album album, List existing) async { - if (album.remoteAssetCount != album.remoteAssets.length) { - album = await _albumApiRepository.get(album.remoteId!); - } - if (album.remoteAssetCount == album.remoteAssets.length) { - // in case an album contains assets not yet present in local DB: - // put missing album assets into local DB - final (existingInDb, updated) = await _linkWithExistingFromDb(album.remoteAssets.toList()); - existing.addAll(existingInDb); - await upsertAssetsWithExif(updated); - - await _entityService.fillAlbumWithDatabaseEntities(album); - await _albumRepository.create(album); - } else { - _log.warning( - "Failed to add album from server: assetCount ${album.remoteAssetCount} != " - "asset array length ${album.remoteAssets.length} for album ${album.name}", - ); - } - } - - /// Accumulates all suitable album assets to the `deleteCandidates` and - /// removes the album from the database. - Future _removeAlbumFromDb(Album album, List deleteCandidates) async { - if (album.isLocal) { - _log.info("Removing local album $album from DB"); - // delete assets in DB unless they are remote or part of some other album - deleteCandidates.addAll(await _assetRepository.getByAlbum(album, state: AssetState.local)); - } else if (album.shared) { - // delete assets in DB unless they belong to this user or are part of some other shared album or belong to a partner - final userIds = (await _getAllAccessibleUsers()).map((user) => user.id); - final orphanedAssets = await _assetRepository.getByAlbum(album, notOwnedBy: userIds); - deleteCandidates.addAll(orphanedAssets); - } - try { - await _albumRepository.delete(album.id); - _log.info("Removed local album $album from DB"); - } catch (e) { - _log.severe("Failed to remove local album $album from DB", e); - } - } - - /// Syncs all device albums and their assets to the database - /// Returns `true` if there were any changes - Future _syncLocalAlbumAssetsToDb(List onDevice, [Set? excludedAssets]) async { - onDevice.sort((a, b) => a.localId!.compareTo(b.localId!)); - final inDb = await _albumRepository.getAll(remote: false, sortBy: AlbumSort.localId); - final List deleteCandidates = []; - final List existing = []; - final bool anyChanges = await diffSortedLists( - onDevice, - inDb, - compare: (Album a, Album b) => a.localId!.compareTo(b.localId!), - both: (Album a, Album b) => _syncAlbumInDbAndOnDevice(a, b, deleteCandidates, existing, excludedAssets), - onlyFirst: (Album a) => _addAlbumFromDevice(a, existing, excludedAssets), - onlySecond: (Album a) => _removeAlbumFromDb(a, deleteCandidates), - ); - _log.fine("Syncing all local albums almost done. Collected ${deleteCandidates.length} asset candidates to delete"); - final (toDelete, toUpdate) = _handleAssetRemoval(deleteCandidates, existing, remote: false); - _log.fine("${toDelete.length} assets to delete, ${toUpdate.length} to update"); - if (toDelete.isNotEmpty || toUpdate.isNotEmpty) { - await _assetRepository.transaction(() async { - await _assetRepository.deleteByIds(toDelete); - await _assetRepository.updateAll(toUpdate); - }); - _log.info("Removed ${toDelete.length} and updated ${toUpdate.length} local assets from DB"); - } - return anyChanges; - } - - /// Syncs the device album to the album in the database - /// returns `true` if there were any changes - /// Accumulates asset candidates to delete and those already existing in DB - Future _syncAlbumInDbAndOnDevice( - Album deviceAlbum, - Album dbAlbum, - List deleteCandidates, - List existing, [ - Set? excludedAssets, - bool forceRefresh = false, - ]) async { - _log.info("Syncing a local album to DB: ${deviceAlbum.name}"); - if (!forceRefresh && !await _hasAlbumChangeOnDevice(deviceAlbum, dbAlbum)) { - _log.info("Local album ${deviceAlbum.name} has not changed. Skipping sync."); - return false; - } - _log.info("Local album ${deviceAlbum.name} has changed. Syncing..."); - if (!forceRefresh && excludedAssets == null && await _syncDeviceAlbumFast(deviceAlbum, dbAlbum)) { - _log.info("Fast synced local album ${deviceAlbum.name} to DB"); - return true; - } - // general case, e.g. some assets have been deleted or there are excluded albums on iOS - final inDb = await _assetRepository.getByAlbum( - dbAlbum, - ownerId: (_userService.getMyUser()).id, - sortBy: AssetSort.checksum, - ); - - assert(inDb.isSorted(Asset.compareByChecksum), "inDb not sorted!"); - final int assetCountOnDevice = await _albumMediaRepository.getAssetCount(deviceAlbum.localId!); - final List onDevice = await _getHashedAssets(deviceAlbum, excludedAssets: excludedAssets); - _removeDuplicates(onDevice); - // _removeDuplicates sorts `onDevice` by checksum - final (toAdd, toUpdate, toDelete) = _diffAssets(onDevice, inDb); - if (toAdd.isEmpty && - toUpdate.isEmpty && - toDelete.isEmpty && - dbAlbum.name == deviceAlbum.name && - dbAlbum.description == deviceAlbum.description && - dbAlbum.modifiedAt.isAtSameMomentAs(deviceAlbum.modifiedAt)) { - // changes only affeted excluded albums - _log.info("Only excluded assets in local album ${deviceAlbum.name} changed. Stopping sync."); - if (assetCountOnDevice != (await _eTagRepository.getById(deviceAlbum.eTagKeyAssetCount))?.assetCount) { - await _eTagRepository.upsertAll([ETag(id: deviceAlbum.eTagKeyAssetCount, assetCount: assetCountOnDevice)]); - } - return false; - } - _log.info( - "Syncing local album ${deviceAlbum.name}. ${toAdd.length} assets to add, ${toUpdate.length} to update, ${toDelete.length} to delete", - ); - final (existingInDb, updated) = await _linkWithExistingFromDb(toAdd); - _log.info( - "Linking assets to add with existing from db. ${existingInDb.length} existing, ${updated.length} to update", - ); - deleteCandidates.addAll(toDelete); - existing.addAll(existingInDb); - dbAlbum.name = deviceAlbum.name; - dbAlbum.description = deviceAlbum.description; - dbAlbum.modifiedAt = deviceAlbum.modifiedAt; - if (dbAlbum.thumbnail.value != null && toDelete.contains(dbAlbum.thumbnail.value)) { - dbAlbum.thumbnail.value = null; - } - try { - await _assetRepository.transaction(() async { - await _assetRepository.updateAll(updated + toUpdate); - await _albumRepository.addAssets(dbAlbum, existingInDb + updated); - await _albumRepository.removeAssets(dbAlbum, toDelete); - await _albumRepository.recalculateMetadata(dbAlbum); - await _albumRepository.update(dbAlbum); - await _eTagRepository.upsertAll([ETag(id: deviceAlbum.eTagKeyAssetCount, assetCount: assetCountOnDevice)]); - }); - _log.info("Synced changes of local album ${deviceAlbum.name} to DB"); - } catch (e) { - _log.severe("Failed to update synced album ${deviceAlbum.name} in DB", e); - } - - return true; - } - - /// fast path for common case: only new assets were added to device album - /// returns `true` if successful, else `false` - Future _syncDeviceAlbumFast(Album deviceAlbum, Album dbAlbum) async { - if (!deviceAlbum.modifiedAt.isAfter(dbAlbum.modifiedAt)) { - _log.info("Local album ${deviceAlbum.name} has not changed. Skipping sync."); - return false; - } - final int totalOnDevice = await _albumMediaRepository.getAssetCount(deviceAlbum.localId!); - final int lastKnownTotal = (await _eTagRepository.getById(deviceAlbum.eTagKeyAssetCount))?.assetCount ?? 0; - if (totalOnDevice <= lastKnownTotal) { - _log.info("Local album ${deviceAlbum.name} totalOnDevice is less than lastKnownTotal. Skipping sync."); - return false; - } - final List newAssets = await _getHashedAssets( - deviceAlbum, - modifiedFrom: dbAlbum.modifiedAt.add(const Duration(seconds: 1)), - modifiedUntil: deviceAlbum.modifiedAt, - ); - - if (totalOnDevice != lastKnownTotal + newAssets.length) { - _log.info( - "Local album ${deviceAlbum.name} totalOnDevice is not equal to lastKnownTotal + newAssets.length. Skipping sync.", - ); - return false; - } - dbAlbum.modifiedAt = deviceAlbum.modifiedAt; - _removeDuplicates(newAssets); - final (existingInDb, updated) = await _linkWithExistingFromDb(newAssets); - try { - await _assetRepository.transaction(() async { - await _assetRepository.updateAll(updated); - await _albumRepository.addAssets(dbAlbum, existingInDb + updated); - await _albumRepository.recalculateMetadata(dbAlbum); - await _albumRepository.update(dbAlbum); - await _eTagRepository.upsertAll([ETag(id: deviceAlbum.eTagKeyAssetCount, assetCount: totalOnDevice)]); - }); - _log.info("Fast synced local album ${deviceAlbum.name} to DB"); - } catch (e) { - _log.severe("Failed to fast sync local album ${deviceAlbum.name} to DB", e); - return false; - } - - return true; - } - - /// Adds a new album from the device to the database and Accumulates all - /// assets already existing in the database to the list of `existing` assets - Future _addAlbumFromDevice(Album album, List existing, [Set? excludedAssets]) async { - _log.info("Adding a new local album to DB: ${album.name}"); - final assets = await _getHashedAssets(album, excludedAssets: excludedAssets); - _removeDuplicates(assets); - final (existingInDb, updated) = await _linkWithExistingFromDb(assets); - _log.info("${existingInDb.length} assets already existed in DB, to upsert ${updated.length}"); - await upsertAssetsWithExif(updated); - existing.addAll(existingInDb); - album.assets.addAll(existingInDb); - album.assets.addAll(updated); - final thumb = existingInDb.firstOrNull ?? updated.firstOrNull; - album.thumbnail.value = thumb; - try { - await _albumRepository.create(album); - final int assetCount = await _albumMediaRepository.getAssetCount(album.localId!); - await _eTagRepository.upsertAll([ETag(id: album.eTagKeyAssetCount, assetCount: assetCount)]); - _log.info("Added a new local album to DB: ${album.name}"); - } catch (e) { - _log.severe("Failed to add new local album ${album.name} to DB", e); - } - } - - /// Returns a tuple (existing, updated) - Future<(List existing, List updated)> _linkWithExistingFromDb(List assets) async { - if (assets.isEmpty) return ([].cast(), [].cast()); - - final List inDb = await _assetRepository.getAllByOwnerIdChecksum( - assets.map((a) => a.ownerId).toInt64List(), - assets.map((a) => a.checksum).toList(growable: false), - ); - assert(inDb.length == assets.length); - final List existing = [], toUpsert = []; - for (int i = 0; i < assets.length; i++) { - final Asset? b = inDb[i]; - if (b == null) { - toUpsert.add(assets[i]); - continue; - } - if (b.canUpdate(assets[i])) { - final updated = b.updatedCopy(assets[i]); - assert(updated.isInDb); - toUpsert.add(updated); - } else { - existing.add(b); - } - } - assert(existing.length + toUpsert.length == assets.length); - return (existing, toUpsert); - } - - Future _toggleTrashStatusForAssets(List assetsList) async { - final trashMediaUrls = []; - - for (final asset in assetsList) { - if (asset.isTrashed) { - final mediaUrl = await asset.local?.getMediaUrl(); - if (mediaUrl == null) { - _log.warning("Failed to get media URL for asset ${asset.name} while moving to trash"); - continue; - } - trashMediaUrls.add(mediaUrl); - } else { - await _localFilesManager.restoreFromTrash(asset.fileName, asset.type.index); - } - } - - if (trashMediaUrls.isNotEmpty) { - await _localFilesManager.moveToTrash(trashMediaUrls); - } - } - - /// Inserts or updates the assets in the database with their ExifInfo (if any) - Future upsertAssetsWithExif(List assets) async { - if (assets.isEmpty) return; - - if (Platform.isAndroid && _appSettingsService.getSetting(AppSettingsEnum.manageLocalMediaAndroid)) { - await _toggleTrashStatusForAssets(assets); - } - - try { - await _assetRepository.transaction(() async { - await _assetRepository.updateAll(assets); - for (final Asset added in assets) { - added.exifInfo = added.exifInfo?.copyWith(assetId: added.id); - } - final exifInfos = assets.map((e) => e.exifInfo).nonNulls.toList(); - await _exifInfoRepository.updateAll(exifInfos); - }); - _log.info("Upserted ${assets.length} assets into the DB"); - } catch (e) { - _log.severe("Failed to upsert ${assets.length} assets into the DB", e); - // give details on the errors - assets.sort(Asset.compareByOwnerChecksum); - final inDb = await _assetRepository.getAllByOwnerIdChecksum( - assets.map((e) => e.ownerId).toInt64List(), - assets.map((e) => e.checksum).toList(growable: false), - ); - for (int i = 0; i < assets.length; i++) { - final Asset a = assets[i]; - final Asset? b = inDb[i]; - if (b == null) { - if (!a.isInDb) { - _log.warning("Trying to update an asset that does not exist in DB:\n$a"); - } - } else if (a.id != b.id) { - _log.warning("Trying to insert another asset with the same checksum+owner. In DB:\n$b\nTo insert:\n$a"); - } - } - for (int i = 1; i < assets.length; i++) { - if (Asset.compareByOwnerChecksum(assets[i - 1], assets[i]) == 0) { - _log.warning("Trying to insert duplicate assets:\n${assets[i - 1]}\n${assets[i]}"); - } - } - } - } - - /// Returns all assets that were successfully hashed - Future> _getHashedAssets( - Album album, { - int start = 0, - int end = 0x7fffffffffffffff, - DateTime? modifiedFrom, - DateTime? modifiedUntil, - Set? excludedAssets, - }) async { - final entities = await _albumMediaRepository.getAssets( - album.localId!, - start: start, - end: end, - modifiedFrom: modifiedFrom, - modifiedUntil: modifiedUntil, - ); - final filtered = excludedAssets == null - ? entities - : entities.where((e) => !excludedAssets.contains(e.localId!)).toList(); - return _hashService.hashAssets(filtered); - } - - List _removeDuplicates(List assets) { - final int before = assets.length; - assets.sort(Asset.compareByOwnerChecksumCreatedModified); - assets.uniqueConsecutive(compare: Asset.compareByOwnerChecksum, onDuplicate: (a, b) => {}); - final int duplicates = before - assets.length; - if (duplicates > 0) { - _log.warning("Ignored $duplicates duplicate assets on device"); - } - return assets; - } - - /// returns `true` if the albums differ on the surface - Future _hasAlbumChangeOnDevice(Album deviceAlbum, Album dbAlbum) async { - return deviceAlbum.name != dbAlbum.name || - deviceAlbum.description != dbAlbum.description || - !deviceAlbum.modifiedAt.isAtSameMomentAs(dbAlbum.modifiedAt) || - await _albumMediaRepository.getAssetCount(deviceAlbum.localId!) != - (await _eTagRepository.getById(deviceAlbum.eTagKeyAssetCount))?.assetCount; - } - - Future _removeAllLocalAlbumsAndAssets() async { - try { - final assets = await _assetRepository.getAllLocal(); - final (toDelete, toUpdate) = _handleAssetRemoval(assets, [], remote: false); - await _assetRepository.transaction(() async { - await _assetRepository.deleteByIds(toDelete); - await _assetRepository.updateAll(toUpdate); - await _albumRepository.deleteAllLocal(); - }); - return true; - } catch (e) { - _log.severe("Failed to remove all local albums and assets", e); - return false; - } - } - - Future?> getUsersFromServer() async { - List? users; - try { - users = await _userApiRepository.getAll(); - } catch (e) { - _log.warning("Failed to fetch users", e); - users = null; - } - final List sharedBy = await _partnerApiRepository.getAll(Direction.sharedByMe); - final List sharedWith = await _partnerApiRepository.getAll(Direction.sharedWithMe); - - if (users == null) { - _log.warning("Failed to refresh users"); - return null; - } - - users.sortBy((u) => u.id); - sharedBy.sortBy((u) => u.id); - sharedWith.sortBy((u) => u.id); - - final updatedSharedBy = []; - - diffSortedListsSync( - users, - sharedBy, - compare: (UserDto a, UserDto b) => a.id.compareTo(b.id), - both: (UserDto a, UserDto b) { - updatedSharedBy.add(a.copyWith(isPartnerSharedBy: true)); - return true; - }, - onlyFirst: (UserDto a) => updatedSharedBy.add(a), - onlySecond: (UserDto b) => updatedSharedBy.add(b), - ); - - final updatedSharedWith = []; - - diffSortedListsSync( - updatedSharedBy, - sharedWith, - compare: (UserDto a, UserDto b) => a.id.compareTo(b.id), - both: (UserDto a, UserDto b) { - updatedSharedWith.add(a.copyWith(inTimeline: b.inTimeline, isPartnerSharedWith: true)); - return true; - }, - onlyFirst: (UserDto a) => updatedSharedWith.add(a), - onlySecond: (UserDto b) => updatedSharedWith.add(b), - ); - - return updatedSharedWith; - } -} - -/// Returns a triple(toAdd, toUpdate, toRemove) -(List toAdd, List toUpdate, List toRemove) _diffAssets( - List assets, - List inDb, { - bool? remote, - int Function(Asset, Asset) compare = Asset.compareByChecksum, -}) { - // fast paths for trivial cases: reduces memory usage during initial sync etc. - if (assets.isEmpty && inDb.isEmpty) { - return const ([], [], []); - } else if (assets.isEmpty && remote == null) { - // remove all from database - return (const [], const [], inDb); - } else if (inDb.isEmpty) { - // add all assets - return (assets, const [], const []); - } - - final List toAdd = []; - final List toUpdate = []; - final List toRemove = []; - diffSortedListsSync( - inDb, - assets, - compare: compare, - both: (Asset a, Asset b) { - if (a.canUpdate(b)) { - toUpdate.add(a.updatedCopy(b)); - return true; - } - return false; - }, - onlyFirst: (Asset a) { - if (remote == true && a.isLocal) { - if (a.remoteId != null) { - a.remoteId = null; - toUpdate.add(a); - } - } else if (remote == false && a.isRemote) { - if (a.isLocal) { - a.localId = null; - toUpdate.add(a); - } - } else { - toRemove.add(a); - } - }, - onlySecond: (Asset b) => toAdd.add(b), - ); - return (toAdd, toUpdate, toRemove); -} - -/// returns a tuple (toDelete toUpdate) when assets are to be deleted -(List toDelete, List toUpdate) _handleAssetRemoval( - List deleteCandidates, - List existing, { - bool? remote, -}) { - if (deleteCandidates.isEmpty) { - return const ([], []); - } - deleteCandidates.sort(Asset.compareById); - deleteCandidates.uniqueConsecutive(compare: Asset.compareById); - existing.sort(Asset.compareById); - existing.uniqueConsecutive(compare: Asset.compareById); - final (tooAdd, toUpdate, toRemove) = _diffAssets( - existing, - deleteCandidates, - compare: Asset.compareById, - remote: remote, - ); - assert(tooAdd.isEmpty, "toAdd should be empty in _handleAssetRemoval"); - return (toRemove.map((e) => e.id).toList(), toUpdate); -} - -/// returns `true` if the albums differ on the surface -bool _hasRemoteAlbumChanged(Album remoteAlbum, Album dbAlbum) { - return remoteAlbum.remoteAssetCount != dbAlbum.assetCount || - remoteAlbum.name != dbAlbum.name || - remoteAlbum.description != dbAlbum.description || - remoteAlbum.remoteThumbnailAssetId != dbAlbum.thumbnail.value?.remoteId || - remoteAlbum.shared != dbAlbum.shared || - remoteAlbum.remoteUsers.length != dbAlbum.sharedUsers.length || - !remoteAlbum.modifiedAt.isAtSameMomentAs(dbAlbum.modifiedAt) || - !isAtSameMomentAs(remoteAlbum.startDate, dbAlbum.startDate) || - !isAtSameMomentAs(remoteAlbum.endDate, dbAlbum.endDate) || - !isAtSameMomentAs(remoteAlbum.lastModifiedAssetTimestamp, dbAlbum.lastModifiedAssetTimestamp); -} diff --git a/mobile/lib/services/timeline.service.dart b/mobile/lib/services/timeline.service.dart deleted file mode 100644 index eaff1027d8..0000000000 --- a/mobile/lib/services/timeline.service.dart +++ /dev/null @@ -1,98 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/domain/services/user.service.dart'; -import 'package:immich_mobile/entities/album.entity.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/providers/app_settings.provider.dart'; -import 'package:immich_mobile/providers/infrastructure/user.provider.dart'; -import 'package:immich_mobile/repositories/timeline.repository.dart'; -import 'package:immich_mobile/services/app_settings.service.dart'; -import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart'; - -final timelineServiceProvider = Provider((ref) { - return TimelineService( - ref.watch(timelineRepositoryProvider), - ref.watch(appSettingsServiceProvider), - ref.watch(userServiceProvider), - ); -}); - -class TimelineService { - final TimelineRepository _timelineRepository; - final AppSettingsService _appSettingsService; - final UserService _userService; - - const TimelineService(this._timelineRepository, this._appSettingsService, this._userService); - - Future> getTimelineUserIds() async { - final me = _userService.getMyUser(); - return _timelineRepository.getTimelineUserIds(me.id); - } - - Stream> watchTimelineUserIds() async* { - final me = _userService.getMyUser(); - yield* _timelineRepository.watchTimelineUsers(me.id); - } - - Stream watchHomeTimeline(String userId) { - return _timelineRepository.watchHomeTimeline(userId, _getGroupByOption()); - } - - Stream watchMultiUsersTimeline(List userIds) { - return _timelineRepository.watchMultiUsersTimeline(userIds, _getGroupByOption()); - } - - Stream watchArchiveTimeline() async* { - final user = _userService.getMyUser(); - - yield* _timelineRepository.watchArchiveTimeline(user.id); - } - - Stream watchFavoriteTimeline() async* { - final user = _userService.getMyUser(); - - yield* _timelineRepository.watchFavoriteTimeline(user.id); - } - - Stream watchAlbumTimeline(Album album) async* { - yield* _timelineRepository.watchAlbumTimeline(album, _getGroupByOption()); - } - - Stream watchTrashTimeline() async* { - final user = _userService.getMyUser(); - - yield* _timelineRepository.watchTrashTimeline(user.id); - } - - Stream watchAllVideosTimeline() { - final user = _userService.getMyUser(); - - return _timelineRepository.watchAllVideosTimeline(user.id); - } - - Future getTimelineFromAssets(List assets, GroupAssetsBy? groupBy) { - GroupAssetsBy groupOption = GroupAssetsBy.none; - if (groupBy == null) { - groupOption = _getGroupByOption(); - } else { - groupOption = groupBy; - } - - return _timelineRepository.getTimelineFromAssets(assets, groupOption); - } - - Stream watchAssetSelectionTimeline() async* { - final user = _userService.getMyUser(); - - yield* _timelineRepository.watchAssetSelectionTimeline(user.id); - } - - GroupAssetsBy _getGroupByOption() { - return GroupAssetsBy.values[_appSettingsService.getSetting(AppSettingsEnum.groupAssetsBy)]; - } - - Stream watchLockedTimelineProvider() async* { - final user = _userService.getMyUser(); - - yield* _timelineRepository.watchLockedTimeline(user.id, _getGroupByOption()); - } -} diff --git a/mobile/lib/services/trash.service.dart b/mobile/lib/services/trash.service.dart deleted file mode 100644 index 2c51a68c59..0000000000 --- a/mobile/lib/services/trash.service.dart +++ /dev/null @@ -1,75 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/domain/services/user.service.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/providers/api.provider.dart'; -import 'package:immich_mobile/providers/infrastructure/user.provider.dart'; -import 'package:immich_mobile/repositories/asset.repository.dart'; -import 'package:immich_mobile/services/api.service.dart'; -import 'package:openapi/api.dart'; - -final trashServiceProvider = Provider((ref) { - return TrashService( - ref.watch(apiServiceProvider), - ref.watch(assetRepositoryProvider), - ref.watch(userServiceProvider), - ); -}); - -class TrashService { - final ApiService _apiService; - final AssetRepository _assetRepository; - final UserService _userService; - - const TrashService(this._apiService, this._assetRepository, this._userService); - - Future restoreAssets(Iterable assetList) async { - final remoteAssets = assetList.where((a) => a.isRemote); - await _apiService.trashApi.restoreAssets(BulkIdsDto(ids: remoteAssets.map((e) => e.remoteId!).toList())); - - final updatedAssets = remoteAssets.map((asset) { - asset.isTrashed = false; - return asset; - }).toList(); - - await _assetRepository.updateAll(updatedAssets); - } - - Future emptyTrash() async { - final user = _userService.getMyUser(); - - await _apiService.trashApi.emptyTrash(); - - final trashedAssets = await _assetRepository.getTrashAssets(user.id); - final ids = trashedAssets.map((e) => e.remoteId!).toList(); - - await _assetRepository.transaction(() async { - await _assetRepository.deleteAllByRemoteId(ids, state: AssetState.remote); - - final merged = await _assetRepository.getAllByRemoteId(ids, state: AssetState.merged); - if (merged.isEmpty) { - return; - } - - for (final Asset asset in merged) { - asset.remoteId = null; - asset.isTrashed = false; - } - - await _assetRepository.updateAll(merged); - }); - } - - Future restoreTrash() async { - final user = _userService.getMyUser(); - - await _apiService.trashApi.restoreTrash(); - - final trashedAssets = await _assetRepository.getTrashAssets(user.id); - final updatedAssets = trashedAssets.map((asset) { - asset.isTrashed = false; - return asset; - }).toList(); - - await _assetRepository.updateAll(updatedAssets); - } -} diff --git a/mobile/lib/utils/backup_progress.dart b/mobile/lib/utils/backup_progress.dart deleted file mode 100644 index 36050f5e20..0000000000 --- a/mobile/lib/utils/backup_progress.dart +++ /dev/null @@ -1,83 +0,0 @@ -import 'dart:async'; -import 'dart:developer'; - -import 'package:easy_localization/easy_localization.dart'; - -final NumberFormat numberFormat = NumberFormat("###0.##"); - -String formatAssetBackupProgress(int uploadedAssets, int assetsToUpload) { - final int percent = (uploadedAssets * 100) ~/ assetsToUpload; - return "$percent% ($uploadedAssets/$assetsToUpload)"; -} - -/// prints progress in useful (kilo/mega/giga)bytes -String humanReadableFileBytesProgress(int bytes, int bytesTotal) { - String unit = "KB"; - - if (bytesTotal >= 0x40000000) { - unit = "GB"; - bytes >>= 20; - bytesTotal >>= 20; - } else if (bytesTotal >= 0x100000) { - unit = "MB"; - bytes >>= 10; - bytesTotal >>= 10; - } else if (bytesTotal < 0x400) { - return "${(bytes).toStringAsFixed(2)} B / ${(bytesTotal).toStringAsFixed(2)} B"; - } - - return "${(bytes / 1024.0).toStringAsFixed(2)} $unit / ${(bytesTotal / 1024.0).toStringAsFixed(2)} $unit"; -} - -/// prints percentage and absolute progress in useful (kilo/mega/giga)bytes -String humanReadableBytesProgress(int bytes, int bytesTotal) { - String unit = "KB"; // Kilobyte - if (bytesTotal >= 0x40000000) { - unit = "GB"; // Gigabyte - bytes >>= 20; - bytesTotal >>= 20; - } else if (bytesTotal >= 0x100000) { - unit = "MB"; // Megabyte - bytes >>= 10; - bytesTotal >>= 10; - } else if (bytesTotal < 0x400) { - return "$bytes / $bytesTotal B"; - } - final int percent = (bytes * 100) ~/ bytesTotal; - final String done = numberFormat.format(bytes / 1024.0); - final String total = numberFormat.format(bytesTotal / 1024.0); - return "$percent% ($done/$total$unit)"; -} - -class ThrottleProgressUpdate { - ThrottleProgressUpdate(this._fun, Duration interval) : _interval = interval.inMicroseconds; - final void Function(String?, int, int) _fun; - final int _interval; - int _invokedAt = 0; - Timer? _timer; - - String? title; - int progress = 0; - int total = 0; - - void call({final String? title, final int progress = 0, final int total = 0}) { - final time = Timeline.now; - this.title = title ?? this.title; - this.progress = progress; - this.total = total; - if (time > _invokedAt + _interval) { - _timer?.cancel(); - _onTimeElapsed(); - } else { - _timer ??= Timer(Duration(microseconds: _interval), _onTimeElapsed); - } - } - - void _onTimeElapsed() { - _invokedAt = Timeline.now; - _fun(title, progress, total); - _timer = null; - // clear title to not send/overwrite it next time if unchanged - title = null; - } -} diff --git a/mobile/lib/utils/bootstrap.dart b/mobile/lib/utils/bootstrap.dart index d63a92ba37..e79b06f53b 100644 --- a/mobile/lib/utils/bootstrap.dart +++ b/mobile/lib/utils/bootstrap.dart @@ -1,30 +1,14 @@ -import 'dart:io'; - import 'package:background_downloader/background_downloader.dart'; -import 'package:flutter/foundation.dart'; import 'package:immich_mobile/constants/constants.dart'; -import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/domain/services/log.service.dart'; import 'package:immich_mobile/domain/services/store.service.dart'; -import 'package:immich_mobile/entities/album.entity.dart'; -import 'package:immich_mobile/entities/android_device_asset.entity.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/entities/backup_album.entity.dart'; -import 'package:immich_mobile/entities/duplicated_asset.entity.dart'; -import 'package:immich_mobile/entities/etag.entity.dart'; -import 'package:immich_mobile/entities/ios_device_asset.entity.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart'; -import 'package:immich_mobile/infrastructure/entities/device_asset.entity.dart'; -import 'package:immich_mobile/infrastructure/entities/exif.entity.dart'; -import 'package:immich_mobile/infrastructure/entities/store.entity.dart'; -import 'package:immich_mobile/infrastructure/entities/user.entity.dart'; import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/log.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/logger_db.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/network.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/store.repository.dart'; -import 'package:isar/isar.dart'; -import 'package:path_provider/path_provider.dart'; +import 'package:photo_manager/photo_manager.dart'; void configureFileDownloaderNotifications() { FileDownloader().configureNotificationForGroup( @@ -57,48 +41,10 @@ void configureFileDownloaderNotifications() { } abstract final class Bootstrap { - static Future<(Isar isar, Drift drift, DriftLogger logDb)> initDB() async { + static Future<(Drift, DriftLogger)> initDomain({bool listenStoreUpdates = true, bool shouldBufferLogs = true}) async { final drift = Drift(); final logDb = DriftLogger(); - - Isar? isar = Isar.getInstance(); - - if (isar != null) { - return (isar, drift, logDb); - } - - final dir = await getApplicationDocumentsDirectory(); - isar = await Isar.open( - [ - StoreValueSchema, - AssetSchema, - AlbumSchema, - ExifInfoSchema, - UserSchema, - BackupAlbumSchema, - DuplicatedAssetSchema, - ETagSchema, - if (Platform.isAndroid) AndroidDeviceAssetSchema, - if (Platform.isIOS) IOSDeviceAssetSchema, - DeviceAssetEntitySchema, - ], - directory: dir.path, - maxSizeMiB: 2048, - inspector: kDebugMode, - ); - - return (isar, drift, logDb); - } - - static Future initDomain( - Isar db, - Drift drift, - DriftLogger logDb, { - bool listenStoreUpdates = true, - bool shouldBufferLogs = true, - }) async { - final isBeta = await IsarStoreRepository(db).tryGet(StoreKey.betaTimeline) ?? true; - final IStoreRepository storeRepo = isBeta ? DriftStoreRepository(drift) : IsarStoreRepository(db); + final DriftStoreRepository storeRepo = DriftStoreRepository(drift); await StoreService.init(storeRepository: storeRepo, listenUpdates: listenStoreUpdates); @@ -109,5 +55,8 @@ abstract final class Bootstrap { ); await NetworkRepository.init(); + // Remove once all asset operations are migrated to Native APIs + await PhotoManager.setIgnorePermissionCheck(true); + return (drift, logDb); } } diff --git a/mobile/lib/utils/color_filter_generator.dart b/mobile/lib/utils/color_filter_generator.dart deleted file mode 100644 index 92aed4b1a0..0000000000 --- a/mobile/lib/utils/color_filter_generator.dart +++ /dev/null @@ -1,99 +0,0 @@ -import 'package:flutter/widgets.dart'; - -class InvertionFilter extends StatelessWidget { - final Widget? child; - const InvertionFilter({super.key, this.child}); - - @override - Widget build(BuildContext context) { - return ColorFiltered( - colorFilter: const ColorFilter.matrix([ - -1, 0, 0, 0, 255, // - 0, -1, 0, 0, 255, // - 0, 0, -1, 0, 255, // - 0, 0, 0, 1, 0, // - ]), - child: child, - ); - } -} - -// -1 - darkest, 1 - brightest, 0 - unchanged -class BrightnessFilter extends StatelessWidget { - final Widget? child; - final double brightness; - const BrightnessFilter({super.key, this.child, this.brightness = 0}); - - @override - Widget build(BuildContext context) { - return ColorFiltered( - colorFilter: ColorFilter.matrix(_ColorFilterGenerator.brightnessAdjustMatrix(brightness)), - child: child, - ); - } -} - -// -1 - greyscale, 1 - most saturated, 0 - unchanged -class SaturationFilter extends StatelessWidget { - final Widget? child; - final double saturation; - const SaturationFilter({super.key, this.child, this.saturation = 0}); - - @override - Widget build(BuildContext context) { - return ColorFiltered( - colorFilter: ColorFilter.matrix(_ColorFilterGenerator.saturationAdjustMatrix(saturation)), - child: child, - ); - } -} - -class _ColorFilterGenerator { - static List brightnessAdjustMatrix(double value) { - value = value * 10; - - if (value == 0) { - return [ - 1, 0, 0, 0, 0, // - 0, 1, 0, 0, 0, // - 0, 0, 1, 0, 0, // - 0, 0, 0, 1, 0, // - ]; - } - - return List.from([ - 1, 0, 0, 0, value, 0, 1, 0, 0, value, 0, 0, 1, 0, value, 0, 0, 0, 1, 0, // - ]).map((i) => i.toDouble()).toList(); - } - - static List saturationAdjustMatrix(double value) { - value = value * 100; - - if (value == 0) { - return [ - 1, 0, 0, 0, 0, // - 0, 1, 0, 0, 0, // - 0, 0, 1, 0, 0, // - 0, 0, 0, 1, 0, // - ]; - } - - double x = ((1 + ((value > 0) ? ((3 * value) / 100) : (value / 100)))).toDouble(); - double lumR = 0.3086; - double lumG = 0.6094; - double lumB = 0.082; - - return List.from([ - (lumR * (1 - x)) + x, lumG * (1 - x), lumB * (1 - x), // - 0, 0, // - lumR * (1 - x), // - (lumG * (1 - x)) + x, // - lumB * (1 - x), // - 0, 0, // - lumR * (1 - x), // - lumG * (1 - x), // - (lumB * (1 - x)) + x, // - 0, 0, 0, 0, 0, 1, 0, // - ]).map((i) => i.toDouble()).toList(); - } -} diff --git a/mobile/lib/utils/datetime_comparison.dart b/mobile/lib/utils/datetime_comparison.dart deleted file mode 100644 index f8ddcfea11..0000000000 --- a/mobile/lib/utils/datetime_comparison.dart +++ /dev/null @@ -1,2 +0,0 @@ -bool isAtSameMomentAs(DateTime? a, DateTime? b) => - (a == null && b == null) || ((a != null && b != null) && a.isAtSameMomentAs(b)); diff --git a/mobile/lib/utils/hooks/blurhash_hook.dart b/mobile/lib/utils/hooks/blurhash_hook.dart index ac5fd31724..534c0ad8fb 100644 --- a/mobile/lib/utils/hooks/blurhash_hook.dart +++ b/mobile/lib/utils/hooks/blurhash_hook.dart @@ -1,20 +1,10 @@ import 'dart:convert'; import 'dart:typed_data'; + import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; import 'package:thumbhash/thumbhash.dart' as thumbhash; -ObjectRef useBlurHashRef(Asset? asset) { - if (asset?.thumbhash == null) { - return useRef(null); - } - - final rbga = thumbhash.thumbHashToRGBA(base64Decode(asset!.thumbhash!)); - - return useRef(thumbhash.rgbaToBmp(rbga)); -} - ObjectRef useDriftBlurHashRef(RemoteAsset? asset) { if (asset?.thumbHash == null) { return useRef(null); diff --git a/mobile/lib/utils/image_url_builder.dart b/mobile/lib/utils/image_url_builder.dart index 079f0e51fa..c562049b1d 100644 --- a/mobile/lib/utils/image_url_builder.dart +++ b/mobile/lib/utils/image_url_builder.dart @@ -1,47 +1,7 @@ import 'package:immich_mobile/domain/models/store.model.dart'; -import 'package:immich_mobile/entities/album.entity.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; import 'package:immich_mobile/entities/store.entity.dart'; import 'package:openapi/api.dart'; -String getThumbnailUrl(final Asset asset, {AssetMediaSize type = AssetMediaSize.thumbnail}) { - return getThumbnailUrlForRemoteId(asset.remoteId!, type: type); -} - -String getThumbnailCacheKey(final Asset asset, {AssetMediaSize type = AssetMediaSize.thumbnail}) { - return getThumbnailCacheKeyForRemoteId(asset.remoteId!, asset.thumbhash!, type: type); -} - -String getThumbnailCacheKeyForRemoteId( - final String id, - final String thumbhash, { - AssetMediaSize type = AssetMediaSize.thumbnail, -}) { - if (type == AssetMediaSize.thumbnail) { - return 'thumbnail-image-$id-$thumbhash'; - } else { - return '${id}_${thumbhash}_previewStage'; - } -} - -String getAlbumThumbnailUrl(final Album album, {AssetMediaSize type = AssetMediaSize.thumbnail}) { - if (album.thumbnail.value?.remoteId == null) { - return ''; - } - return getThumbnailUrlForRemoteId(album.thumbnail.value!.remoteId!, type: type); -} - -String getAlbumThumbNailCacheKey(final Album album, {AssetMediaSize type = AssetMediaSize.thumbnail}) { - if (album.thumbnail.value?.remoteId == null) { - return ''; - } - return getThumbnailCacheKeyForRemoteId( - album.thumbnail.value!.remoteId!, - album.thumbnail.value!.thumbhash!, - type: type, - ); -} - String getOriginalUrlForRemoteId(final String id, {bool edited = true}) { return '${Store.get(StoreKey.serverEndpoint)}/assets/$id/original?edited=$edited'; } diff --git a/mobile/lib/utils/immich_loading_overlay.dart b/mobile/lib/utils/immich_loading_overlay.dart deleted file mode 100644 index be49c3bae9..0000000000 --- a/mobile/lib/utils/immich_loading_overlay.dart +++ /dev/null @@ -1,64 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/widgets/common/delayed_loading_indicator.dart'; - -final _loadingEntry = OverlayEntry( - builder: (context) => SizedBox.square( - dimension: double.infinity, - child: DecoratedBox( - decoration: BoxDecoration(color: context.colorScheme.surface.withAlpha(200)), - child: const Center( - child: DelayedLoadingIndicator(delay: Duration(seconds: 1), fadeInDuration: Duration(milliseconds: 400)), - ), - ), - ), -); - -ValueNotifier useProcessingOverlay() { - return use(const _LoadingOverlay()); -} - -class _LoadingOverlay extends Hook> { - const _LoadingOverlay(); - - @override - _LoadingOverlayState createState() => _LoadingOverlayState(); -} - -class _LoadingOverlayState extends HookState, _LoadingOverlay> { - late final _isLoading = ValueNotifier(false)..addListener(_listener); - OverlayEntry? _loadingOverlay; - - void _listener() { - setState(() { - WidgetsBinding.instance.addPostFrameCallback((_) { - if (_isLoading.value) { - _loadingOverlay?.remove(); - _loadingOverlay = _loadingEntry; - Overlay.of(context).insert(_loadingEntry); - } else { - _loadingOverlay?.remove(); - _loadingOverlay = null; - } - }); - }); - } - - @override - ValueNotifier build(BuildContext context) { - return _isLoading; - } - - @override - void dispose() { - _isLoading.dispose(); - super.dispose(); - } - - @override - Object? get debugValue => _isLoading.value; - - @override - String get debugLabel => 'useProcessingOverlay<>'; -} diff --git a/mobile/lib/utils/isolate.dart b/mobile/lib/utils/isolate.dart index c8224b9c55..20b56d4875 100644 --- a/mobile/lib/utils/isolate.dart +++ b/mobile/lib/utils/isolate.dart @@ -5,7 +5,6 @@ import 'package:flutter/services.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/services/log.service.dart'; import 'package:immich_mobile/entities/store.entity.dart'; -import 'package:immich_mobile/providers/db.provider.dart'; import 'package:immich_mobile/providers/infrastructure/cancel.provider.dart'; import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; import 'package:immich_mobile/utils/bootstrap.dart'; @@ -38,13 +37,9 @@ Cancelable runInIsolateGentle({ BackgroundIsolateBinaryMessenger.ensureInitialized(token); DartPluginRegistrant.ensureInitialized(); - final (isar, drift, logDb) = await Bootstrap.initDB(); - await Bootstrap.initDomain(isar, drift, logDb, shouldBufferLogs: false, listenStoreUpdates: false); + final (drift, logDb) = await Bootstrap.initDomain(shouldBufferLogs: false, listenStoreUpdates: false); final ref = ProviderContainer( overrides: [ - // TODO: Remove once isar is removed - dbProvider.overrideWithValue(isar), - isarProvider.overrideWithValue(isar), cancellationProvider.overrideWithValue(cancelledChecker), driftProvider.overrideWith(driftOverride(drift)), ], @@ -66,15 +61,6 @@ Cancelable runInIsolateGentle({ await LogService.I.dispose(); await logDb.close(); await drift.close(); - - // Close Isar safely - try { - if (isar.isOpen) { - await isar.close(); - } - } catch (e) { - dPrint(() => "Error closing Isar: $e"); - } } catch (error, stack) { dPrint(() => "Error closing resources in isolate: $error, $stack"); } finally { diff --git a/mobile/lib/utils/migration.dart b/mobile/lib/utils/migration.dart index 76916cee1e..9ac805af39 100644 --- a/mobile/lib/utils/migration.dart +++ b/mobile/lib/utils/migration.dart @@ -1,115 +1,14 @@ import 'dart:async'; -import 'dart:convert'; -import 'dart:io'; -import 'package:collection/collection.dart'; -import 'package:drift/drift.dart'; -import 'package:immich_mobile/domain/models/album/local_album.model.dart'; -import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/domain/models/store.model.dart'; -import 'package:immich_mobile/entities/album.entity.dart'; -import 'package:immich_mobile/entities/android_device_asset.entity.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/entities/backup_album.entity.dart' as isar_backup_album; -import 'package:immich_mobile/entities/etag.entity.dart'; -import 'package:immich_mobile/entities/ios_device_asset.entity.dart'; import 'package:immich_mobile/entities/store.entity.dart'; -import 'package:immich_mobile/infrastructure/entities/device_asset.entity.dart'; -import 'package:immich_mobile/infrastructure/entities/exif.entity.dart'; -import 'package:immich_mobile/infrastructure/entities/local_album.entity.drift.dart'; -import 'package:immich_mobile/infrastructure/entities/local_asset.entity.drift.dart'; -import 'package:immich_mobile/infrastructure/entities/trashed_local_asset.entity.drift.dart'; -import 'package:immich_mobile/infrastructure/entities/store.entity.dart'; -import 'package:immich_mobile/infrastructure/entities/store.entity.drift.dart'; -import 'package:immich_mobile/infrastructure/entities/user.entity.dart'; -import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; -import 'package:immich_mobile/infrastructure/repositories/sync_stream.repository.dart'; -import 'package:immich_mobile/platform/native_sync_api.g.dart'; import 'package:immich_mobile/infrastructure/repositories/network.repository.dart'; -import 'package:immich_mobile/platform/network_api.g.dart'; -import 'package:immich_mobile/providers/infrastructure/platform.provider.dart'; import 'package:immich_mobile/services/api.service.dart'; -import 'package:immich_mobile/services/app_settings.service.dart'; -import 'package:immich_mobile/utils/datetime_helpers.dart'; -import 'package:immich_mobile/utils/debug_print.dart'; -import 'package:immich_mobile/utils/diff.dart'; -import 'package:isar/isar.dart'; -// ignore: import_rule_photo_manager -import 'package:photo_manager/photo_manager.dart'; const int targetVersion = 25; -Future migrateDatabaseIfNeeded(Isar db, Drift drift) async { - final hasVersion = Store.tryGet(StoreKey.version) != null; +Future migrateDatabaseIfNeeded() async { final int version = Store.get(StoreKey.version, targetVersion); - if (version < 9) { - await Store.put(StoreKey.version, targetVersion); - final value = await db.storeValues.get(StoreKey.currentUser.id); - if (value != null) { - final id = value.intValue; - if (id != null) { - await db.writeTxn(() async { - final user = await db.users.get(id); - await db.storeValues.put(StoreValue(StoreKey.currentUser.id, strValue: user?.id)); - }); - } - } - } - - if (version < 10) { - await Store.put(StoreKey.version, targetVersion); - await _migrateDeviceAsset(db); - } - - if (version < 13) { - await Store.put(StoreKey.photoManagerCustomFilter, true); - } - - // This means that the SQLite DB is just created and has no version - if (version < 14 || !hasVersion) { - await migrateStoreToSqlite(db, drift); - await Store.populateCache(); - } - - final syncStreamRepository = SyncStreamRepository(drift); - await handleBetaMigration(version, await _isNewInstallation(db, drift), syncStreamRepository); - - if (version < 17 && Store.isBetaTimelineEnabled) { - final delay = Store.get(StoreKey.backupTriggerDelay, AppSettingsEnum.backupTriggerDelay.defaultValue); - if (delay >= 1000) { - await Store.put(StoreKey.backupTriggerDelay, (delay / 1000).toInt()); - } - } - - if (version < 18 && Store.isBetaTimelineEnabled) { - await syncStreamRepository.reset(); - await Store.put(StoreKey.shouldResetSync, true); - } - - if (version < 19 && Store.isBetaTimelineEnabled) { - if (!await _populateLocalAssetTime(drift)) { - return; - } - } - - if (version < 20 && Store.isBetaTimelineEnabled) { - await _syncLocalAlbumIsIosSharedAlbum(drift); - } - - if (version < 21) { - final certData = SSLClientCertStoreVal.load(); - if (certData != null) { - await networkApi.addCertificate(ClientCertData(data: certData.data, password: certData.password ?? "")); - } - } - - if (version < 23 && Store.isBetaTimelineEnabled) { - await _populateLocalAssetPlaybackStyle(drift); - } - - if (version < 24 && Store.isBetaTimelineEnabled) { - await _applyLocalAssetOrientation(drift); - } if (version < 25) { final accessToken = Store.tryGet(StoreKey.accessToken); @@ -121,365 +20,6 @@ Future migrateDatabaseIfNeeded(Isar db, Drift drift) async { } } - if (version < 22 && !Store.isBetaTimelineEnabled) { - await Store.put(StoreKey.needBetaMigration, true); - } - - if (targetVersion >= 12) { - await Store.put(StoreKey.version, targetVersion); - return; - } - - final shouldTruncate = version < 8 || version < targetVersion; - - if (shouldTruncate) { - await _migrateTo(db, targetVersion); - } -} - -Future handleBetaMigration(int version, bool isNewInstallation, SyncStreamRepository syncStreamRepository) async { - // Handle migration only for this version - // TODO: remove when old timeline is removed - final isBeta = Store.tryGet(StoreKey.betaTimeline); - final needBetaMigration = Store.tryGet(StoreKey.needBetaMigration); - if (version <= 15 && needBetaMigration == null) { - // For new installations, no migration needed - // For existing installations, only migrate if beta timeline is not enabled (null or false) - if (isNewInstallation || isBeta == true) { - await Store.put(StoreKey.needBetaMigration, false); - await Store.put(StoreKey.betaTimeline, true); - } else { - await Store.put(StoreKey.needBetaMigration, true); - } - } - - if (version > 15) { - if (isBeta == null || isBeta) { - await Store.put(StoreKey.needBetaMigration, false); - await Store.put(StoreKey.betaTimeline, true); - } else { - await Store.put(StoreKey.needBetaMigration, false); - } - } - - if (version < 16) { - await syncStreamRepository.reset(); - await Store.put(StoreKey.shouldResetSync, true); - } -} - -Future _isNewInstallation(Isar db, Drift drift) async { - try { - final isarUserCount = await db.users.count(); - if (isarUserCount > 0) { - return false; - } - - final isarAssetCount = await db.assets.count(); - if (isarAssetCount > 0) { - return false; - } - - final driftStoreCount = await drift.storeEntity.select().get().then((list) => list.length); - if (driftStoreCount > 0) { - return false; - } - - final driftAssetCount = await drift.localAssetEntity.select().get().then((list) => list.length); - if (driftAssetCount > 0) { - return false; - } - - return true; - } catch (error) { - dPrint(() => "[MIGRATION] Error checking if new installation: $error"); - return false; - } -} - -Future _migrateTo(Isar db, int version) async { - await Store.delete(StoreKey.assetETag); - await db.writeTxn(() async { - await db.assets.clear(); - await db.exifInfos.clear(); - await db.albums.clear(); - await db.eTags.clear(); - await db.users.clear(); - }); - await Store.put(StoreKey.version, version); -} - -Future _migrateDeviceAsset(Isar db) async { - final ids = Platform.isAndroid - ? (await db.androidDeviceAssets.where().findAll()) - .map((a) => _DeviceAsset(assetId: a.id.toString(), hash: a.hash)) - .toList() - : (await db.iOSDeviceAssets.where().findAll()).map((i) => _DeviceAsset(assetId: i.id, hash: i.hash)).toList(); - - final PermissionState ps = await PhotoManager.requestPermissionExtend(); - if (!ps.hasAccess) { - dPrint(() => "[MIGRATION] Photo library permission not granted. Skipping device asset migration."); - return; - } - - List<_DeviceAsset> localAssets = []; - final List paths = await PhotoManager.getAssetPathList(onlyAll: true); - - if (paths.isEmpty) { - localAssets = (await db.assets.where().anyOf(ids, (query, id) => query.localIdEqualTo(id.assetId)).findAll()) - .map((a) => _DeviceAsset(assetId: a.localId!, dateTime: a.fileModifiedAt)) - .toList(); - } else { - final AssetPathEntity albumWithAll = paths.first; - final int assetCount = await albumWithAll.assetCountAsync; - - final List allDeviceAssets = await albumWithAll.getAssetListRange(start: 0, end: assetCount); - - localAssets = allDeviceAssets.map((a) => _DeviceAsset(assetId: a.id, dateTime: a.modifiedDateTime)).toList(); - } - - dPrint(() => "[MIGRATION] Device Asset Ids length - ${ids.length}"); - dPrint(() => "[MIGRATION] Local Asset Ids length - ${localAssets.length}"); - ids.sort((a, b) => a.assetId.compareTo(b.assetId)); - localAssets.sort((a, b) => a.assetId.compareTo(b.assetId)); - final List toAdd = []; - await diffSortedLists( - ids, - localAssets, - compare: (a, b) => a.assetId.compareTo(b.assetId), - both: (deviceAsset, asset) { - toAdd.add( - DeviceAssetEntity(assetId: deviceAsset.assetId, hash: deviceAsset.hash!, modifiedTime: asset.dateTime!), - ); - return false; - }, - onlyFirst: (deviceAsset) { - dPrint(() => '[MIGRATION] Local asset not found in DeviceAsset: ${deviceAsset.assetId}'); - }, - onlySecond: (asset) { - dPrint(() => '[MIGRATION] Local asset not found in DeviceAsset: ${asset.assetId}'); - }, - ); - - dPrint(() => "[MIGRATION] Total number of device assets migrated - ${toAdd.length}"); - - await db.writeTxn(() async { - await db.deviceAssetEntitys.putAll(toAdd); - }); -} - -Future _populateLocalAssetTime(Drift db) async { - try { - final nativeApi = NativeSyncApi(); - final albums = await nativeApi.getAlbums(); - for (final album in albums) { - final assets = await nativeApi.getAssetsForAlbum(album.id); - await db.batch((batch) async { - for (final asset in assets) { - batch.update( - db.localAssetEntity, - LocalAssetEntityCompanion( - longitude: Value(asset.longitude), - latitude: Value(asset.latitude), - adjustmentTime: Value(tryFromSecondsSinceEpoch(asset.adjustmentTime, isUtc: true)), - updatedAt: Value(tryFromSecondsSinceEpoch(asset.updatedAt, isUtc: true) ?? DateTime.timestamp()), - ), - where: (t) => t.id.equals(asset.id), - ); - } - }); - } - - return true; - } catch (error) { - dPrint(() => "[MIGRATION] Error while populating asset time: $error"); - return false; - } -} - -Future _syncLocalAlbumIsIosSharedAlbum(Drift db) async { - try { - final nativeApi = NativeSyncApi(); - final albums = await nativeApi.getAlbums(); - await db.batch((batch) { - for (final album in albums) { - batch.update( - db.localAlbumEntity, - LocalAlbumEntityCompanion(isIosSharedAlbum: Value(album.isCloud)), - where: (t) => t.id.equals(album.id), - ); - } - }); - dPrint(() => "[MIGRATION] Successfully updated isIosSharedAlbum for ${albums.length} albums"); - } catch (error) { - dPrint(() => "[MIGRATION] Error while syncing local album isIosSharedAlbum: $error"); - } -} - -Future migrateDeviceAssetToSqlite(Isar db, Drift drift) async { - try { - final isarDeviceAssets = await db.deviceAssetEntitys.where().findAll(); - await drift.batch((batch) { - for (final deviceAsset in isarDeviceAssets) { - batch.update( - drift.localAssetEntity, - LocalAssetEntityCompanion(checksum: Value(base64.encode(deviceAsset.hash))), - where: (t) => t.id.equals(deviceAsset.assetId), - ); - } - }); - } catch (error) { - dPrint(() => "[MIGRATION] Error while migrating device assets to SQLite: $error"); - } -} - -Future migrateBackupAlbumsToSqlite(Isar db, Drift drift) async { - try { - final isarBackupAlbums = await db.backupAlbums.where().findAll(); - // Recents is a virtual album on Android, and we don't have it with the new sync - // If recents is selected previously, select all albums during migration except the excluded ones - if (Platform.isAndroid) { - final recentAlbum = isarBackupAlbums.firstWhereOrNull((album) => album.id == 'isAll'); - if (recentAlbum != null) { - await drift.localAlbumEntity.update().write( - const LocalAlbumEntityCompanion(backupSelection: Value(BackupSelection.selected)), - ); - final excluded = isarBackupAlbums - .where((album) => album.selection == isar_backup_album.BackupSelection.exclude) - .map((album) => album.id) - .toList(); - await drift.batch((batch) async { - for (final id in excluded) { - batch.update( - drift.localAlbumEntity, - const LocalAlbumEntityCompanion(backupSelection: Value(BackupSelection.excluded)), - where: (t) => t.id.equals(id), - ); - } - }); - return; - } - } - - await drift.batch((batch) { - for (final album in isarBackupAlbums) { - batch.update( - drift.localAlbumEntity, - LocalAlbumEntityCompanion( - backupSelection: Value(switch (album.selection) { - isar_backup_album.BackupSelection.none => BackupSelection.none, - isar_backup_album.BackupSelection.select => BackupSelection.selected, - isar_backup_album.BackupSelection.exclude => BackupSelection.excluded, - }), - ), - where: (t) => t.id.equals(album.id), - ); - } - }); - } catch (error) { - dPrint(() => "[MIGRATION] Error while migrating backup albums to SQLite: $error"); - } -} - -Future migrateStoreToSqlite(Isar db, Drift drift) async { - try { - final isarStoreValues = await db.storeValues.where().findAll(); - await drift.batch((batch) { - for (final storeValue in isarStoreValues) { - final companion = StoreEntityCompanion( - id: Value(storeValue.id), - stringValue: Value(storeValue.strValue), - intValue: Value(storeValue.intValue), - ); - batch.insert(drift.storeEntity, companion, onConflict: DoUpdate((_) => companion)); - } - }); - } catch (error) { - dPrint(() => "[MIGRATION] Error while migrating store values to SQLite: $error"); - } -} - -Future migrateStoreToIsar(Isar db, Drift drift) async { - try { - final driftStoreValues = await drift.storeEntity - .select() - .map((entity) => StoreValue(entity.id, intValue: entity.intValue, strValue: entity.stringValue)) - .get(); - - await db.writeTxn(() async { - await db.storeValues.putAll(driftStoreValues); - }); - } catch (error) { - dPrint(() => "[MIGRATION] Error while migrating store values to Isar: $error"); - } -} - -Future _populateLocalAssetPlaybackStyle(Drift db) async { - try { - final nativeApi = NativeSyncApi(); - - final albums = await nativeApi.getAlbums(); - for (final album in albums) { - final assets = await nativeApi.getAssetsForAlbum(album.id); - await db.batch((batch) { - for (final asset in assets) { - batch.update( - db.localAssetEntity, - LocalAssetEntityCompanion(playbackStyle: Value(_toPlaybackStyle(asset.playbackStyle))), - where: (t) => t.id.equals(asset.id), - ); - } - }); - } - - if (Platform.isAndroid) { - final trashedAssetMap = await nativeApi.getTrashedAssets(); - for (final entry in trashedAssetMap.cast>().entries) { - final assets = entry.value.cast(); - await db.batch((batch) { - for (final asset in assets) { - batch.update( - db.trashedLocalAssetEntity, - TrashedLocalAssetEntityCompanion(playbackStyle: Value(_toPlaybackStyle(asset.playbackStyle))), - where: (t) => t.id.equals(asset.id), - ); - } - }); - } - dPrint(() => "[MIGRATION] Successfully populated playbackStyle for local and trashed assets"); - } else { - dPrint(() => "[MIGRATION] Successfully populated playbackStyle for local assets"); - } - } catch (error) { - dPrint(() => "[MIGRATION] Error while populating playbackStyle: $error"); - } -} - -Future _applyLocalAssetOrientation(Drift db) { - final query = db.localAssetEntity.update() - ..where((filter) => (filter.orientation.equals(90) | (filter.orientation.equals(270)))); - return query.write( - LocalAssetEntityCompanion.custom( - width: db.localAssetEntity.height, - height: db.localAssetEntity.width, - orientation: const Variable(0), - ), - ); -} - -AssetPlaybackStyle _toPlaybackStyle(PlatformAssetPlaybackStyle style) => switch (style) { - PlatformAssetPlaybackStyle.unknown => AssetPlaybackStyle.unknown, - PlatformAssetPlaybackStyle.image => AssetPlaybackStyle.image, - PlatformAssetPlaybackStyle.video => AssetPlaybackStyle.video, - PlatformAssetPlaybackStyle.imageAnimated => AssetPlaybackStyle.imageAnimated, - PlatformAssetPlaybackStyle.livePhoto => AssetPlaybackStyle.livePhoto, - PlatformAssetPlaybackStyle.videoLooping => AssetPlaybackStyle.videoLooping, -}; - -class _DeviceAsset { - final String assetId; - final List? hash; - final DateTime? dateTime; - - const _DeviceAsset({required this.assetId, this.hash, this.dateTime}); + await Store.put(StoreKey.version, targetVersion); + return; } diff --git a/mobile/lib/utils/provider_utils.dart b/mobile/lib/utils/provider_utils.dart index 6c2d6e0f11..9524433c05 100644 --- a/mobile/lib/utils/provider_utils.dart +++ b/mobile/lib/utils/provider_utils.dart @@ -2,21 +2,17 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/providers/infrastructure/search.provider.dart'; import 'package:immich_mobile/providers/infrastructure/user.provider.dart'; import 'package:immich_mobile/repositories/activity_api.repository.dart'; -import 'package:immich_mobile/repositories/album_api.repository.dart'; import 'package:immich_mobile/repositories/asset_api.repository.dart'; import 'package:immich_mobile/repositories/drift_album_api_repository.dart'; import 'package:immich_mobile/repositories/partner_api.repository.dart'; import 'package:immich_mobile/repositories/person_api.repository.dart'; -import 'package:immich_mobile/repositories/timeline.repository.dart'; void invalidateAllApiRepositoryProviders(WidgetRef ref) { ref.invalidate(userApiRepositoryProvider); ref.invalidate(activityApiRepositoryProvider); ref.invalidate(partnerApiRepositoryProvider); - ref.invalidate(albumApiRepositoryProvider); ref.invalidate(personApiRepositoryProvider); ref.invalidate(assetApiRepositoryProvider); - ref.invalidate(timelineRepositoryProvider); ref.invalidate(searchApiRepositoryProvider); // Drift diff --git a/mobile/lib/utils/selection_handlers.dart b/mobile/lib/utils/selection_handlers.dart deleted file mode 100644 index f0d333e262..0000000000 --- a/mobile/lib/utils/selection_handlers.dart +++ /dev/null @@ -1,143 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:fluttertoast/fluttertoast.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/constants/enums.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/extensions/asset_extensions.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; -import 'package:immich_mobile/providers/asset.provider.dart'; -import 'package:immich_mobile/services/asset.service.dart'; -import 'package:immich_mobile/services/share.service.dart'; -import 'package:immich_mobile/widgets/common/date_time_picker.dart'; -import 'package:immich_mobile/widgets/common/immich_toast.dart'; -import 'package:immich_mobile/widgets/common/location_picker.dart'; -import 'package:immich_mobile/widgets/common/share_dialog.dart'; -import 'package:maplibre_gl/maplibre_gl.dart'; - -void handleShareAssets(WidgetRef ref, BuildContext context, Iterable selection) { - showDialog( - context: context, - builder: (BuildContext buildContext) { - ref.watch(shareServiceProvider).shareAssets(selection.toList(), context).then((bool status) { - if (!status) { - ImmichToast.show( - context: context, - msg: 'image_viewer_page_state_provider_share_error'.tr(), - toastType: ToastType.error, - gravity: ToastGravity.BOTTOM, - ); - } - buildContext.pop(); - }); - return const ShareDialog(); - }, - barrierDismissible: false, - useRootNavigator: false, - ); -} - -Future handleArchiveAssets( - WidgetRef ref, - BuildContext context, - List selection, { - bool? shouldArchive, - ToastGravity toastGravity = ToastGravity.BOTTOM, -}) async { - if (selection.isNotEmpty) { - shouldArchive ??= !selection.every((a) => a.isArchived); - await ref.read(assetProvider.notifier).toggleArchive(selection, shouldArchive); - final message = shouldArchive - ? 'moved_to_archive'.t(context: context, args: {'count': selection.length}) - : 'moved_to_library'.t(context: context, args: {'count': selection.length}); - if (context.mounted) { - ImmichToast.show(context: context, msg: message, gravity: toastGravity); - } - } -} - -Future handleFavoriteAssets( - WidgetRef ref, - BuildContext context, - List selection, { - bool? shouldFavorite, - ToastGravity toastGravity = ToastGravity.BOTTOM, -}) async { - if (selection.isNotEmpty) { - shouldFavorite ??= !selection.every((a) => a.isFavorite); - await ref.watch(assetProvider.notifier).toggleFavorite(selection, shouldFavorite); - - final assetOrAssets = selection.length > 1 ? 'assets' : 'asset'; - final toastMessage = shouldFavorite - ? 'Added ${selection.length} $assetOrAssets to favorites' - : 'Removed ${selection.length} $assetOrAssets from favorites'; - if (context.mounted) { - ImmichToast.show(context: context, msg: toastMessage, gravity: toastGravity); - } - } -} - -Future handleEditDateTime(WidgetRef ref, BuildContext context, List selection) async { - DateTime? initialDate; - String? timeZone; - Duration? offset; - if (selection.length == 1) { - final asset = selection.first; - final assetWithExif = await ref.watch(assetServiceProvider).loadExif(asset); - final (dt, oft) = assetWithExif.getTZAdjustedTimeAndOffset(); - initialDate = dt; - offset = oft; - timeZone = assetWithExif.exifInfo?.timeZone; - } - final dateTime = await showDateTimePicker( - context: context, - initialDateTime: initialDate, - initialTZ: timeZone, - initialTZOffset: offset, - ); - - if (dateTime == null) { - return; - } - - await ref.read(assetServiceProvider).changeDateTime(selection.toList(), dateTime); -} - -Future handleEditLocation(WidgetRef ref, BuildContext context, List selection) async { - LatLng? initialLatLng; - if (selection.length == 1) { - final asset = selection.first; - final assetWithExif = await ref.watch(assetServiceProvider).loadExif(asset); - if (assetWithExif.exifInfo?.latitude != null && assetWithExif.exifInfo?.longitude != null) { - initialLatLng = LatLng(assetWithExif.exifInfo!.latitude!, assetWithExif.exifInfo!.longitude!); - } - } - - final location = await showLocationPicker(context: context, initialLatLng: initialLatLng); - - if (location == null) { - return; - } - - await ref.read(assetServiceProvider).changeLocation(selection.toList(), location); -} - -Future handleSetAssetsVisibility( - WidgetRef ref, - BuildContext context, - AssetVisibilityEnum visibility, - List selection, -) async { - if (selection.isNotEmpty) { - await ref.watch(assetProvider.notifier).setLockedView(selection, visibility); - - final assetOrAssets = selection.length > 1 ? 'assets' : 'asset'; - final toastMessage = visibility == AssetVisibilityEnum.locked - ? 'Added ${selection.length} $assetOrAssets to locked folder' - : 'Removed ${selection.length} $assetOrAssets from locked folder'; - if (context.mounted) { - ImmichToast.show(context: context, msg: toastMessage, gravity: ToastGravity.BOTTOM); - } - } -} diff --git a/mobile/lib/utils/string_helper.dart b/mobile/lib/utils/string_helper.dart deleted file mode 100644 index 201d141531..0000000000 --- a/mobile/lib/utils/string_helper.dart +++ /dev/null @@ -1,7 +0,0 @@ -extension StringExtension on String { - String capitalizeFirstLetter() { - return "${this[0].toUpperCase()}${substring(1).toLowerCase()}"; - } -} - -String s(num count) => (count == 1 ? '' : 's'); diff --git a/mobile/lib/utils/throttle.dart b/mobile/lib/utils/throttle.dart deleted file mode 100644 index 8b41d92318..0000000000 --- a/mobile/lib/utils/throttle.dart +++ /dev/null @@ -1,51 +0,0 @@ -import 'package:flutter_hooks/flutter_hooks.dart'; - -/// Throttles function calls with the [interval] provided. -/// Also make sures to call the last Action after the elapsed interval -class Throttler { - final Duration interval; - DateTime? _lastActionTime; - - Throttler({required this.interval}); - - T? run(T Function() action) { - if (_lastActionTime == null || (DateTime.now().difference(_lastActionTime!) > interval)) { - final response = action(); - _lastActionTime = DateTime.now(); - return response; - } - - return null; - } - - void dispose() { - _lastActionTime = null; - } -} - -/// Creates a [Throttler] that will be disposed automatically. If no [interval] is provided, a -/// default interval of 300ms is used to throttle the function calls -Throttler useThrottler({Duration interval = const Duration(milliseconds: 300), List? keys}) => - use(_ThrottleHook(interval: interval, keys: keys)); - -class _ThrottleHook extends Hook { - const _ThrottleHook({required this.interval, super.keys}); - - final Duration interval; - - @override - HookState> createState() => _ThrottlerHookState(); -} - -class _ThrottlerHookState extends HookState { - late final throttler = Throttler(interval: hook.interval); - - @override - Throttler build(_) => throttler; - - @override - void dispose() => throttler.dispose(); - - @override - String get debugLabel => 'useThrottler'; -} diff --git a/mobile/lib/utils/thumbnail_utils.dart b/mobile/lib/utils/thumbnail_utils.dart deleted file mode 100644 index 685dc2b1c2..0000000000 --- a/mobile/lib/utils/thumbnail_utils.dart +++ /dev/null @@ -1,49 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:immich_mobile/domain/models/exif.model.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; - -String getAltText(ExifInfo? exifInfo, DateTime fileCreatedAt, AssetType type, List peopleNames) { - if (exifInfo?.description != null && exifInfo!.description!.isNotEmpty) { - return exifInfo.description!; - } - final (template, args) = getAltTextTemplate(exifInfo, fileCreatedAt, type, peopleNames); - return template.t(args: args); -} - -(String, Map) getAltTextTemplate( - ExifInfo? exifInfo, - DateTime fileCreatedAt, - AssetType type, - List peopleNames, -) { - final isVideo = type == AssetType.video; - final hasLocation = exifInfo?.city != null && exifInfo?.country != null; - final date = DateFormat.yMMMMd().format(fileCreatedAt); - final args = { - "isVideo": isVideo.toString(), - "date": date, - "city": exifInfo?.city ?? "", - "country": exifInfo?.country ?? "", - "person1": peopleNames.elementAtOrNull(0) ?? "", - "person2": peopleNames.elementAtOrNull(1) ?? "", - "person3": peopleNames.elementAtOrNull(2) ?? "", - "additionalCount": (peopleNames.length - 3).toString(), - }; - final template = hasLocation - ? (switch (peopleNames.length) { - 0 => "image_alt_text_date_place", - 1 => "image_alt_text_date_place_1_person", - 2 => "image_alt_text_date_place_2_people", - 3 => "image_alt_text_date_place_3_people", - _ => "image_alt_text_date_place_4_or_more_people", - }) - : (switch (peopleNames.length) { - 0 => "image_alt_text_date", - 1 => "image_alt_text_date_1_person", - 2 => "image_alt_text_date_2_people", - 3 => "image_alt_text_date_3_people", - _ => "image_alt_text_date_4_or_more_people", - }); - return (template, args); -} diff --git a/mobile/lib/widgets/activities/activity_text_field.dart b/mobile/lib/widgets/activities/activity_text_field.dart deleted file mode 100644 index d21cdfbc94..0000000000 --- a/mobile/lib/widgets/activities/activity_text_field.dart +++ /dev/null @@ -1,85 +0,0 @@ -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/providers/activity.provider.dart'; -import 'package:immich_mobile/providers/album/current_album.provider.dart'; -import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart'; -import 'package:immich_mobile/providers/user.provider.dart'; -import 'package:immich_mobile/widgets/common/user_circle_avatar.dart'; - -class ActivityTextField extends HookConsumerWidget { - final bool isEnabled; - final String? likeId; - final Function(String) onSubmit; - - const ActivityTextField({required this.onSubmit, this.isEnabled = true, this.likeId, super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final album = ref.watch(currentAlbumProvider)!; - final asset = ref.watch(currentAssetProvider); - final activityNotifier = ref.read(albumActivityProvider(album.remoteId!, asset?.remoteId).notifier); - final user = ref.watch(currentUserProvider); - final inputController = useTextEditingController(); - final inputFocusNode = useFocusNode(); - final liked = likeId != null; - - // Show keyboard immediately on activities open - useEffect(() { - inputFocusNode.requestFocus(); - return null; - }, []); - - // Pass text to callback and reset controller - void onEditingComplete() { - onSubmit(inputController.text); - inputController.clear(); - inputFocusNode.unfocus(); - } - - Future addLike() async { - await activityNotifier.addLike(); - } - - Future removeLike() async { - if (liked) { - await activityNotifier.removeActivity(likeId!); - } - } - - return Padding( - padding: const EdgeInsets.only(bottom: 10), - child: TextField( - controller: inputController, - enabled: isEnabled, - focusNode: inputFocusNode, - textInputAction: TextInputAction.send, - autofocus: false, - decoration: InputDecoration( - border: InputBorder.none, - focusedBorder: InputBorder.none, - prefixIcon: user != null - ? Padding( - padding: const EdgeInsets.symmetric(horizontal: 15), - child: UserCircleAvatar(user: user, size: 30), - ) - : null, - suffixIcon: Padding( - padding: const EdgeInsets.only(right: 10), - child: IconButton( - icon: Icon(liked ? Icons.thumb_up : Icons.thumb_up_off_alt), - onPressed: liked ? removeLike : addLike, - ), - ), - suffixIconColor: liked ? context.primaryColor : null, - hintText: !isEnabled ? 'shared_album_activities_input_disable'.tr() : 'say_something'.tr(), - hintStyle: TextStyle(fontWeight: FontWeight.normal, fontSize: 14, color: Colors.grey[600]), - ), - onEditingComplete: onEditingComplete, - onTapOutside: (_) => inputFocusNode.unfocus(), - ), - ); - } -} diff --git a/mobile/lib/widgets/activities/activity_tile.dart b/mobile/lib/widgets/activities/activity_tile.dart deleted file mode 100644 index ac3b6c95a4..0000000000 --- a/mobile/lib/widgets/activities/activity_tile.dart +++ /dev/null @@ -1,113 +0,0 @@ -import 'package:auto_route/auto_route.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/extensions/datetime_extensions.dart'; -import 'package:immich_mobile/models/activities/activity.model.dart'; -import 'package:immich_mobile/presentation/widgets/images/remote_image_provider.dart'; -import 'package:immich_mobile/providers/activity_service.provider.dart'; -import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart'; -import 'package:immich_mobile/widgets/common/user_circle_avatar.dart'; - -class ActivityTile extends HookConsumerWidget { - final Activity activity; - final bool isBottomSheet; - - const ActivityTile(this.activity, {super.key, this.isBottomSheet = false}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final asset = ref.watch(currentAssetProvider); - final isLike = activity.type == ActivityType.like; - // Asset thumbnail is displayed when we are accessing activities from the album page - // currentAssetProvider will not be set until we open the gallery viewer - final showAssetThumbnail = asset == null && activity.assetId != null && !isBottomSheet; - - onTap() async { - final activityService = ref.read(activityServiceProvider); - final route = await activityService.buildAssetViewerRoute(activity.assetId!, ref); - if (route != null) { - await context.pushRoute(route); - } - } - - return ListTile( - minVerticalPadding: 15, - leading: isLike - ? Container( - width: isBottomSheet ? 30 : 44, - alignment: Alignment.center, - child: Icon(Icons.thumb_up, color: context.primaryColor), - ) - : isBottomSheet - ? UserCircleAvatar(user: activity.user, size: 30) - : UserCircleAvatar(user: activity.user), - title: _ActivityTitle( - userName: activity.user.name, - createdAt: activity.createdAt.timeAgo(), - leftAlign: isBottomSheet ? false : (isLike || showAssetThumbnail), - ), - // No subtitle for like, so center title - titleAlignment: !isLike ? ListTileTitleAlignment.top : ListTileTitleAlignment.center, - trailing: showAssetThumbnail ? _ActivityAssetThumbnail(activity.assetId!, onTap) : null, - subtitle: !isLike ? Text(activity.comment!) : null, - ); - } -} - -class _ActivityTitle extends StatelessWidget { - final String userName; - final String createdAt; - final bool leftAlign; - - const _ActivityTitle({required this.userName, required this.createdAt, required this.leftAlign}); - - @override - Widget build(BuildContext context) { - final textColor = context.isDarkTheme ? Colors.white : Colors.black; - final textStyle = context.textTheme.bodyMedium?.copyWith(color: textColor.withValues(alpha: 0.6)); - - return Row( - mainAxisAlignment: leftAlign ? MainAxisAlignment.start : MainAxisAlignment.spaceBetween, - mainAxisSize: leftAlign ? MainAxisSize.min : MainAxisSize.max, - children: [ - Text(userName, style: textStyle, overflow: TextOverflow.ellipsis), - if (leftAlign) Text(" • ", style: textStyle), - Expanded( - child: Text( - createdAt, - style: textStyle, - overflow: TextOverflow.ellipsis, - textAlign: leftAlign ? TextAlign.left : TextAlign.right, - ), - ), - ], - ); - } -} - -class _ActivityAssetThumbnail extends StatelessWidget { - final String assetId; - final GestureTapCallback? onTap; - - const _ActivityAssetThumbnail(this.assetId, this.onTap); - - @override - Widget build(BuildContext context) { - return GestureDetector( - onTap: onTap, - child: Container( - width: 40, - height: 30, - decoration: BoxDecoration( - borderRadius: const BorderRadius.all(Radius.circular(4)), - image: DecorationImage( - image: RemoteImageProvider.thumbnail(assetId: assetId, thumbhash: ""), - fit: BoxFit.cover, - ), - ), - child: const SizedBox.shrink(), - ), - ); - } -} diff --git a/mobile/lib/widgets/activities/dismissible_activity.dart b/mobile/lib/widgets/activities/dismissible_activity.dart index 806181ecdc..c056f5ee35 100644 --- a/mobile/lib/widgets/activities/dismissible_activity.dart +++ b/mobile/lib/widgets/activities/dismissible_activity.dart @@ -1,8 +1,6 @@ import 'package:flutter/material.dart'; -import 'package:immich_mobile/widgets/activities/activity_tile.dart'; import 'package:immich_mobile/widgets/common/confirm_dialog.dart'; -/// Wraps an [ActivityTile] and makes it dismissible class DismissibleActivity extends StatelessWidget { final String activityId; final Widget body; diff --git a/mobile/lib/widgets/album/add_to_album_bottom_sheet.dart b/mobile/lib/widgets/album/add_to_album_bottom_sheet.dart deleted file mode 100644 index d8f6a8885a..0000000000 --- a/mobile/lib/widgets/album/add_to_album_bottom_sheet.dart +++ /dev/null @@ -1,98 +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/providers/album/album.provider.dart'; -import 'package:immich_mobile/services/album.service.dart'; -import 'package:immich_mobile/widgets/album/add_to_album_sliverlist.dart'; -import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/entities/album.entity.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/widgets/common/drag_sheet.dart'; -import 'package:immich_mobile/widgets/common/immich_toast.dart'; - -class AddToAlbumBottomSheet extends HookConsumerWidget { - /// The asset to add to an album - final List assets; - - const AddToAlbumBottomSheet({super.key, required this.assets}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final albums = ref.watch(albumProvider).where((a) => a.isRemote).toList(); - final albumService = ref.watch(albumServiceProvider); - - useEffect(() { - // Fetch album updates, e.g., cover image - ref.read(albumProvider.notifier).refreshRemoteAlbums(); - - return null; - }, []); - - void addToAlbum(Album album) async { - final result = await albumService.addAssets(album, assets); - - if (result != null) { - if (result.alreadyInAlbum.isNotEmpty) { - ImmichToast.show( - context: context, - msg: 'add_to_album_bottom_sheet_already_exists'.tr(namedArgs: {"album": album.name}), - ); - } else { - ImmichToast.show( - context: context, - msg: 'add_to_album_bottom_sheet_added'.tr(namedArgs: {"album": album.name}), - ); - } - } - context.pop(); - } - - return Card( - elevation: 0, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.only(topLeft: Radius.circular(15), topRight: Radius.circular(15)), - ), - child: CustomScrollView( - slivers: [ - SliverPadding( - padding: const EdgeInsets.symmetric(horizontal: 16), - sliver: SliverToBoxAdapter( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const SizedBox(height: 12), - const Align(alignment: Alignment.center, child: CustomDraggingHandle()), - const SizedBox(height: 12), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text('add_to_album'.tr(), style: context.textTheme.displayMedium), - TextButton.icon( - icon: Icon(Icons.add, color: context.primaryColor), - label: Text('common_create_new_album'.tr(), style: TextStyle(color: context.primaryColor)), - onPressed: () { - context.pushRoute(CreateAlbumRoute(assets: assets)); - }, - ), - ], - ), - ], - ), - ), - ), - SliverPadding( - padding: const EdgeInsets.symmetric(horizontal: 16), - sliver: AddToAlbumSliverList( - albums: albums, - sharedAlbums: albums.where((a) => a.shared).toList(), - onAddToAlbum: addToAlbum, - ), - ), - ], - ), - ); - } -} diff --git a/mobile/lib/widgets/album/add_to_album_sliverlist.dart b/mobile/lib/widgets/album/add_to_album_sliverlist.dart deleted file mode 100644 index defbd90388..0000000000 --- a/mobile/lib/widgets/album/add_to_album_sliverlist.dart +++ /dev/null @@ -1,65 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/providers/album/album_sort_by_options.provider.dart'; -import 'package:immich_mobile/widgets/album/album_thumbnail_listtile.dart'; -import 'package:immich_mobile/entities/album.entity.dart'; - -class AddToAlbumSliverList extends HookConsumerWidget { - /// The asset to add to an album - final List albums; - final List sharedAlbums; - final void Function(Album) onAddToAlbum; - final bool enabled; - - const AddToAlbumSliverList({ - super.key, - required this.onAddToAlbum, - required this.albums, - required this.sharedAlbums, - this.enabled = true, - }); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final albumSortMode = ref.watch(albumSortByOptionsProvider); - final albumSortIsReverse = ref.watch(albumSortOrderProvider); - final sortedAlbums = albumSortMode.sortFn(albums, albumSortIsReverse); - final sortedSharedAlbums = albumSortMode.sortFn(sharedAlbums, albumSortIsReverse); - - return SliverList( - delegate: SliverChildBuilderDelegate(childCount: albums.length + (sharedAlbums.isEmpty ? 0 : 1), ( - context, - index, - ) { - // Build shared expander - if (index == 0 && sortedSharedAlbums.isNotEmpty) { - return Padding( - padding: const EdgeInsets.only(bottom: 8), - child: ExpansionTile( - title: Text('shared'.tr()), - tilePadding: const EdgeInsets.symmetric(horizontal: 10.0), - leading: const Icon(Icons.group), - children: [ - ListView.builder( - shrinkWrap: true, - physics: const ClampingScrollPhysics(), - itemCount: sortedSharedAlbums.length, - itemBuilder: (context, index) => AlbumThumbnailListTile( - album: sortedSharedAlbums[index], - onTap: enabled ? () => onAddToAlbum(sortedSharedAlbums[index]) : () {}, - ), - ), - ], - ), - ); - } - - // Build albums list - final offset = index - (sharedAlbums.isNotEmpty ? 1 : 0); - final album = sortedAlbums[offset]; - return AlbumThumbnailListTile(album: album, onTap: enabled ? () => onAddToAlbum(album) : () {}); - }), - ); - } -} diff --git a/mobile/lib/widgets/album/album_thumbnail_card.dart b/mobile/lib/widgets/album/album_thumbnail_card.dart deleted file mode 100644 index 6c56f5d843..0000000000 --- a/mobile/lib/widgets/album/album_thumbnail_card.dart +++ /dev/null @@ -1,111 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/album.entity.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/extensions/theme_extensions.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; -import 'package:immich_mobile/providers/user.provider.dart'; -import 'package:immich_mobile/widgets/common/immich_thumbnail.dart'; - -class AlbumThumbnailCard extends ConsumerWidget { - final Function()? onTap; - - /// Whether or not to show the owner of the album (or "Owned") - /// in the subtitle of the album - final bool showOwner; - final bool showTitle; - - const AlbumThumbnailCard({super.key, required this.album, this.onTap, this.showOwner = false, this.showTitle = true}); - - final Album album; - - @override - Widget build(BuildContext context, WidgetRef ref) { - return LayoutBuilder( - builder: (context, constraints) { - var cardSize = constraints.maxWidth; - - buildEmptyThumbnail() { - return Container( - height: cardSize, - width: cardSize, - decoration: BoxDecoration(color: context.colorScheme.surfaceContainerHigh), - child: Center( - child: Icon(Icons.no_photography, size: cardSize * .15, color: context.colorScheme.primary), - ), - ); - } - - buildAlbumThumbnail() => ImmichThumbnail(asset: album.thumbnail.value, width: cardSize, height: cardSize); - - buildAlbumTextRow() { - // Add the owner name to the subtitle - String? owner; - if (showOwner) { - if (album.ownerId == ref.read(currentUserProvider)?.id) { - owner = 'owned'.tr(); - } else if (album.ownerName != null) { - owner = 'shared_by_user'.t(context: context, args: {'user': album.ownerName!}); - } - } - - return Text.rich( - TextSpan( - children: [ - TextSpan( - text: 'items_count'.t(context: context, args: {'count': album.assetCount}), - ), - if (owner != null) const TextSpan(text: ' • '), - if (owner != null) TextSpan(text: owner), - ], - style: context.textTheme.bodyMedium?.copyWith(color: context.colorScheme.onSurfaceSecondary), - ), - overflow: TextOverflow.fade, - ); - } - - return GestureDetector( - onTap: onTap, - child: Flex( - direction: Axis.vertical, - children: [ - Flexible( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox( - width: cardSize, - height: cardSize, - child: ClipRRect( - borderRadius: const BorderRadius.all(Radius.circular(20)), - child: album.thumbnail.value == null ? buildEmptyThumbnail() : buildAlbumThumbnail(), - ), - ), - if (showTitle) ...[ - Padding( - padding: const EdgeInsets.only(top: 8.0), - child: SizedBox( - width: cardSize, - child: Text( - album.name, - overflow: TextOverflow.ellipsis, - style: context.textTheme.titleSmall?.copyWith( - color: context.colorScheme.onSurface, - fontWeight: FontWeight.w500, - ), - ), - ), - ), - buildAlbumTextRow(), - ], - ], - ), - ), - ], - ), - ); - }, - ); - } -} diff --git a/mobile/lib/widgets/album/album_thumbnail_listtile.dart b/mobile/lib/widgets/album/album_thumbnail_listtile.dart deleted file mode 100644 index 386084b034..0000000000 --- a/mobile/lib/widgets/album/album_thumbnail_listtile.dart +++ /dev/null @@ -1,93 +0,0 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:immich_mobile/entities/album.entity.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; -import 'package:immich_mobile/presentation/widgets/images/remote_image_provider.dart'; -import 'package:immich_mobile/presentation/widgets/images/thumbnail.widget.dart'; -import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/utils/image_url_builder.dart'; -import 'package:openapi/api.dart'; - -class AlbumThumbnailListTile extends StatelessWidget { - const AlbumThumbnailListTile({super.key, required this.album, this.onTap}); - - final Album album; - final void Function()? onTap; - - @override - Widget build(BuildContext context) { - var cardSize = 68.0; - - buildEmptyThumbnail() { - return Container( - decoration: BoxDecoration(color: context.isDarkTheme ? Colors.grey[800] : Colors.grey[200]), - child: SizedBox( - height: cardSize, - width: cardSize, - child: const Center(child: Icon(Icons.no_photography)), - ), - ); - } - - buildAlbumThumbnail() { - return SizedBox( - width: cardSize, - height: cardSize, - child: Thumbnail( - imageProvider: RemoteImageProvider(url: getAlbumThumbnailUrl(album, type: AssetMediaSize.thumbnail)), - ), - ); - } - - return GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: - onTap ?? - () { - context.pushRoute(AlbumViewerRoute(albumId: album.id)); - }, - child: Padding( - padding: const EdgeInsets.only(bottom: 12.0), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - ClipRRect( - borderRadius: const BorderRadius.all(Radius.circular(8)), - child: album.thumbnail.value == null ? buildEmptyThumbnail() : buildAlbumThumbnail(), - ), - Expanded( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 8.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - album.name, - overflow: TextOverflow.ellipsis, - style: const TextStyle(fontWeight: FontWeight.bold), - ), - Row( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - 'items_count'.t(context: context, args: {'count': album.assetCount}), - style: const TextStyle(fontSize: 12), - ), - if (album.shared) ...[ - const Text(' • ', style: TextStyle(fontSize: 12)), - Text('shared'.tr(), style: const TextStyle(fontSize: 12)), - ], - ], - ), - ], - ), - ), - ), - ], - ), - ), - ); - } -} diff --git a/mobile/lib/widgets/album/album_title_text_field.dart b/mobile/lib/widgets/album/album_title_text_field.dart deleted file mode 100644 index 0a7438b7ae..0000000000 --- a/mobile/lib/widgets/album/album_title_text_field.dart +++ /dev/null @@ -1,74 +0,0 @@ -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/album/album_title.provider.dart'; - -class AlbumTitleTextField extends ConsumerWidget { - const AlbumTitleTextField({ - super.key, - required this.isAlbumTitleEmpty, - required this.albumTitleTextFieldFocusNode, - required this.albumTitleController, - required this.isAlbumTitleTextFieldFocus, - }); - - final ValueNotifier isAlbumTitleEmpty; - final FocusNode albumTitleTextFieldFocusNode; - final TextEditingController albumTitleController; - final ValueNotifier isAlbumTitleTextFieldFocus; - - @override - Widget build(BuildContext context, WidgetRef ref) { - return TextField( - onChanged: (v) { - if (v.isEmpty) { - isAlbumTitleEmpty.value = true; - } else { - isAlbumTitleEmpty.value = false; - } - - ref.watch(albumTitleProvider.notifier).setAlbumTitle(v); - }, - focusNode: albumTitleTextFieldFocusNode, - style: TextStyle(fontSize: 28, color: context.colorScheme.onSurface, fontWeight: FontWeight.bold), - controller: albumTitleController, - onTap: () { - isAlbumTitleTextFieldFocus.value = true; - - if (albumTitleController.text == 'Untitled') { - albumTitleController.clear(); - } - }, - decoration: InputDecoration( - contentPadding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8), - suffixIcon: !isAlbumTitleEmpty.value && isAlbumTitleTextFieldFocus.value - ? IconButton( - onPressed: () { - albumTitleController.clear(); - isAlbumTitleEmpty.value = true; - }, - icon: Icon(Icons.cancel_rounded, color: context.primaryColor), - splashRadius: 10, - ) - : null, - enabledBorder: const OutlineInputBorder( - borderSide: BorderSide(color: Colors.transparent), - borderRadius: BorderRadius.all(Radius.circular(10)), - ), - focusedBorder: const OutlineInputBorder( - borderSide: BorderSide(color: Colors.transparent), - borderRadius: BorderRadius.all(Radius.circular(10)), - ), - hintText: 'add_a_title'.tr(), - hintStyle: context.themeData.inputDecorationTheme.hintStyle?.copyWith( - fontSize: 28, - fontWeight: FontWeight.bold, - ), - focusColor: Colors.grey[300], - fillColor: context.colorScheme.surfaceContainerHigh, - filled: isAlbumTitleTextFieldFocus.value, - ), - ); - } -} diff --git a/mobile/lib/widgets/album/album_viewer_appbar.dart b/mobile/lib/widgets/album/album_viewer_appbar.dart deleted file mode 100644 index 4fd4b31013..0000000000 --- a/mobile/lib/widgets/album/album_viewer_appbar.dart +++ /dev/null @@ -1,307 +0,0 @@ -import 'dart:async'; - -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:fluttertoast/fluttertoast.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.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_viewer.provider.dart'; -import 'package:immich_mobile/providers/album/current_album.provider.dart'; -import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/widgets/common/immich_toast.dart'; - -class AlbumViewerAppbar extends HookConsumerWidget implements PreferredSizeWidget { - const AlbumViewerAppbar({ - super.key, - required this.userId, - required this.titleFocusNode, - required this.descriptionFocusNode, - this.onAddPhotos, - this.onAddUsers, - required this.onActivities, - }); - - final String userId; - final FocusNode titleFocusNode; - final FocusNode descriptionFocusNode; - final void Function()? onAddPhotos; - final void Function()? onAddUsers; - final void Function() onActivities; - - @override - Widget build(BuildContext context, WidgetRef ref) { - final albumState = useState(ref.read(currentAlbumProvider)); - final album = albumState.value; - ref.listen(currentAlbumProvider, (_, newAlbum) { - final oldAlbum = albumState.value; - if (oldAlbum != null && newAlbum != null && oldAlbum.id == newAlbum.id) { - return; - } - - albumState.value = newAlbum; - }); - - if (album == null) { - return const SizedBox(); - } - - final albumViewer = ref.watch(albumViewerProvider); - final newAlbumTitle = albumViewer.editTitleText; - final newAlbumDescription = albumViewer.editDescriptionText; - final isEditAlbum = albumViewer.isEditAlbum; - - final comments = album.shared ? ref.watch(activityStatisticsProvider(album.remoteId!)) : 0; - - deleteAlbum() async { - final bool success = await ref.watch(albumProvider.notifier).deleteAlbum(album); - - unawaited(context.navigateTo(const TabControllerRoute(children: [AlbumsRoute()]))); - - if (!success) { - ImmichToast.show( - context: context, - msg: "album_viewer_appbar_share_err_delete".tr(), - toastType: ToastType.error, - gravity: ToastGravity.BOTTOM, - ); - } - } - - Future onDeleteAlbumPressed() { - return showDialog( - context: context, - barrierDismissible: false, // user must tap button! - builder: (BuildContext context) { - return AlertDialog( - title: const Text('delete_album').tr(), - content: const Text('album_viewer_appbar_delete_confirm').tr(), - actions: [ - TextButton( - onPressed: () => context.pop('Cancel'), - child: Text( - 'cancel', - style: TextStyle(color: context.primaryColor, fontWeight: FontWeight.bold), - ).tr(), - ), - TextButton( - onPressed: () { - context.pop('Confirm'); - deleteAlbum(); - }, - child: Text( - 'confirm', - style: TextStyle(fontWeight: FontWeight.bold, color: context.colorScheme.error), - ).tr(), - ), - ], - ); - }, - ); - } - - void onLeaveAlbumPressed() async { - bool isSuccess = await ref.watch(albumProvider.notifier).leaveAlbum(album); - - if (isSuccess) { - unawaited(context.navigateTo(const TabControllerRoute(children: [AlbumsRoute()]))); - } else { - context.pop(); - ImmichToast.show( - context: context, - msg: "album_viewer_appbar_share_err_leave".tr(), - toastType: ToastType.error, - gravity: ToastGravity.BOTTOM, - ); - } - } - - buildBottomSheetActions() { - return [ - album.ownerId == userId - ? ListTile( - leading: const Icon(Icons.delete_forever_rounded), - title: const Text('delete_album', style: TextStyle(fontWeight: FontWeight.w500)).tr(), - onTap: onDeleteAlbumPressed, - ) - : ListTile( - leading: const Icon(Icons.person_remove_rounded), - title: const Text( - 'album_viewer_appbar_share_leave', - style: TextStyle(fontWeight: FontWeight.w500), - ).tr(), - onTap: onLeaveAlbumPressed, - ), - ]; - // } - } - - void onSortOrderToggled() async { - final updatedAlbum = await ref.read(albumProvider.notifier).toggleSortOrder(album); - - if (updatedAlbum == null) { - ImmichToast.show( - context: context, - msg: "error_change_sort_album".tr(), - toastType: ToastType.error, - gravity: ToastGravity.BOTTOM, - ); - } - - context.pop(); - } - - void buildBottomSheet() { - final ownerActions = [ - ListTile( - leading: const Icon(Icons.person_add_alt_rounded), - onTap: () { - context.pop(); - final onAddUsers = this.onAddUsers; - if (onAddUsers != null) { - onAddUsers(); - } - }, - title: const Text("album_viewer_page_share_add_users", style: TextStyle(fontWeight: FontWeight.w500)).tr(), - ), - ListTile( - leading: const Icon(Icons.swap_vert_rounded), - onTap: onSortOrderToggled, - title: const Text("change_display_order", style: TextStyle(fontWeight: FontWeight.w500)).tr(), - ), - ListTile( - leading: const Icon(Icons.link_rounded), - onTap: () { - context.pushRoute(SharedLinkEditRoute(albumId: album.remoteId)); - context.pop(); - }, - title: const Text("control_bottom_app_bar_share_link", style: TextStyle(fontWeight: FontWeight.w500)).tr(), - ), - ListTile( - leading: const Icon(Icons.settings_rounded), - onTap: () => context.navigateTo(const AlbumOptionsRoute()), - title: const Text("options", style: TextStyle(fontWeight: FontWeight.w500)).tr(), - ), - ]; - - final commonActions = [ - ListTile( - leading: const Icon(Icons.add_photo_alternate_outlined), - onTap: () { - context.pop(); - final onAddPhotos = this.onAddPhotos; - if (onAddPhotos != null) { - onAddPhotos(); - } - }, - title: const Text("add_photos", style: TextStyle(fontWeight: FontWeight.w500)).tr(), - ), - ]; - showModalBottomSheet( - backgroundColor: context.scaffoldBackgroundColor, - isScrollControlled: false, - context: context, - builder: (context) { - return SafeArea( - child: Padding( - padding: const EdgeInsets.only(top: 24.0), - child: ListView( - shrinkWrap: true, - children: [ - ...buildBottomSheetActions(), - if (onAddPhotos != null) ...commonActions, - if (onAddPhotos != null && userId == album.ownerId) ...ownerActions, - ], - ), - ), - ); - }, - ); - } - - Widget buildActivitiesButton() { - return IconButton( - onPressed: onActivities, - icon: Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - const Icon(Icons.mode_comment_outlined), - if (comments != 0) - Padding( - padding: const EdgeInsets.only(left: 5), - child: Text( - comments.toString(), - style: TextStyle(fontWeight: FontWeight.bold, color: context.primaryColor), - ), - ), - ], - ), - ); - } - - buildLeadingButton() { - if (isEditAlbum) { - return IconButton( - onPressed: () async { - if (newAlbumTitle.isNotEmpty) { - bool isSuccess = await ref.watch(albumViewerProvider.notifier).changeAlbumTitle(album, newAlbumTitle); - if (!isSuccess) { - ImmichToast.show( - context: context, - msg: "album_viewer_appbar_share_err_title".tr(), - gravity: ToastGravity.BOTTOM, - toastType: ToastType.error, - ); - } - titleFocusNode.unfocus(); - } else if (newAlbumDescription.isNotEmpty) { - bool isSuccessDescription = await ref - .watch(albumViewerProvider.notifier) - .changeAlbumDescription(album, newAlbumDescription); - if (!isSuccessDescription) { - ImmichToast.show( - context: context, - msg: "album_viewer_appbar_share_err_description".tr(), - gravity: ToastGravity.BOTTOM, - toastType: ToastType.error, - ); - } - descriptionFocusNode.unfocus(); - } else { - titleFocusNode.unfocus(); - descriptionFocusNode.unfocus(); - ref.read(albumViewerProvider.notifier).disableEditAlbum(); - } - }, - icon: const Icon(Icons.check_rounded), - splashRadius: 25, - ); - } else { - return IconButton( - onPressed: context.maybePop, - icon: const Icon(Icons.arrow_back_ios_rounded), - splashRadius: 25, - ); - } - } - - return AppBar( - elevation: 0, - backgroundColor: context.scaffoldBackgroundColor, - leading: buildLeadingButton(), - centerTitle: false, - actions: [ - if (album.shared && (album.activityEnabled || comments != 0)) buildActivitiesButton(), - if (album.isRemote) ...[ - IconButton(splashRadius: 25, onPressed: buildBottomSheet, icon: const Icon(Icons.more_horiz_rounded)), - ], - ], - ); - } - - @override - Size get preferredSize => const Size.fromHeight(kToolbarHeight); -} diff --git a/mobile/lib/widgets/album/album_viewer_editable_description.dart b/mobile/lib/widgets/album/album_viewer_editable_description.dart deleted file mode 100644 index decd268ff3..0000000000 --- a/mobile/lib/widgets/album/album_viewer_editable_description.dart +++ /dev/null @@ -1,82 +0,0 @@ -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/providers/album/album_viewer.provider.dart'; - -class AlbumViewerEditableDescription extends HookConsumerWidget { - final String albumDescription; - final FocusNode descriptionFocusNode; - const AlbumViewerEditableDescription({super.key, required this.albumDescription, required this.descriptionFocusNode}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final albumViewerState = ref.watch(albumViewerProvider); - - final descriptionTextEditController = useTextEditingController( - text: albumViewerState.isEditAlbum && albumViewerState.editDescriptionText.isNotEmpty - ? albumViewerState.editDescriptionText - : albumDescription, - ); - - void onFocusModeChange() { - if (!descriptionFocusNode.hasFocus && descriptionTextEditController.text.isEmpty) { - ref.watch(albumViewerProvider.notifier).setEditDescriptionText(""); - descriptionTextEditController.text = ""; - } - } - - useEffect(() { - descriptionFocusNode.addListener(onFocusModeChange); - return () { - descriptionFocusNode.removeListener(onFocusModeChange); - }; - }, []); - - return Material( - color: Colors.transparent, - child: TextField( - onChanged: (value) { - if (value.isEmpty) { - } else { - ref.watch(albumViewerProvider.notifier).setEditDescriptionText(value); - } - }, - focusNode: descriptionFocusNode, - style: context.textTheme.bodyLarge, - maxLines: 3, - minLines: 1, - controller: descriptionTextEditController, - onTap: () { - context.focusScope.requestFocus(descriptionFocusNode); - - ref.watch(albumViewerProvider.notifier).setEditDescriptionText(albumDescription); - ref.watch(albumViewerProvider.notifier).enableEditAlbum(); - - if (descriptionTextEditController.text == '') { - descriptionTextEditController.clear(); - } - }, - decoration: InputDecoration( - contentPadding: const EdgeInsets.all(8), - suffixIcon: descriptionFocusNode.hasFocus - ? IconButton( - onPressed: () { - descriptionTextEditController.clear(); - }, - icon: Icon(Icons.cancel_rounded, color: context.primaryColor), - splashRadius: 10, - ) - : null, - enabledBorder: const OutlineInputBorder(borderSide: BorderSide(color: Colors.transparent)), - focusedBorder: const OutlineInputBorder(borderSide: BorderSide(color: Colors.transparent)), - focusColor: Colors.grey[300], - fillColor: context.scaffoldBackgroundColor, - filled: descriptionFocusNode.hasFocus, - hintText: 'add_a_description'.tr(), - ), - ), - ); - } -} diff --git a/mobile/lib/widgets/album/album_viewer_editable_title.dart b/mobile/lib/widgets/album/album_viewer_editable_title.dart deleted file mode 100644 index c84e613017..0000000000 --- a/mobile/lib/widgets/album/album_viewer_editable_title.dart +++ /dev/null @@ -1,81 +0,0 @@ -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/providers/album/album_viewer.provider.dart'; - -class AlbumViewerEditableTitle extends HookConsumerWidget { - final String albumName; - final FocusNode titleFocusNode; - const AlbumViewerEditableTitle({super.key, required this.albumName, required this.titleFocusNode}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final albumViewerState = ref.watch(albumViewerProvider); - - final titleTextEditController = useTextEditingController( - text: albumViewerState.isEditAlbum && albumViewerState.editTitleText.isNotEmpty - ? albumViewerState.editTitleText - : albumName, - ); - - void onFocusModeChange() { - if (!titleFocusNode.hasFocus && titleTextEditController.text.isEmpty) { - ref.watch(albumViewerProvider.notifier).setEditTitleText("Untitled"); - titleTextEditController.text = "Untitled"; - } - } - - useEffect(() { - titleFocusNode.addListener(onFocusModeChange); - return () { - titleFocusNode.removeListener(onFocusModeChange); - }; - }, []); - - return Material( - color: Colors.transparent, - child: TextField( - onChanged: (value) { - if (value.isEmpty) { - } else { - ref.watch(albumViewerProvider.notifier).setEditTitleText(value); - } - }, - focusNode: titleFocusNode, - style: context.textTheme.headlineLarge?.copyWith(fontWeight: FontWeight.w700), - controller: titleTextEditController, - onTap: () { - context.focusScope.requestFocus(titleFocusNode); - - ref.watch(albumViewerProvider.notifier).setEditTitleText(albumName); - ref.watch(albumViewerProvider.notifier).enableEditAlbum(); - - if (titleTextEditController.text == 'Untitled') { - titleTextEditController.clear(); - } - }, - decoration: InputDecoration( - contentPadding: const EdgeInsets.symmetric(horizontal: 8, vertical: 0), - suffixIcon: titleFocusNode.hasFocus - ? IconButton( - onPressed: () { - titleTextEditController.clear(); - }, - icon: Icon(Icons.cancel_rounded, color: context.primaryColor), - splashRadius: 10, - ) - : null, - enabledBorder: const OutlineInputBorder(borderSide: BorderSide(color: Colors.transparent)), - focusedBorder: const OutlineInputBorder(borderSide: BorderSide(color: Colors.transparent)), - focusColor: Colors.grey[300], - fillColor: context.scaffoldBackgroundColor, - filled: titleFocusNode.hasFocus, - hintText: 'add_a_title'.tr(), - hintStyle: context.themeData.inputDecorationTheme.hintStyle?.copyWith(fontSize: 28), - ), - ), - ); - } -} diff --git a/mobile/lib/widgets/album/shared_album_thumbnail_image.dart b/mobile/lib/widgets/album/shared_album_thumbnail_image.dart deleted file mode 100644 index b21e86d145..0000000000 --- a/mobile/lib/widgets/album/shared_album_thumbnail_image.dart +++ /dev/null @@ -1,20 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/widgets/common/immich_thumbnail.dart'; - -class SharedAlbumThumbnailImage extends HookConsumerWidget { - final Asset asset; - - const SharedAlbumThumbnailImage({super.key, required this.asset}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - return GestureDetector( - onTap: () { - // debugPrint("View ${asset.id}"); - }, - child: Stack(children: [ImmichThumbnail(asset: asset, width: 500, height: 500)]), - ); - } -} diff --git a/mobile/lib/widgets/asset_grid/asset_drag_region.dart b/mobile/lib/widgets/asset_grid/asset_drag_region.dart deleted file mode 100644 index 71e55acbd6..0000000000 --- a/mobile/lib/widgets/asset_grid/asset_drag_region.dart +++ /dev/null @@ -1,207 +0,0 @@ -// Based on https://stackoverflow.com/a/52625182 - -import 'dart:async'; - -import 'package:collection/collection.dart'; -import 'package:flutter/gestures.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/rendering.dart'; - -class AssetDragRegion extends StatefulWidget { - final Widget child; - - final void Function(AssetIndex valueKey)? onStart; - final void Function(AssetIndex valueKey)? onAssetEnter; - final void Function()? onEnd; - final void Function()? onScrollStart; - final void Function(ScrollDirection direction)? onScroll; - - const AssetDragRegion({ - super.key, - required this.child, - this.onStart, - this.onAssetEnter, - this.onEnd, - this.onScrollStart, - this.onScroll, - }); - @override - State createState() => _AssetDragRegionState(); -} - -class _AssetDragRegionState extends State { - late AssetIndex? assetUnderPointer; - late AssetIndex? anchorAsset; - - // Scroll related state - static const double scrollOffset = 0.10; - double? topScrollOffset; - double? bottomScrollOffset; - Timer? scrollTimer; - late bool scrollNotified; - - @override - void initState() { - super.initState(); - assetUnderPointer = null; - anchorAsset = null; - scrollNotified = false; - } - - @override - void didChangeDependencies() { - super.didChangeDependencies(); - topScrollOffset = null; - bottomScrollOffset = null; - } - - @override - void dispose() { - scrollTimer?.cancel(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return RawGestureDetector( - gestures: { - _CustomLongPressGestureRecognizer: GestureRecognizerFactoryWithHandlers<_CustomLongPressGestureRecognizer>( - () => _CustomLongPressGestureRecognizer(), - _registerCallbacks, - ), - }, - child: widget.child, - ); - } - - void _registerCallbacks(_CustomLongPressGestureRecognizer recognizer) { - recognizer.onLongPressMoveUpdate = (details) => _onLongPressMove(details); - recognizer.onLongPressStart = (details) => _onLongPressStart(details); - recognizer.onLongPressUp = _onLongPressEnd; - } - - AssetIndex? _getValueKeyAtPositon(Offset position) { - final box = context.findAncestorRenderObjectOfType(); - if (box == null) return null; - - final hitTestResult = BoxHitTestResult(); - final local = box.globalToLocal(position); - if (!box.hitTest(hitTestResult, position: local)) return null; - - return (hitTestResult.path.firstWhereOrNull((hit) => hit.target is _AssetIndexProxy)?.target as _AssetIndexProxy?) - ?.index; - } - - void _onLongPressStart(LongPressStartDetails event) { - /// Calculate widget height and scroll offset when long press starting instead of in [initState] - /// or [didChangeDependencies] as the grid might still be rendering into view to get the actual size - final height = context.size?.height; - if (height != null && (topScrollOffset == null || bottomScrollOffset == null)) { - topScrollOffset = height * scrollOffset; - bottomScrollOffset = height - topScrollOffset!; - } - - final initialHit = _getValueKeyAtPositon(event.globalPosition); - anchorAsset = initialHit; - if (initialHit == null) return; - - if (anchorAsset != null) { - widget.onStart?.call(anchorAsset!); - } - } - - void _onLongPressEnd() { - scrollNotified = false; - scrollTimer?.cancel(); - widget.onEnd?.call(); - } - - void _onLongPressMove(LongPressMoveUpdateDetails event) { - if (anchorAsset == null) return; - if (topScrollOffset == null || bottomScrollOffset == null) return; - - final currentDy = event.localPosition.dy; - - if (currentDy > bottomScrollOffset!) { - scrollTimer ??= Timer.periodic( - const Duration(milliseconds: 50), - (_) => widget.onScroll?.call(ScrollDirection.forward), - ); - } else if (currentDy < topScrollOffset!) { - scrollTimer ??= Timer.periodic( - const Duration(milliseconds: 50), - (_) => widget.onScroll?.call(ScrollDirection.reverse), - ); - } else { - scrollTimer?.cancel(); - scrollTimer = null; - } - - final currentlyTouchingAsset = _getValueKeyAtPositon(event.globalPosition); - if (currentlyTouchingAsset == null) return; - - if (assetUnderPointer != currentlyTouchingAsset) { - if (!scrollNotified) { - scrollNotified = true; - widget.onScrollStart?.call(); - } - - widget.onAssetEnter?.call(currentlyTouchingAsset); - assetUnderPointer = currentlyTouchingAsset; - } - } -} - -class _CustomLongPressGestureRecognizer extends LongPressGestureRecognizer { - @override - void rejectGesture(int pointer) { - acceptGesture(pointer); - } -} - -class AssetIndexWrapper extends SingleChildRenderObjectWidget { - final int rowIndex; - final int sectionIndex; - - const AssetIndexWrapper({required Widget super.child, required this.rowIndex, required this.sectionIndex, super.key}); - - @override - // ignore: library_private_types_in_public_api - _AssetIndexProxy createRenderObject(BuildContext context) { - return _AssetIndexProxy( - index: AssetIndex(rowIndex: rowIndex, sectionIndex: sectionIndex), - ); - } - - @override - void updateRenderObject( - BuildContext context, - // ignore: library_private_types_in_public_api - _AssetIndexProxy renderObject, - ) { - renderObject.index = AssetIndex(rowIndex: rowIndex, sectionIndex: sectionIndex); - } -} - -class _AssetIndexProxy extends RenderProxyBox { - AssetIndex index; - - _AssetIndexProxy({required this.index}); -} - -class AssetIndex { - final int rowIndex; - final int sectionIndex; - - const AssetIndex({required this.rowIndex, required this.sectionIndex}); - - @override - bool operator ==(covariant AssetIndex other) { - if (identical(this, other)) return true; - - return other.rowIndex == rowIndex && other.sectionIndex == sectionIndex; - } - - @override - int get hashCode => rowIndex.hashCode ^ sectionIndex.hashCode; -} diff --git a/mobile/lib/widgets/asset_grid/asset_grid_data_structure.dart b/mobile/lib/widgets/asset_grid/asset_grid_data_structure.dart deleted file mode 100644 index d95d6efe2e..0000000000 --- a/mobile/lib/widgets/asset_grid/asset_grid_data_structure.dart +++ /dev/null @@ -1,307 +0,0 @@ -import 'dart:math'; - -import 'package:collection/collection.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:isar/isar.dart'; -import 'package:logging/logging.dart'; - -final log = Logger('AssetGridDataStructure'); - -enum RenderAssetGridElementType { assets, assetRow, groupDividerTitle, monthTitle } - -class RenderAssetGridElement { - final RenderAssetGridElementType type; - final String? title; - final DateTime date; - final int count; - final int offset; - final int totalCount; - - const RenderAssetGridElement( - this.type, { - this.title, - required this.date, - this.count = 0, - this.offset = 0, - this.totalCount = 0, - }); -} - -enum GroupAssetsBy { day, month, auto, none } - -class RenderList { - final List elements; - final List? allAssets; - final QueryBuilder? query; - final int totalAssets; - - /// reference to batch of assets loaded from DB with offset [_bufOffset] - List _buf = []; - - /// global offset of assets in [_buf] - int _bufOffset = 0; - - RenderList(this.elements, this.query, this.allAssets) : totalAssets = allAssets?.length ?? query!.countSync(); - - bool get isEmpty => totalAssets == 0; - - /// Loads the requested assets from the database to an internal buffer if not cached - /// and returns a slice of that buffer - List loadAssets(int offset, int count) { - assert(offset >= 0); - assert(count > 0); - assert(offset + count <= totalAssets); - if (allAssets != null) { - // if we already loaded all assets (e.g. from search result) - // simply return the requested slice of that array - return allAssets!.slice(offset, offset + count); - } else if (query != null) { - // general case: we have the query to load assets via offset from the DB on demand - if (offset < _bufOffset || offset + count > _bufOffset + _buf.length) { - // the requested slice (offset:offset+count) is not contained in the cache buffer `_buf` - // thus, fill the buffer with a new batch of assets that at least contains the requested - // assets and some more - - final bool forward = _bufOffset < offset; - // if the requested offset is greater than the cached offset, the user scrolls forward "down" - const batchSize = 256; - const oppositeSize = 64; - - // make sure to load a meaningful amount of data (and not only the requested slice) - // otherwise, each call to [loadAssets] would result in DB call trashing performance - // fills small requests to [batchSize], adds some legroom into the opposite scroll direction for large requests - final len = max(batchSize, count + oppositeSize); - // when scrolling forward, start shortly before the requested offset... - // when scrolling backward, end shortly after the requested offset... - // ... to guard against the user scrolling in the other direction - // a tiny bit resulting in a another required load from the DB - final start = max(0, forward ? offset - oppositeSize : (len > batchSize ? offset : offset + count - len)); - // load the calculated batch (start:start+len) from the DB and put it into the buffer - _buf = query!.offset(start).limit(len).findAllSync(); - _bufOffset = start; - } - assert(_bufOffset <= offset); - assert(_bufOffset + _buf.length >= offset + count); - // return the requested slice from the buffer (we made sure before that the assets are loaded!) - return _buf.slice(offset - _bufOffset, offset - _bufOffset + count); - } - throw Exception("RenderList has neither assets nor query"); - } - - /// Returns the requested asset either from cached buffer or directly from the database - Asset loadAsset(int index) { - if (allAssets != null) { - // all assets are already loaded (e.g. from search result) - return allAssets![index]; - } else if (query != null) { - // general case: we have the DB query to load asset(s) on demand - if (index >= _bufOffset && index < _bufOffset + _buf.length) { - // lucky case: the requested asset is already cached in the buffer! - return _buf[index - _bufOffset]; - } - // request the asset from the database (not changing the buffer!) - final asset = query!.offset(index).findFirstSync(); - if (asset == null) { - throw Exception("Asset at index $index does no longer exist in database"); - } - return asset; - } - throw Exception("RenderList has neither assets nor query"); - } - - static Future fromQuery(QueryBuilder query, GroupAssetsBy groupBy) => - _buildRenderList(null, query, groupBy); - - static Future _buildRenderList( - List? assets, - QueryBuilder? query, - GroupAssetsBy groupBy, - ) async { - final List elements = []; - - const pageSize = 50000; - const sectionSize = 60; // divides evenly by 2,3,4,5,6 - - if (groupBy == GroupAssetsBy.none) { - final int total = assets?.length ?? query!.countSync(); - - final dateLoader = query != null ? DateBatchLoader(query: query, batchSize: 1000 * sectionSize) : null; - - for (int i = 0; i < total; i += sectionSize) { - final date = assets != null ? assets[i].fileCreatedAt : await dateLoader?.getDate(i); - - final int count = i + sectionSize > total ? total - i : sectionSize; - if (date == null) break; - elements.add( - RenderAssetGridElement( - RenderAssetGridElementType.assets, - date: date, - count: count, - totalCount: total, - offset: i, - ), - ); - } - return RenderList(elements, query, assets); - } - - final formatSameYear = groupBy == GroupAssetsBy.month ? DateFormat.MMMM() : DateFormat.MMMEd(); - final formatOtherYear = groupBy == GroupAssetsBy.month ? DateFormat.yMMMM() : DateFormat.yMMMEd(); - final currentYear = DateTime.now().year; - final formatMergedSameYear = DateFormat.MMMd(); - final formatMergedOtherYear = DateFormat.yMMMd(); - - int offset = 0; - DateTime? last; - DateTime? current; - int lastOffset = 0; - int count = 0; - int monthCount = 0; - int lastMonthIndex = 0; - - String formatDateRange(DateTime from, DateTime to) { - final startDate = (from.year == currentYear ? formatMergedSameYear : formatMergedOtherYear).format(from); - final endDate = (to.year == currentYear ? formatMergedSameYear : formatMergedOtherYear).format(to); - if (DateTime(from.year, from.month, from.day) == DateTime(to.year, to.month, to.day)) { - // format range with time when both dates are on the same day - final startTime = DateFormat.Hm().format(from); - final endTime = DateFormat.Hm().format(to); - return "$startDate $startTime - $endTime"; - } - return "$startDate - $endDate"; - } - - void mergeMonth() { - if (last != null && groupBy == GroupAssetsBy.auto && monthCount <= 30 && elements.length > lastMonthIndex + 1) { - // merge all days into a single section - assert(elements[lastMonthIndex].date.month == last.month); - final e = elements[lastMonthIndex]; - - elements[lastMonthIndex] = RenderAssetGridElement( - RenderAssetGridElementType.monthTitle, - date: e.date, - count: monthCount, - totalCount: monthCount, - offset: e.offset, - title: formatDateRange(e.date, elements.last.date), - ); - elements.removeRange(lastMonthIndex + 1, elements.length); - } - } - - void addElems(DateTime d, DateTime? prevDate) { - final bool newMonth = last == null || last.year != d.year || last.month != d.month; - if (newMonth) { - mergeMonth(); - lastMonthIndex = elements.length; - monthCount = 0; - } - for (int j = 0; j < count; j += sectionSize) { - final type = j == 0 - ? (groupBy != GroupAssetsBy.month && newMonth - ? RenderAssetGridElementType.monthTitle - : RenderAssetGridElementType.groupDividerTitle) - : (groupBy == GroupAssetsBy.auto - ? RenderAssetGridElementType.groupDividerTitle - : RenderAssetGridElementType.assets); - final sectionCount = j + sectionSize > count ? count - j : sectionSize; - assert(sectionCount > 0 && sectionCount <= sectionSize); - elements.add( - RenderAssetGridElement( - type, - date: d, - count: sectionCount, - totalCount: groupBy == GroupAssetsBy.auto ? sectionCount : count, - offset: lastOffset + j, - title: j == 0 - ? (d.year == currentYear ? formatSameYear.format(d) : formatOtherYear.format(d)) - : (groupBy == GroupAssetsBy.auto ? formatDateRange(d, prevDate ?? d) : null), - ), - ); - } - monthCount += count; - } - - DateTime? prevDate; - while (true) { - // this iterates all assets (only their createdAt property) in batches - // memory usage is okay, however runtime is linear with number of assets - // TODO replace with groupBy once Isar supports such queries - final dates = assets != null - ? assets.map((a) => a.fileCreatedAt) - : await query!.offset(offset).limit(pageSize).fileCreatedAtProperty().findAll(); - int i = 0; - for (final date in dates) { - final d = DateTime(date.year, date.month, groupBy == GroupAssetsBy.month ? 1 : date.day); - current ??= d; - if (current != d) { - addElems(current, prevDate); - last = current; - current = d; - lastOffset = offset + i; - count = 0; - } - prevDate = date; - count++; - i++; - } - - if (assets != null || dates.length != pageSize) break; - offset += pageSize; - } - if (count > 0 && current != null) { - addElems(current, prevDate); - mergeMonth(); - } - assert(elements.every((e) => e.count <= sectionSize), "too large section"); - return RenderList(elements, query, assets); - } - - static RenderList empty() => RenderList([], null, []); - - static Future fromAssets(List assets, GroupAssetsBy groupBy) => - _buildRenderList(assets, null, groupBy); - - /// Deletes an asset from the render list and clears the buffer - /// This is only a workaround for deleted images still appearing in the gallery - void deleteAsset(Asset deleteAsset) { - allAssets?.remove(deleteAsset); - _buf.clear(); - _bufOffset = 0; - } -} - -class DateBatchLoader { - final QueryBuilder query; - final int batchSize; - - List _buffer = []; - int _bufferStart = 0; - - DateBatchLoader({required this.query, required this.batchSize}); - - Future getDate(int index) async { - if (!_isIndexInBuffer(index)) { - await _loadBatch(index); - } - - if (_isIndexInBuffer(index)) { - return _buffer[index - _bufferStart]; - } - - return null; - } - - Future _loadBatch(int targetIndex) async { - final batchStart = (targetIndex ~/ batchSize) * batchSize; - - _buffer = await query.offset(batchStart).limit(batchSize).fileCreatedAtProperty().findAll(); - - _bufferStart = batchStart; - } - - bool _isIndexInBuffer(int index) { - return index >= _bufferStart && index < _bufferStart + _buffer.length; - } -} diff --git a/mobile/lib/widgets/asset_grid/control_bottom_app_bar.dart b/mobile/lib/widgets/asset_grid/control_bottom_app_bar.dart deleted file mode 100644 index cd2dc70dae..0000000000 --- a/mobile/lib/widgets/asset_grid/control_bottom_app_bar.dart +++ /dev/null @@ -1,388 +0,0 @@ -import 'dart:io'; - -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/providers/album/album.provider.dart'; -import 'package:immich_mobile/providers/routes.provider.dart'; -import 'package:immich_mobile/widgets/album/add_to_album_sliverlist.dart'; -import 'package:immich_mobile/widgets/album/add_to_album_bottom_sheet.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/upload_dialog.dart'; -import 'package:immich_mobile/providers/server_info.provider.dart'; -import 'package:immich_mobile/widgets/common/drag_sheet.dart'; -import 'package:immich_mobile/entities/album.entity.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/utils/draggable_scroll_controller.dart'; - -final controlBottomAppBarNotifier = ControlBottomAppBarNotifier(); - -class ControlBottomAppBarNotifier with ChangeNotifier { - void minimize() { - notifyListeners(); - } -} - -class ControlBottomAppBar extends HookConsumerWidget { - final void Function(bool shareLocal) onShare; - final void Function()? onFavorite; - final void Function()? onArchive; - final void Function([bool force])? onDelete; - final void Function([bool force])? onDeleteServer; - final void Function(bool onlyBackedUp)? onDeleteLocal; - final Function(Album album) onAddToAlbum; - final void Function() onCreateNewAlbum; - final void Function() onUpload; - final void Function()? onStack; - final void Function()? onEditTime; - final void Function()? onEditLocation; - final void Function()? onRemoveFromAlbum; - final void Function()? onToggleLocked; - final void Function()? onDownload; - - final bool enabled; - final bool unfavorite; - final bool unarchive; - final AssetSelectionState selectionAssetState; - final List selectedAssets; - - const ControlBottomAppBar({ - super.key, - required this.onShare, - this.onFavorite, - this.onArchive, - this.onDelete, - this.onDeleteServer, - this.onDeleteLocal, - required this.onAddToAlbum, - required this.onCreateNewAlbum, - required this.onUpload, - this.onDownload, - this.onStack, - this.onEditTime, - this.onEditLocation, - this.onRemoveFromAlbum, - this.onToggleLocked, - this.selectionAssetState = const AssetSelectionState(), - this.selectedAssets = const [], - this.enabled = true, - this.unarchive = false, - this.unfavorite = false, - }); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final hasRemote = selectionAssetState.hasRemote || selectionAssetState.hasMerged; - final hasLocal = selectionAssetState.hasLocal || selectionAssetState.hasMerged; - final trashEnabled = ref.watch(serverInfoProvider.select((v) => v.serverFeatures.trash)); - final albums = ref.watch(albumProvider).where((a) => a.isRemote).toList(); - final sharedAlbums = ref.watch(albumProvider).where((a) => a.shared).toList(); - const bottomPadding = 0.24; - final scrollController = useDraggableScrollController(); - final isInLockedView = ref.watch(inLockedViewProvider); - - void minimize() { - scrollController.animateTo(bottomPadding, duration: const Duration(milliseconds: 300), curve: Curves.easeOut); - } - - useEffect(() { - controlBottomAppBarNotifier.addListener(minimize); - return () { - controlBottomAppBarNotifier.removeListener(minimize); - }; - }, []); - - void showForceDeleteDialog(Function(bool) deleteCb, {String? alertMsg}) { - showDialog( - context: context, - builder: (BuildContext context) { - return DeleteDialog(alert: alertMsg, onDelete: () => deleteCb(true)); - }, - ); - } - - /// Show existing AddToAlbumBottomSheet - void showAddToAlbumBottomSheet() { - showModalBottomSheet( - elevation: 0, - shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(15.0))), - context: context, - builder: (BuildContext _) { - return AddToAlbumBottomSheet(assets: selectedAssets); - }, - ); - } - - void handleRemoteDelete(bool force, Function(bool) deleteCb, {String? alertMsg}) { - if (!force) { - deleteCb(force); - return; - } - return showForceDeleteDialog(deleteCb, alertMsg: alertMsg); - } - - List renderActionButtons() { - return [ - ControlBoxButton( - iconData: Platform.isAndroid ? Icons.share_rounded : Icons.ios_share_rounded, - label: "share".tr(), - onPressed: enabled ? () => onShare(true) : null, - ), - if (!isInLockedView && hasRemote) - ControlBoxButton( - iconData: Icons.link_rounded, - label: "share_link".tr(), - onPressed: enabled ? () => onShare(false) : null, - ), - if (!isInLockedView && hasRemote && albums.isNotEmpty) - ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 100), - child: ControlBoxButton( - iconData: Icons.photo_album, - label: "add_to_album".tr(), - onPressed: enabled ? showAddToAlbumBottomSheet : null, - ), - ), - if (hasRemote && onArchive != null) - ControlBoxButton( - iconData: unarchive ? Icons.unarchive_outlined : Icons.archive_outlined, - label: (unarchive ? "unarchive" : "archive").tr(), - onPressed: enabled ? onArchive : null, - ), - if (hasRemote && onFavorite != null) - ControlBoxButton( - iconData: unfavorite ? Icons.favorite_border_rounded : Icons.favorite_rounded, - label: (unfavorite ? "unfavorite" : "favorite").tr(), - onPressed: enabled ? onFavorite : null, - ), - if (hasRemote && onDownload != null) - ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 90), - child: ControlBoxButton(iconData: Icons.download, label: "download".tr(), onPressed: onDownload), - ), - if (hasLocal && hasRemote && onDelete != null && !isInLockedView) - ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 90), - child: ControlBoxButton( - iconData: Icons.delete_sweep_outlined, - label: "delete".tr(), - onPressed: enabled ? () => handleRemoteDelete(!trashEnabled, onDelete!) : null, - onLongPressed: enabled ? () => showForceDeleteDialog(onDelete!) : null, - ), - ), - if (hasRemote && onDeleteServer != null && !isInLockedView) - ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 85), - child: ControlBoxButton( - iconData: Icons.cloud_off_outlined, - label: trashEnabled - ? "control_bottom_app_bar_trash_from_immich".tr() - : "control_bottom_app_bar_delete_from_immich".tr(), - onPressed: enabled - ? () => handleRemoteDelete(!trashEnabled, onDeleteServer!, alertMsg: "delete_dialog_alert_remote") - : null, - onLongPressed: enabled - ? () => showForceDeleteDialog(onDeleteServer!, alertMsg: "delete_dialog_alert_remote") - : null, - ), - ), - if (isInLockedView) - ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 110), - child: ControlBoxButton( - iconData: Icons.delete_forever, - label: "delete_dialog_title".tr(), - onPressed: enabled - ? () => showForceDeleteDialog(onDeleteServer!, alertMsg: "delete_dialog_alert_remote") - : null, - ), - ), - if (hasLocal && onDeleteLocal != null && !isInLockedView) - ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 95), - child: ControlBoxButton( - iconData: Icons.no_cell_outlined, - label: "control_bottom_app_bar_delete_from_local".tr(), - onPressed: enabled - ? () { - if (!selectionAssetState.hasLocal) { - return onDeleteLocal?.call(true); - } - - showDialog( - context: context, - builder: (BuildContext context) { - return DeleteLocalOnlyDialog(onDeleteLocal: onDeleteLocal!); - }, - ); - } - : null, - ), - ), - if (hasRemote && onEditTime != null) - ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 95), - child: ControlBoxButton( - iconData: Icons.edit_calendar_outlined, - label: "control_bottom_app_bar_edit_time".tr(), - onPressed: enabled ? onEditTime : null, - ), - ), - if (hasRemote && onEditLocation != null) - ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 90), - child: ControlBoxButton( - iconData: Icons.edit_location_alt_outlined, - label: "control_bottom_app_bar_edit_location".tr(), - onPressed: enabled ? onEditLocation : null, - ), - ), - if (hasRemote) - ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 100), - child: ControlBoxButton( - iconData: isInLockedView ? Icons.lock_open_rounded : Icons.lock_outline_rounded, - label: isInLockedView ? "remove_from_locked_folder".tr() : "move_to_locked_folder".tr(), - onPressed: enabled ? onToggleLocked : null, - ), - ), - if (!selectionAssetState.hasLocal && selectionAssetState.selectedCount > 1 && onStack != null) - ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 90), - child: ControlBoxButton( - iconData: Icons.filter_none_rounded, - label: "stack".tr(), - onPressed: enabled ? onStack : null, - ), - ), - if (onRemoveFromAlbum != null) - ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 90), - child: ControlBoxButton( - iconData: Icons.remove_circle_outline, - label: 'remove_from_album'.tr(), - onPressed: enabled ? onRemoveFromAlbum : null, - ), - ), - if (selectionAssetState.hasLocal) - ControlBoxButton( - iconData: Icons.backup_outlined, - label: "upload".tr(), - onPressed: enabled - ? () => showDialog( - context: context, - builder: (BuildContext context) { - return UploadDialog(onUpload: onUpload); - }, - ) - : null, - ), - ]; - } - - getInitialSize() { - if (isInLockedView) { - return bottomPadding; - } - if (hasRemote) { - return 0.35; - } - return bottomPadding; - } - - getMaxChildSize() { - if (isInLockedView) { - return bottomPadding; - } - if (hasRemote) { - return 0.65; - } - return bottomPadding; - } - - return DraggableScrollableSheet( - initialChildSize: getInitialSize(), - minChildSize: bottomPadding, - maxChildSize: getMaxChildSize(), - snap: true, - controller: scrollController, - builder: (BuildContext context, ScrollController scrollController) { - return Card( - color: context.colorScheme.surfaceContainerHigh, - surfaceTintColor: context.colorScheme.surfaceContainerHigh, - elevation: 6.0, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.only(topLeft: Radius.circular(12), topRight: Radius.circular(12)), - ), - margin: const EdgeInsets.all(0), - child: CustomScrollView( - controller: scrollController, - slivers: [ - SliverToBoxAdapter( - child: Column( - children: [ - const SizedBox(height: 12), - const CustomDraggingHandle(), - const SizedBox(height: 12), - SizedBox( - height: 120, - child: ListView( - shrinkWrap: true, - scrollDirection: Axis.horizontal, - children: renderActionButtons(), - ), - ), - if (hasRemote && !isInLockedView) ...[ - const Divider(indent: 16, endIndent: 16, thickness: 1), - _AddToAlbumTitleRow(onCreateNewAlbum: enabled ? onCreateNewAlbum : null), - ], - ], - ), - ), - if (hasRemote && !isInLockedView) - SliverPadding( - padding: const EdgeInsets.symmetric(horizontal: 16), - sliver: AddToAlbumSliverList( - albums: albums, - sharedAlbums: sharedAlbums, - onAddToAlbum: onAddToAlbum, - enabled: enabled, - ), - ), - ], - ), - ); - }, - ); - } -} - -class _AddToAlbumTitleRow extends StatelessWidget { - const _AddToAlbumTitleRow({required this.onCreateNewAlbum}); - - final VoidCallback? onCreateNewAlbum; - - @override - Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text("add_to_album", style: context.textTheme.titleSmall).tr(), - TextButton.icon( - onPressed: onCreateNewAlbum, - icon: Icon(Icons.add, color: context.primaryColor), - label: Text( - "common_create_new_album", - style: TextStyle(color: context.primaryColor, fontWeight: FontWeight.bold, fontSize: 14), - ).tr(), - ), - ], - ), - ); - } -} diff --git a/mobile/lib/widgets/asset_grid/delete_dialog.dart b/mobile/lib/widgets/asset_grid/delete_dialog.dart index adb22889a8..ff5aac617a 100644 --- a/mobile/lib/widgets/asset_grid/delete_dialog.dart +++ b/mobile/lib/widgets/asset_grid/delete_dialog.dart @@ -1,18 +1,6 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/widgets/common/confirm_dialog.dart'; - -class DeleteDialog extends ConfirmDialog { - const DeleteDialog({super.key, String? alert, required Function onDelete}) - : super( - title: "delete_dialog_title", - content: alert ?? "delete_dialog_alert", - cancel: "cancel", - ok: "delete", - onOk: onDelete, - ); -} class DeleteLocalOnlyDialog extends StatelessWidget { final void Function(bool onlyMerged) onDeleteLocal; diff --git a/mobile/lib/widgets/asset_grid/disable_multi_select_button.dart b/mobile/lib/widgets/asset_grid/disable_multi_select_button.dart deleted file mode 100644 index 93a1d53f4e..0000000000 --- a/mobile/lib/widgets/asset_grid/disable_multi_select_button.dart +++ /dev/null @@ -1,31 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; - -class DisableMultiSelectButton extends ConsumerWidget { - const DisableMultiSelectButton({super.key, required this.onPressed, required this.selectedItemCount}); - - final Function onPressed; - final int selectedItemCount; - - @override - Widget build(BuildContext context, WidgetRef ref) { - return Align( - alignment: Alignment.topLeft, - child: Padding( - padding: const EdgeInsets.only(left: 16.0, top: 8.0), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 4.0), - child: ElevatedButton.icon( - onPressed: () => onPressed(), - icon: Icon(Icons.close_rounded, color: context.colorScheme.onPrimary), - label: Text( - '$selectedItemCount', - style: context.textTheme.titleMedium?.copyWith(height: 2.5, color: context.colorScheme.onPrimary), - ), - ), - ), - ), - ); - } -} diff --git a/mobile/lib/widgets/asset_grid/draggable_scrollbar.dart b/mobile/lib/widgets/asset_grid/draggable_scrollbar.dart deleted file mode 100644 index 3de52c2816..0000000000 --- a/mobile/lib/widgets/asset_grid/draggable_scrollbar.dart +++ /dev/null @@ -1,559 +0,0 @@ -import 'dart:async'; - -import 'package:flutter/material.dart'; - -/// Build the Scroll Thumb and label using the current configuration -typedef ScrollThumbBuilder = - Widget Function( - Color backgroundColor, - Animation thumbAnimation, - Animation labelAnimation, - double height, { - Text? labelText, - BoxConstraints? labelConstraints, - }); - -/// Build a Text widget using the current scroll offset -typedef LabelTextBuilder = Text Function(double offsetY); - -/// A widget that will display a BoxScrollView with a ScrollThumb that can be dragged -/// for quick navigation of the BoxScrollView. -class DraggableScrollbar extends StatefulWidget { - /// The view that will be scrolled with the scroll thumb - final CustomScrollView child; - - /// A function that builds a thumb using the current configuration - final ScrollThumbBuilder scrollThumbBuilder; - - /// The height of the scroll thumb - final double heightScrollThumb; - - /// The background color of the label and thumb - final Color backgroundColor; - - /// The amount of padding that should surround the thumb - final EdgeInsetsGeometry? padding; - - /// Determines how quickly the scrollbar will animate in and out - final Duration scrollbarAnimationDuration; - - /// How long should the thumb be visible before fading out - final Duration scrollbarTimeToFade; - - /// Build a Text widget from the current offset in the BoxScrollView - final LabelTextBuilder? labelTextBuilder; - - /// Determines box constraints for Container displaying label - final BoxConstraints? labelConstraints; - - /// The ScrollController for the BoxScrollView - final ScrollController controller; - - /// Determines scrollThumb displaying. If you draw own ScrollThumb and it is true you just don't need to use animation parameters in [scrollThumbBuilder] - final bool alwaysVisibleScrollThumb; - - DraggableScrollbar({ - super.key, - this.alwaysVisibleScrollThumb = false, - required this.heightScrollThumb, - required this.backgroundColor, - required this.scrollThumbBuilder, - required this.child, - required this.controller, - this.padding, - this.scrollbarAnimationDuration = const Duration(milliseconds: 300), - this.scrollbarTimeToFade = const Duration(milliseconds: 600), - this.labelTextBuilder, - this.labelConstraints, - }) : assert(child.scrollDirection == Axis.vertical); - - DraggableScrollbar.rrect({ - super.key, - Key? scrollThumbKey, - this.alwaysVisibleScrollThumb = false, - required this.child, - required this.controller, - this.heightScrollThumb = 48.0, - this.backgroundColor = Colors.white, - this.padding, - this.scrollbarAnimationDuration = const Duration(milliseconds: 300), - this.scrollbarTimeToFade = const Duration(milliseconds: 600), - this.labelTextBuilder, - this.labelConstraints, - }) : assert(child.scrollDirection == Axis.vertical), - scrollThumbBuilder = _thumbRRectBuilder(alwaysVisibleScrollThumb); - - DraggableScrollbar.arrows({ - super.key, - Key? scrollThumbKey, - this.alwaysVisibleScrollThumb = false, - required this.child, - required this.controller, - this.heightScrollThumb = 48.0, - this.backgroundColor = Colors.white, - this.padding, - this.scrollbarAnimationDuration = const Duration(milliseconds: 300), - this.scrollbarTimeToFade = const Duration(milliseconds: 600), - this.labelTextBuilder, - this.labelConstraints, - }) : assert(child.scrollDirection == Axis.vertical), - scrollThumbBuilder = _thumbArrowBuilder(alwaysVisibleScrollThumb); - - DraggableScrollbar.semicircle({ - super.key, - Key? scrollThumbKey, - this.alwaysVisibleScrollThumb = false, - required this.child, - required this.controller, - this.heightScrollThumb = 48.0, - this.backgroundColor = Colors.white, - this.padding, - this.scrollbarAnimationDuration = const Duration(milliseconds: 300), - this.scrollbarTimeToFade = const Duration(milliseconds: 600), - this.labelTextBuilder, - this.labelConstraints, - }) : assert(child.scrollDirection == Axis.vertical), - scrollThumbBuilder = _thumbSemicircleBuilder(heightScrollThumb * 0.6, scrollThumbKey, alwaysVisibleScrollThumb); - - @override - DraggableScrollbarState createState() => DraggableScrollbarState(); - - static buildScrollThumbAndLabel({ - required Widget scrollThumb, - required Color backgroundColor, - required Animation? thumbAnimation, - required Animation? labelAnimation, - required Text? labelText, - required BoxConstraints? labelConstraints, - required bool alwaysVisibleScrollThumb, - }) { - var scrollThumbAndLabel = labelText == null - ? scrollThumb - : Row( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.end, - children: [ - ScrollLabel( - animation: labelAnimation, - backgroundColor: backgroundColor, - constraints: labelConstraints, - child: labelText, - ), - scrollThumb, - ], - ); - - if (alwaysVisibleScrollThumb) { - return scrollThumbAndLabel; - } - return SlideFadeTransition(animation: thumbAnimation!, child: scrollThumbAndLabel); - } - - static ScrollThumbBuilder _thumbSemicircleBuilder(double width, Key? scrollThumbKey, bool alwaysVisibleScrollThumb) { - return ( - Color backgroundColor, - Animation thumbAnimation, - Animation labelAnimation, - double height, { - Text? labelText, - BoxConstraints? labelConstraints, - }) { - final scrollThumb = CustomPaint( - key: scrollThumbKey, - foregroundPainter: ArrowCustomPainter(Colors.white), - child: Material( - elevation: 4.0, - color: backgroundColor, - borderRadius: BorderRadius.only( - topLeft: Radius.circular(height), - bottomLeft: Radius.circular(height), - topRight: const Radius.circular(4.0), - bottomRight: const Radius.circular(4.0), - ), - child: Container(constraints: BoxConstraints.tight(Size(width, height))), - ), - ); - - return buildScrollThumbAndLabel( - scrollThumb: scrollThumb, - backgroundColor: backgroundColor, - thumbAnimation: thumbAnimation, - labelAnimation: labelAnimation, - labelText: labelText, - labelConstraints: labelConstraints, - alwaysVisibleScrollThumb: alwaysVisibleScrollThumb, - ); - }; - } - - static ScrollThumbBuilder _thumbArrowBuilder(bool alwaysVisibleScrollThumb) { - return ( - Color backgroundColor, - Animation thumbAnimation, - Animation labelAnimation, - double height, { - Text? labelText, - BoxConstraints? labelConstraints, - }) { - final scrollThumb = ClipPath( - clipper: const ArrowClipper(), - child: Container( - height: height, - width: 20.0, - decoration: BoxDecoration( - color: backgroundColor, - borderRadius: const BorderRadius.all(Radius.circular(12.0)), - ), - ), - ); - - return buildScrollThumbAndLabel( - scrollThumb: scrollThumb, - backgroundColor: backgroundColor, - thumbAnimation: thumbAnimation, - labelAnimation: labelAnimation, - labelText: labelText, - labelConstraints: labelConstraints, - alwaysVisibleScrollThumb: alwaysVisibleScrollThumb, - ); - }; - } - - static ScrollThumbBuilder _thumbRRectBuilder(bool alwaysVisibleScrollThumb) { - return ( - Color backgroundColor, - Animation thumbAnimation, - Animation labelAnimation, - double height, { - Text? labelText, - BoxConstraints? labelConstraints, - }) { - final scrollThumb = Material( - elevation: 4.0, - color: backgroundColor, - borderRadius: const BorderRadius.all(Radius.circular(7.0)), - child: Container(constraints: BoxConstraints.tight(Size(16.0, height))), - ); - - return buildScrollThumbAndLabel( - scrollThumb: scrollThumb, - backgroundColor: backgroundColor, - thumbAnimation: thumbAnimation, - labelAnimation: labelAnimation, - labelText: labelText, - labelConstraints: labelConstraints, - alwaysVisibleScrollThumb: alwaysVisibleScrollThumb, - ); - }; - } -} - -class ScrollLabel extends StatelessWidget { - final Animation? animation; - final Color backgroundColor; - final Text child; - - final BoxConstraints? constraints; - static const BoxConstraints _defaultConstraints = BoxConstraints.tightFor(width: 72.0, height: 28.0); - - const ScrollLabel({ - super.key, - required this.child, - required this.animation, - required this.backgroundColor, - this.constraints = _defaultConstraints, - }); - - @override - Widget build(BuildContext context) { - return FadeTransition( - opacity: animation!, - child: Container( - margin: const EdgeInsets.only(right: 12.0), - child: Material( - elevation: 4.0, - color: backgroundColor, - borderRadius: const BorderRadius.all(Radius.circular(16.0)), - child: Container(constraints: constraints ?? _defaultConstraints, alignment: Alignment.center, child: child), - ), - ), - ); - } -} - -class DraggableScrollbarState extends State with TickerProviderStateMixin { - late double _barOffset; - late double _viewOffset; - late bool _isDragInProcess; - - late AnimationController _thumbAnimationController; - late Animation _thumbAnimation; - late AnimationController _labelAnimationController; - late Animation _labelAnimation; - Timer? _fadeoutTimer; - - @override - void initState() { - super.initState(); - _barOffset = 0.0; - _viewOffset = 0.0; - _isDragInProcess = false; - - _thumbAnimationController = AnimationController(vsync: this, duration: widget.scrollbarAnimationDuration); - - _thumbAnimation = CurvedAnimation(parent: _thumbAnimationController, curve: Curves.fastOutSlowIn); - - _labelAnimationController = AnimationController(vsync: this, duration: widget.scrollbarAnimationDuration); - - _labelAnimation = CurvedAnimation(parent: _labelAnimationController, curve: Curves.fastOutSlowIn); - } - - @override - void dispose() { - _thumbAnimationController.dispose(); - _labelAnimationController.dispose(); - _fadeoutTimer?.cancel(); - super.dispose(); - } - - double get barMaxScrollExtent => context.size!.height - widget.heightScrollThumb; - - double get barMinScrollExtent => 0; - - double get viewMaxScrollExtent => widget.controller.position.maxScrollExtent; - - double get viewMinScrollExtent => widget.controller.position.minScrollExtent; - - @override - Widget build(BuildContext context) { - Text? labelText; - if (widget.labelTextBuilder != null && _isDragInProcess) { - labelText = widget.labelTextBuilder!(_viewOffset + _barOffset + widget.heightScrollThumb / 2); - } - - return LayoutBuilder( - builder: (BuildContext context, BoxConstraints constraints) { - //print("LayoutBuilder constraints=$constraints"); - - return NotificationListener( - onNotification: (ScrollNotification notification) { - changePosition(notification); - return false; - }, - child: Stack( - children: [ - RepaintBoundary(child: widget.child), - RepaintBoundary( - child: GestureDetector( - onVerticalDragStart: _onVerticalDragStart, - onVerticalDragUpdate: _onVerticalDragUpdate, - onVerticalDragEnd: _onVerticalDragEnd, - child: Container( - alignment: Alignment.topRight, - margin: EdgeInsets.only(top: _barOffset), - padding: widget.padding, - child: widget.scrollThumbBuilder( - widget.backgroundColor, - _thumbAnimation, - _labelAnimation, - widget.heightScrollThumb, - labelText: labelText, - labelConstraints: widget.labelConstraints, - ), - ), - ), - ), - ], - ), - ); - }, - ); - } - - //scroll bar has received notification that it's view was scrolled - //so it should also changes his position - //but only if it isn't dragged - changePosition(ScrollNotification notification) { - if (_isDragInProcess) { - return; - } - - setState(() { - if (notification is ScrollUpdateNotification) { - _barOffset += getBarDelta(notification.scrollDelta!, barMaxScrollExtent, viewMaxScrollExtent); - - if (_barOffset < barMinScrollExtent) { - _barOffset = barMinScrollExtent; - } - if (_barOffset > barMaxScrollExtent) { - _barOffset = barMaxScrollExtent; - } - - _viewOffset += notification.scrollDelta!; - if (_viewOffset < widget.controller.position.minScrollExtent) { - _viewOffset = widget.controller.position.minScrollExtent; - } - if (_viewOffset > viewMaxScrollExtent) { - _viewOffset = viewMaxScrollExtent; - } - } - - if (notification is ScrollUpdateNotification || notification is OverscrollNotification) { - if (_thumbAnimationController.status != AnimationStatus.forward) { - _thumbAnimationController.forward(); - } - - _fadeoutTimer?.cancel(); - _fadeoutTimer = Timer(widget.scrollbarTimeToFade, () { - _thumbAnimationController.reverse(); - _labelAnimationController.reverse(); - _fadeoutTimer = null; - }); - } - }); - } - - double getBarDelta(double scrollViewDelta, double barMaxScrollExtent, double viewMaxScrollExtent) { - return scrollViewDelta * barMaxScrollExtent / viewMaxScrollExtent; - } - - double getScrollViewDelta(double barDelta, double barMaxScrollExtent, double viewMaxScrollExtent) { - return barDelta * viewMaxScrollExtent / barMaxScrollExtent; - } - - void _onVerticalDragStart(DragStartDetails details) { - setState(() { - _isDragInProcess = true; - _labelAnimationController.forward(); - _fadeoutTimer?.cancel(); - }); - } - - void _onVerticalDragUpdate(DragUpdateDetails details) { - setState(() { - if (_thumbAnimationController.status != AnimationStatus.forward) { - _thumbAnimationController.forward(); - } - if (_isDragInProcess) { - _barOffset += details.delta.dy; - - if (_barOffset < barMinScrollExtent) { - _barOffset = barMinScrollExtent; - } - if (_barOffset > barMaxScrollExtent) { - _barOffset = barMaxScrollExtent; - } - - double viewDelta = getScrollViewDelta(details.delta.dy, barMaxScrollExtent, viewMaxScrollExtent); - - _viewOffset = widget.controller.position.pixels + viewDelta; - if (_viewOffset < widget.controller.position.minScrollExtent) { - _viewOffset = widget.controller.position.minScrollExtent; - } - if (_viewOffset > viewMaxScrollExtent) { - _viewOffset = viewMaxScrollExtent; - } - widget.controller.jumpTo(_viewOffset); - } - }); - } - - void _onVerticalDragEnd(DragEndDetails details) { - _fadeoutTimer = Timer(widget.scrollbarTimeToFade, () { - _thumbAnimationController.reverse(); - _labelAnimationController.reverse(); - _fadeoutTimer = null; - }); - setState(() { - _isDragInProcess = false; - }); - } -} - -/// Draws 2 triangles like arrow up and arrow down -class ArrowCustomPainter extends CustomPainter { - Color color; - - ArrowCustomPainter(this.color); - - @override - bool shouldRepaint(covariant CustomPainter oldDelegate) => false; - - @override - void paint(Canvas canvas, Size size) { - final paint = Paint()..color = color; - const width = 12.0; - const height = 8.0; - final baseX = size.width / 2; - final baseY = size.height / 2; - - canvas.drawPath(_trianglePath(Offset(baseX, baseY - 2.0), width, height, true), paint); - canvas.drawPath(_trianglePath(Offset(baseX, baseY + 2.0), width, height, false), paint); - } - - static Path _trianglePath(Offset o, double width, double height, bool isUp) { - return Path() - ..moveTo(o.dx, o.dy) - ..lineTo(o.dx + width, o.dy) - ..lineTo(o.dx + (width / 2), isUp ? o.dy - height : o.dy + height) - ..close(); - } -} - -///This cut 2 lines in arrow shape -class ArrowClipper extends CustomClipper { - const ArrowClipper(); - @override - Path getClip(Size size) { - Path path = Path(); - path.lineTo(0.0, size.height); - path.lineTo(size.width, size.height); - path.lineTo(size.width, 0.0); - path.lineTo(0.0, 0.0); - path.close(); - - double arrowWidth = 8.0; - double startPointX = (size.width - arrowWidth) / 2; - double startPointY = size.height / 2 - arrowWidth / 2; - path.moveTo(startPointX, startPointY); - path.lineTo(startPointX + arrowWidth / 2, startPointY - arrowWidth / 2); - path.lineTo(startPointX + arrowWidth, startPointY); - path.lineTo(startPointX + arrowWidth, startPointY + 1.0); - path.lineTo(startPointX + arrowWidth / 2, startPointY - arrowWidth / 2 + 1.0); - path.lineTo(startPointX, startPointY + 1.0); - path.close(); - - startPointY = size.height / 2 + arrowWidth / 2; - path.moveTo(startPointX + arrowWidth, startPointY); - path.lineTo(startPointX + arrowWidth / 2, startPointY + arrowWidth / 2); - path.lineTo(startPointX, startPointY); - path.lineTo(startPointX, startPointY - 1.0); - path.lineTo(startPointX + arrowWidth / 2, startPointY + arrowWidth / 2 - 1.0); - path.lineTo(startPointX + arrowWidth, startPointY - 1.0); - path.close(); - - return path; - } - - @override - bool shouldReclip(CustomClipper oldClipper) => false; -} - -class SlideFadeTransition extends StatelessWidget { - final Animation animation; - final Widget child; - - const SlideFadeTransition({super.key, required this.animation, required this.child}); - - @override - Widget build(BuildContext context) { - return AnimatedBuilder( - animation: animation, - builder: (context, child) => animation.value == 0.0 ? const SizedBox() : child!, - child: SlideTransition( - position: Tween(begin: const Offset(0.3, 0.0), end: const Offset(0.0, 0.0)).animate(animation), - child: FadeTransition(opacity: animation, child: child), - ), - ); - } -} diff --git a/mobile/lib/widgets/asset_grid/draggable_scrollbar_custom.dart b/mobile/lib/widgets/asset_grid/draggable_scrollbar_custom.dart deleted file mode 100644 index 17f35311f0..0000000000 --- a/mobile/lib/widgets/asset_grid/draggable_scrollbar_custom.dart +++ /dev/null @@ -1,490 +0,0 @@ -import 'dart:async'; - -import 'package:flutter/material.dart'; -import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; - -/// Build the Scroll Thumb and label using the current configuration -typedef ScrollThumbBuilder = - Widget Function( - Color backgroundColor, - Animation thumbAnimation, - Animation labelAnimation, - double height, { - Text? labelText, - BoxConstraints? labelConstraints, - }); - -/// Build a Text widget using the current scroll offset -typedef LabelTextBuilder = Text Function(int item); - -/// A widget that will display a BoxScrollView with a ScrollThumb that can be dragged -/// for quick navigation of the BoxScrollView. -class DraggableScrollbar extends StatefulWidget { - /// The view that will be scrolled with the scroll thumb - final ScrollablePositionedList child; - - final ItemPositionsListener itemPositionsListener; - - /// A function that builds a thumb using the current configuration - final ScrollThumbBuilder scrollThumbBuilder; - - /// The height of the scroll thumb - final double heightScrollThumb; - - /// The background color of the label and thumb - final Color backgroundColor; - - /// The amount of padding that should surround the thumb - final EdgeInsetsGeometry? padding; - - /// The height offset of the thumb/bar from the bottom of the page - final double? heightOffset; - - /// Determines how quickly the scrollbar will animate in and out - final Duration scrollbarAnimationDuration; - - /// How long should the thumb be visible before fading out - final Duration scrollbarTimeToFade; - - /// Build a Text widget from the current offset in the BoxScrollView - final LabelTextBuilder? labelTextBuilder; - - /// Determines box constraints for Container displaying label - final BoxConstraints? labelConstraints; - - /// The ScrollController for the BoxScrollView - final ItemScrollController controller; - - /// Determines scrollThumb displaying. If you draw own ScrollThumb and it is true you just don't need to use animation parameters in [scrollThumbBuilder] - final bool alwaysVisibleScrollThumb; - - final Function(bool scrolling) scrollStateListener; - - DraggableScrollbar.semicircle({ - super.key, - Key? scrollThumbKey, - this.alwaysVisibleScrollThumb = false, - required this.child, - required this.controller, - required this.itemPositionsListener, - required this.scrollStateListener, - this.heightScrollThumb = 48.0, - this.backgroundColor = Colors.white, - this.padding, - this.heightOffset, - this.scrollbarAnimationDuration = const Duration(milliseconds: 300), - this.scrollbarTimeToFade = const Duration(milliseconds: 600), - this.labelTextBuilder, - this.labelConstraints, - }) : assert(child.scrollDirection == Axis.vertical), - scrollThumbBuilder = _thumbSemicircleBuilder(heightScrollThumb * 0.6, scrollThumbKey, alwaysVisibleScrollThumb); - - @override - DraggableScrollbarState createState() => DraggableScrollbarState(); - - static buildScrollThumbAndLabel({ - required Widget scrollThumb, - required Color backgroundColor, - required Animation? thumbAnimation, - required Animation? labelAnimation, - required Text? labelText, - required BoxConstraints? labelConstraints, - required bool alwaysVisibleScrollThumb, - }) { - var scrollThumbAndLabel = labelText == null - ? scrollThumb - : Row( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.end, - children: [ - ScrollLabel( - animation: labelAnimation, - backgroundColor: backgroundColor, - constraints: labelConstraints, - child: labelText, - ), - scrollThumb, - ], - ); - - if (alwaysVisibleScrollThumb) { - return scrollThumbAndLabel; - } - return SlideFadeTransition(animation: thumbAnimation!, child: scrollThumbAndLabel); - } - - static ScrollThumbBuilder _thumbSemicircleBuilder(double width, Key? scrollThumbKey, bool alwaysVisibleScrollThumb) { - return ( - Color backgroundColor, - Animation thumbAnimation, - Animation labelAnimation, - double height, { - Text? labelText, - BoxConstraints? labelConstraints, - }) { - final scrollThumb = CustomPaint( - key: scrollThumbKey, - foregroundPainter: ArrowCustomPainter(Colors.white), - child: Material( - elevation: 4.0, - color: backgroundColor, - borderRadius: BorderRadius.only( - topLeft: Radius.circular(height), - bottomLeft: Radius.circular(height), - topRight: const Radius.circular(4.0), - bottomRight: const Radius.circular(4.0), - ), - child: Container(constraints: BoxConstraints.tight(Size(width, height))), - ), - ); - - return buildScrollThumbAndLabel( - scrollThumb: scrollThumb, - backgroundColor: backgroundColor, - thumbAnimation: thumbAnimation, - labelAnimation: labelAnimation, - labelText: labelText, - labelConstraints: labelConstraints, - alwaysVisibleScrollThumb: alwaysVisibleScrollThumb, - ); - }; - } -} - -class ScrollLabel extends StatelessWidget { - final Animation? animation; - final Color backgroundColor; - final Text child; - - final BoxConstraints? constraints; - static const BoxConstraints _defaultConstraints = BoxConstraints.tightFor(width: 72.0, height: 28.0); - - const ScrollLabel({ - super.key, - required this.child, - required this.animation, - required this.backgroundColor, - this.constraints = _defaultConstraints, - }); - - @override - Widget build(BuildContext context) { - return FadeTransition( - opacity: animation!, - child: Container( - margin: const EdgeInsets.only(right: 12.0), - child: Material( - elevation: 4.0, - color: backgroundColor, - borderRadius: const BorderRadius.all(Radius.circular(16.0)), - child: Container( - constraints: constraints ?? _defaultConstraints, - padding: const EdgeInsets.symmetric(horizontal: 10.0), - alignment: Alignment.center, - child: child, - ), - ), - ), - ); - } -} - -class DraggableScrollbarState extends State with TickerProviderStateMixin { - late double _barOffset; - late bool _isDragInProcess; - late int _currentItem; - - late AnimationController _thumbAnimationController; - late Animation _thumbAnimation; - late AnimationController _labelAnimationController; - late Animation _labelAnimation; - Timer? _fadeoutTimer; - - @override - void initState() { - super.initState(); - _barOffset = 0.0; - _isDragInProcess = false; - _currentItem = 0; - - _thumbAnimationController = AnimationController(vsync: this, duration: widget.scrollbarAnimationDuration); - - _thumbAnimation = CurvedAnimation(parent: _thumbAnimationController, curve: Curves.fastOutSlowIn); - - _labelAnimationController = AnimationController(vsync: this, duration: widget.scrollbarAnimationDuration); - - _labelAnimation = CurvedAnimation(parent: _labelAnimationController, curve: Curves.fastOutSlowIn); - } - - @override - void dispose() { - _thumbAnimationController.dispose(); - _labelAnimationController.dispose(); - _fadeoutTimer?.cancel(); - super.dispose(); - } - - double get barMaxScrollExtent => (context.size?.height ?? 0) - widget.heightScrollThumb - (widget.heightOffset ?? 0); - - double get barMinScrollExtent => 0; - - int get maxItemCount => widget.child.itemCount; - - @override - Widget build(BuildContext context) { - Text? labelText; - if (widget.labelTextBuilder != null && _isDragInProcess) { - labelText = widget.labelTextBuilder!(_currentItem); - } - - return LayoutBuilder( - builder: (BuildContext context, BoxConstraints constraints) { - //print("LayoutBuilder constraints=$constraints"); - - return NotificationListener( - onNotification: (ScrollNotification notification) { - changePosition(notification); - return false; - }, - child: Stack( - children: [ - RepaintBoundary(child: widget.child), - RepaintBoundary( - child: GestureDetector( - onVerticalDragStart: _onVerticalDragStart, - onVerticalDragUpdate: _onVerticalDragUpdate, - onVerticalDragEnd: _onVerticalDragEnd, - child: Container( - alignment: Alignment.topRight, - margin: EdgeInsets.only(top: _barOffset), - padding: widget.padding, - child: widget.scrollThumbBuilder( - widget.backgroundColor, - _thumbAnimation, - _labelAnimation, - widget.heightScrollThumb, - labelText: labelText, - labelConstraints: widget.labelConstraints, - ), - ), - ), - ), - ], - ), - ); - }, - ); - } - - // scroll bar has received notification that it's view was scrolled - // so it should also changes his position - // but only if it isn't dragged - changePosition(ScrollNotification notification) { - if (_isDragInProcess) { - return; - } - - setState(() { - try { - int firstItemIndex = widget.itemPositionsListener.itemPositions.value.first.index; - - if (notification is ScrollUpdateNotification) { - _barOffset = (firstItemIndex / maxItemCount) * barMaxScrollExtent; - - if (_barOffset < barMinScrollExtent) { - _barOffset = barMinScrollExtent; - } - if (_barOffset > barMaxScrollExtent) { - _barOffset = barMaxScrollExtent; - } - } - - if (notification is ScrollUpdateNotification || notification is OverscrollNotification) { - if (_thumbAnimationController.status != AnimationStatus.forward) { - _thumbAnimationController.forward(); - } - - if (itemPosition < maxItemCount) { - _currentItem = itemPosition; - } - - _fadeoutTimer?.cancel(); - _fadeoutTimer = Timer(widget.scrollbarTimeToFade, () { - _thumbAnimationController.reverse(); - _labelAnimationController.reverse(); - _fadeoutTimer = null; - }); - } - } catch (_) {} - }); - } - - void _onVerticalDragStart(DragStartDetails details) { - setState(() { - _isDragInProcess = true; - _labelAnimationController.forward(); - _fadeoutTimer?.cancel(); - }); - - widget.scrollStateListener(true); - } - - int get itemPosition { - int numberOfItems = widget.child.itemCount; - return ((_barOffset / barMaxScrollExtent) * numberOfItems).toInt(); - } - - void _jumpToBarPosition() { - if (itemPosition > maxItemCount - 1) { - return; - } - - _currentItem = itemPosition; - - /// If the bar is at the bottom but the item position is still smaller than the max item count (due to rounding error) - /// jump to the end of the list - if (barMaxScrollExtent - _barOffset < 10 && itemPosition < maxItemCount) { - widget.controller.jumpTo(index: maxItemCount); - - return; - } - - widget.controller.jumpTo(index: itemPosition); - } - - Timer? dragHaltTimer; - int lastTimerPosition = 0; - - void _onVerticalDragUpdate(DragUpdateDetails details) { - setState(() { - if (_thumbAnimationController.status != AnimationStatus.forward) { - _thumbAnimationController.forward(); - } - if (_isDragInProcess) { - _barOffset += details.delta.dy; - - if (_barOffset < barMinScrollExtent) { - _barOffset = barMinScrollExtent; - } - if (_barOffset > barMaxScrollExtent) { - _barOffset = barMaxScrollExtent; - } - - if (itemPosition != lastTimerPosition) { - lastTimerPosition = itemPosition; - dragHaltTimer?.cancel(); - widget.scrollStateListener(true); - - dragHaltTimer = Timer(const Duration(milliseconds: 500), () { - widget.scrollStateListener(false); - }); - } - - _jumpToBarPosition(); - } - }); - } - - void _onVerticalDragEnd(DragEndDetails details) { - _fadeoutTimer = Timer(widget.scrollbarTimeToFade, () { - _thumbAnimationController.reverse(); - _labelAnimationController.reverse(); - _fadeoutTimer = null; - }); - - setState(() { - _jumpToBarPosition(); - _isDragInProcess = false; - }); - - widget.scrollStateListener(false); - } -} - -/// Draws 2 triangles like arrow up and arrow down -class ArrowCustomPainter extends CustomPainter { - Color color; - - ArrowCustomPainter(this.color); - - @override - bool shouldRepaint(covariant CustomPainter oldDelegate) => false; - - @override - void paint(Canvas canvas, Size size) { - final paint = Paint()..color = color; - const width = 12.0; - const height = 8.0; - final baseX = size.width / 2; - final baseY = size.height / 2; - - canvas.drawPath(_trianglePath(Offset(baseX, baseY - 2.0), width, height, true), paint); - canvas.drawPath(_trianglePath(Offset(baseX, baseY + 2.0), width, height, false), paint); - } - - static Path _trianglePath(Offset o, double width, double height, bool isUp) { - return Path() - ..moveTo(o.dx, o.dy) - ..lineTo(o.dx + width, o.dy) - ..lineTo(o.dx + (width / 2), isUp ? o.dy - height : o.dy + height) - ..close(); - } -} - -///This cut 2 lines in arrow shape -class ArrowClipper extends CustomClipper { - const ArrowClipper(); - @override - Path getClip(Size size) { - Path path = Path(); - path.lineTo(0.0, size.height); - path.lineTo(size.width, size.height); - path.lineTo(size.width, 0.0); - path.lineTo(0.0, 0.0); - path.close(); - - double arrowWidth = 8.0; - double startPointX = (size.width - arrowWidth) / 2; - double startPointY = size.height / 2 - arrowWidth / 2; - path.moveTo(startPointX, startPointY); - path.lineTo(startPointX + arrowWidth / 2, startPointY - arrowWidth / 2); - path.lineTo(startPointX + arrowWidth, startPointY); - path.lineTo(startPointX + arrowWidth, startPointY + 1.0); - path.lineTo(startPointX + arrowWidth / 2, startPointY - arrowWidth / 2 + 1.0); - path.lineTo(startPointX, startPointY + 1.0); - path.close(); - - startPointY = size.height / 2 + arrowWidth / 2; - path.moveTo(startPointX + arrowWidth, startPointY); - path.lineTo(startPointX + arrowWidth / 2, startPointY + arrowWidth / 2); - path.lineTo(startPointX, startPointY); - path.lineTo(startPointX, startPointY - 1.0); - path.lineTo(startPointX + arrowWidth / 2, startPointY + arrowWidth / 2 - 1.0); - path.lineTo(startPointX + arrowWidth, startPointY - 1.0); - path.close(); - - return path; - } - - @override - bool shouldReclip(CustomClipper oldClipper) => false; -} - -class SlideFadeTransition extends StatelessWidget { - final Animation animation; - final Widget child; - - const SlideFadeTransition({super.key, required this.animation, required this.child}); - - @override - Widget build(BuildContext context) { - return AnimatedBuilder( - animation: animation, - builder: (context, child) => animation.value == 0.0 ? const SizedBox() : child!, - child: SlideTransition( - position: Tween(begin: const Offset(0.3, 0.0), end: const Offset(0.0, 0.0)).animate(animation), - child: FadeTransition(opacity: animation, child: child), - ), - ); - } -} diff --git a/mobile/lib/widgets/asset_grid/group_divider_title.dart b/mobile/lib/widgets/asset_grid/group_divider_title.dart deleted file mode 100644 index 1464c941f0..0000000000 --- a/mobile/lib/widgets/asset_grid/group_divider_title.dart +++ /dev/null @@ -1,84 +0,0 @@ -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/widgets/asset_grid/asset_grid_data_structure.dart'; -import 'package:immich_mobile/providers/app_settings.provider.dart'; -import 'package:immich_mobile/services/app_settings.service.dart'; -import 'package:immich_mobile/providers/haptic_feedback.provider.dart'; - -class GroupDividerTitle extends HookConsumerWidget { - const GroupDividerTitle({ - super.key, - required this.text, - required this.multiselectEnabled, - required this.onSelect, - required this.onDeselect, - required this.selected, - }); - - final String text; - final bool multiselectEnabled; - final Function onSelect; - final Function onDeselect; - final bool selected; - - @override - Widget build(BuildContext context, WidgetRef ref) { - final appSettingService = ref.watch(appSettingsServiceProvider); - final groupBy = useState(GroupAssetsBy.day); - - useEffect(() { - groupBy.value = GroupAssetsBy.values[appSettingService.getSetting(AppSettingsEnum.groupAssetsBy)]; - return null; - }, []); - - void handleTitleIconClick() { - ref.read(hapticFeedbackProvider.notifier).heavyImpact(); - if (selected) { - onDeselect(); - } else { - onSelect(); - } - } - - return Padding( - padding: EdgeInsets.only( - top: groupBy.value == GroupAssetsBy.month ? 32.0 : 16.0, - bottom: 16.0, - left: 12.0, - right: 12.0, - ), - child: Row( - children: [ - Text( - text, - style: groupBy.value == GroupAssetsBy.month - ? context.textTheme.bodyLarge?.copyWith(fontSize: 24.0) - : context.textTheme.labelLarge?.copyWith( - color: context.textTheme.labelLarge?.color?.withAlpha(250), - fontWeight: FontWeight.w500, - ), - ), - const Spacer(), - GestureDetector( - onTap: handleTitleIconClick, - child: multiselectEnabled && selected - ? Icon( - Icons.check_circle_rounded, - color: context.primaryColor, - semanticLabel: "unselect_all_in".tr(namedArgs: {"group": text}), - ) - : Icon( - Icons.check_circle_outline_rounded, - color: context.colorScheme.onSurfaceSecondary, - semanticLabel: "select_all_in".tr(namedArgs: {"group": text}), - ), - ), - ], - ), - ); - } -} diff --git a/mobile/lib/widgets/asset_grid/immich_asset_grid.dart b/mobile/lib/widgets/asset_grid/immich_asset_grid.dart deleted file mode 100644 index ab6b350a7b..0000000000 --- a/mobile/lib/widgets/asset_grid/immich_asset_grid.dart +++ /dev/null @@ -1,135 +0,0 @@ -import 'dart:math'; - -import 'package:auto_route/auto_route.dart'; -import 'package:flutter/gestures.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/asyncvalue_extensions.dart'; -import 'package:immich_mobile/providers/timeline.provider.dart'; -import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart'; -import 'package:immich_mobile/widgets/asset_grid/immich_asset_grid_view.dart'; -import 'package:immich_mobile/providers/app_settings.provider.dart'; -import 'package:immich_mobile/services/app_settings.service.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; - -class ImmichAssetGrid extends HookConsumerWidget { - final int? assetsPerRow; - final double margin; - final bool? showStorageIndicator; - final ImmichAssetGridSelectionListener? listener; - final bool selectionActive; - final List? assets; - final RenderList? renderList; - final Future Function()? onRefresh; - final Set? preselectedAssets; - final bool canDeselect; - final bool? dynamicLayout; - final bool showMultiSelectIndicator; - final void Function(Iterable itemPositions)? visibleItemsListener; - final Widget? topWidget; - final bool shrinkWrap; - final bool showDragScroll; - final bool showDragScrollLabel; - final bool showStack; - - const ImmichAssetGrid({ - super.key, - this.assets, - this.onRefresh, - this.renderList, - this.assetsPerRow, - this.showStorageIndicator, - this.listener, - this.margin = 2.0, - this.selectionActive = false, - this.preselectedAssets, - this.canDeselect = true, - this.dynamicLayout, - this.showMultiSelectIndicator = true, - this.visibleItemsListener, - this.topWidget, - this.shrinkWrap = false, - this.showDragScroll = true, - this.showDragScrollLabel = true, - this.showStack = false, - }); - - @override - Widget build(BuildContext context, WidgetRef ref) { - var settings = ref.watch(appSettingsServiceProvider); - - final perRow = useState(assetsPerRow ?? settings.getSetting(AppSettingsEnum.tilesPerRow)!); - final scaleFactor = useState(7.0 - perRow.value); - final baseScaleFactor = useState(7.0 - perRow.value); - - /// assets need different hero tags across tabs / modals - /// otherwise, hero animations are performed across tabs (looks buggy!) - int heroOffset() { - const int range = 1152921504606846976; // 2^60 - final tabScope = TabsRouterScope.of(context); - if (tabScope != null) { - final int tabIndex = tabScope.controller.activeIndex; - return tabIndex * range; - } - return range * 7; - } - - Widget buildAssetGridView(RenderList renderList) { - return RawGestureDetector( - gestures: { - CustomScaleGestureRecognizer: GestureRecognizerFactoryWithHandlers( - () => CustomScaleGestureRecognizer(), - (CustomScaleGestureRecognizer scale) { - scale.onStart = (details) { - baseScaleFactor.value = scaleFactor.value; - }; - - scale.onUpdate = (details) { - scaleFactor.value = max(min(5.0, baseScaleFactor.value * details.scale), 1.0); - if (7 - scaleFactor.value.toInt() != perRow.value) { - perRow.value = 7 - scaleFactor.value.toInt(); - settings.setSetting(AppSettingsEnum.tilesPerRow, perRow.value); - } - }; - }, - ), - }, - child: ImmichAssetGridView( - onRefresh: onRefresh, - assetsPerRow: perRow.value, - listener: listener, - showStorageIndicator: showStorageIndicator ?? settings.getSetting(AppSettingsEnum.storageIndicator), - renderList: renderList, - margin: margin, - selectionActive: selectionActive, - preselectedAssets: preselectedAssets, - canDeselect: canDeselect, - dynamicLayout: dynamicLayout ?? settings.getSetting(AppSettingsEnum.dynamicLayout), - showMultiSelectIndicator: showMultiSelectIndicator, - visibleItemsListener: visibleItemsListener, - topWidget: topWidget, - heroOffset: heroOffset(), - shrinkWrap: shrinkWrap, - showDragScroll: showDragScroll, - showStack: showStack, - showLabel: showDragScrollLabel, - ), - ); - } - - if (renderList != null) return buildAssetGridView(renderList!); - - final renderListFuture = ref.watch(assetsTimelineProvider(assets!)); - return renderListFuture.widgetWhen(onData: (renderList) => buildAssetGridView(renderList)); - } -} - -/// accepts a gesture even though it should reject it (because child won) -class CustomScaleGestureRecognizer extends ScaleGestureRecognizer { - @override - void rejectGesture(int pointer) { - acceptGesture(pointer); - } -} diff --git a/mobile/lib/widgets/asset_grid/immich_asset_grid_view.dart b/mobile/lib/widgets/asset_grid/immich_asset_grid_view.dart deleted file mode 100644 index c323c573b4..0000000000 --- a/mobile/lib/widgets/asset_grid/immich_asset_grid_view.dart +++ /dev/null @@ -1,828 +0,0 @@ -import 'dart:collection'; -import 'dart:developer'; -import 'dart:math'; - -import 'package:auto_route/auto_route.dart'; -import 'package:collection/collection.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/rendering.dart'; -import 'package:flutter/services.dart'; -import 'package:fluttertoast/fluttertoast.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/extensions/collection_extensions.dart'; -import 'package:immich_mobile/extensions/theme_extensions.dart'; -import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart'; -import 'package:immich_mobile/providers/asset_viewer/is_motion_video_playing.provider.dart'; -import 'package:immich_mobile/providers/asset_viewer/scroll_notifier.provider.dart'; -import 'package:immich_mobile/providers/asset_viewer/scroll_to_date_notifier.provider.dart'; -import 'package:immich_mobile/providers/asset_viewer/show_controls.provider.dart'; -import 'package:immich_mobile/providers/haptic_feedback.provider.dart'; -import 'package:immich_mobile/providers/tab.provider.dart'; -import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/widgets/asset_grid/asset_drag_region.dart'; -import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart'; -import 'package:immich_mobile/widgets/asset_grid/control_bottom_app_bar.dart'; -import 'package:immich_mobile/widgets/asset_grid/disable_multi_select_button.dart'; -import 'package:immich_mobile/widgets/asset_grid/draggable_scrollbar_custom.dart'; -import 'package:immich_mobile/widgets/asset_grid/group_divider_title.dart'; -import 'package:immich_mobile/widgets/asset_grid/thumbnail_image.dart'; -import 'package:immich_mobile/widgets/asset_grid/thumbnail_placeholder.dart'; -import 'package:immich_mobile/widgets/common/immich_toast.dart'; -import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; - -typedef ImmichAssetGridSelectionListener = void Function(bool, Set); - -class ImmichAssetGridView extends ConsumerStatefulWidget { - final RenderList renderList; - final int assetsPerRow; - final double margin; - final bool showStorageIndicator; - final ImmichAssetGridSelectionListener? listener; - final bool selectionActive; - final Future Function()? onRefresh; - final Set? preselectedAssets; - final bool canDeselect; - final bool dynamicLayout; - final bool showMultiSelectIndicator; - final void Function(Iterable itemPositions)? visibleItemsListener; - final Widget? topWidget; - final int heroOffset; - final bool shrinkWrap; - final bool showDragScroll; - final bool showStack; - final bool showLabel; - - const ImmichAssetGridView({ - super.key, - required this.renderList, - required this.assetsPerRow, - required this.showStorageIndicator, - this.listener, - this.margin = 5.0, - this.selectionActive = false, - this.onRefresh, - this.preselectedAssets, - this.canDeselect = true, - this.dynamicLayout = true, - this.showMultiSelectIndicator = true, - this.visibleItemsListener, - this.topWidget, - this.heroOffset = 0, - this.shrinkWrap = false, - this.showDragScroll = true, - this.showStack = false, - this.showLabel = true, - }); - - @override - createState() { - return ImmichAssetGridViewState(); - } -} - -class ImmichAssetGridViewState extends ConsumerState { - final ItemScrollController _itemScrollController = ItemScrollController(); - final ScrollOffsetController _scrollOffsetController = ScrollOffsetController(); - final ItemPositionsListener _itemPositionsListener = ItemPositionsListener.create(); - late final KeepAliveLink currentAssetLink; - - /// The timestamp when the haptic feedback was last invoked - int _hapticFeedbackTS = 0; - DateTime? _prevItemTime; - bool _scrolling = false; - final Set _selectedAssets = LinkedHashSet(equals: (a, b) => a.id == b.id, hashCode: (a) => a.id); - - bool _dragging = false; - int? _dragAnchorAssetIndex; - int? _dragAnchorSectionIndex; - final Set _draggedAssets = HashSet(equals: (a, b) => a.id == b.id, hashCode: (a) => a.id); - - ScrollPhysics? _scrollPhysics; - - Set _getSelectedAssets() { - return Set.from(_selectedAssets); - } - - void _callSelectionListener(bool selectionActive) { - widget.listener?.call(selectionActive, _getSelectedAssets()); - } - - void _selectAssets(List assets) { - setState(() { - if (_dragging) { - _draggedAssets.addAll(assets); - } - _selectedAssets.addAll(assets); - _callSelectionListener(true); - }); - } - - void _deselectAssets(List assets) { - final assetsToDeselect = assets.where( - (a) => widget.canDeselect || !(widget.preselectedAssets?.contains(a) ?? false), - ); - - setState(() { - _selectedAssets.removeAll(assetsToDeselect); - if (_dragging) { - _draggedAssets.removeAll(assetsToDeselect); - } - _callSelectionListener(_selectedAssets.isNotEmpty); - }); - } - - void _deselectAll() { - setState(() { - _selectedAssets.clear(); - _dragAnchorAssetIndex = null; - _dragAnchorSectionIndex = null; - _draggedAssets.clear(); - _dragging = false; - if (!widget.canDeselect && widget.preselectedAssets != null && widget.preselectedAssets!.isNotEmpty) { - _selectedAssets.addAll(widget.preselectedAssets!); - } - _callSelectionListener(false); - }); - } - - bool _allAssetsSelected(List assets) { - return widget.selectionActive && assets.firstWhereOrNull((e) => !_selectedAssets.contains(e)) == null; - } - - Future _scrollToIndex(int index) async { - // if the index is so far down, that the end of the list is reached on the screen - // the scroll_position widget crashes. This is a workaround to prevent this. - // If the index is within the last 10 elements, we jump instead of scrolling. - if (widget.renderList.elements.length <= index + 10) { - _itemScrollController.jumpTo(index: index); - return; - } - await _itemScrollController.scrollTo(index: index, alignment: 0, duration: const Duration(milliseconds: 500)); - } - - Widget _itemBuilder(BuildContext c, int position) { - int index = position; - if (widget.topWidget != null) { - if (index == 0) { - return widget.topWidget!; - } - index--; - } - - final section = widget.renderList.elements[index]; - return _Section( - showStorageIndicator: widget.showStorageIndicator, - selectedAssets: _selectedAssets, - selectionActive: widget.selectionActive, - sectionIndex: index, - section: section, - margin: widget.margin, - renderList: widget.renderList, - assetsPerRow: widget.assetsPerRow, - scrolling: _scrolling, - dynamicLayout: widget.dynamicLayout, - selectAssets: _selectAssets, - deselectAssets: _deselectAssets, - allAssetsSelected: _allAssetsSelected, - showStack: widget.showStack, - heroOffset: widget.heroOffset, - onAssetTap: (asset) { - ref.read(currentAssetProvider.notifier).set(asset); - ref.read(isPlayingMotionVideoProvider.notifier).playing = false; - if (asset.isVideo) { - ref.read(showControlsProvider.notifier).show = false; - } - }, - ); - } - - Text _labelBuilder(int pos) { - final maxLength = widget.renderList.elements.length; - if (pos < 0 || pos >= maxLength) { - return const Text(""); - } - - final date = widget.renderList.elements[pos % maxLength].date; - - return Text( - DateFormat.yMMMM().format(date), - style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold), - ); - } - - Widget _buildMultiSelectIndicator() { - return DisableMultiSelectButton(onPressed: () => _deselectAll(), selectedItemCount: _selectedAssets.length); - } - - Widget _buildAssetGrid() { - final useDragScrolling = widget.showDragScroll && widget.renderList.totalAssets >= 20; - - void dragScrolling(bool active) { - if (active != _scrolling) { - setState(() { - _scrolling = active; - }); - } - } - - bool appBarOffset() { - return (ref.watch(tabProvider).index == 0 && ModalRoute.of(context)?.settings.name == TabControllerRoute.name) || - (ModalRoute.of(context)?.settings.name == AlbumViewerRoute.name); - } - - final listWidget = ScrollablePositionedList.builder( - padding: EdgeInsets.only(top: appBarOffset() ? 60 : 0, bottom: 220), - itemBuilder: _itemBuilder, - itemPositionsListener: _itemPositionsListener, - physics: _scrollPhysics, - itemScrollController: _itemScrollController, - scrollOffsetController: _scrollOffsetController, - itemCount: widget.renderList.elements.length + (widget.topWidget != null ? 1 : 0), - addRepaintBoundaries: true, - shrinkWrap: widget.shrinkWrap, - ); - - final child = (useDragScrolling && ModalRoute.of(context) != null) - ? DraggableScrollbar.semicircle( - scrollStateListener: dragScrolling, - itemPositionsListener: _itemPositionsListener, - controller: _itemScrollController, - backgroundColor: context.isDarkTheme - ? context.colorScheme.primary.darken(amount: .5) - : context.colorScheme.primary, - labelTextBuilder: widget.showLabel ? _labelBuilder : null, - padding: appBarOffset() ? const EdgeInsets.only(top: 60) : const EdgeInsets.only(), - heightOffset: appBarOffset() ? 60 : 0, - labelConstraints: const BoxConstraints(maxHeight: 28), - scrollbarAnimationDuration: const Duration(milliseconds: 300), - scrollbarTimeToFade: const Duration(milliseconds: 1000), - child: listWidget, - ) - : listWidget; - - return widget.onRefresh == null - ? child - : appBarOffset() - ? RefreshIndicator(onRefresh: widget.onRefresh!, edgeOffset: 30, child: child) - : RefreshIndicator(onRefresh: widget.onRefresh!, child: child); - } - - void _scrollToDate() { - final date = scrollToDateNotifierProvider.value; - if (date == null) { - ImmichToast.show( - context: context, - msg: "Scroll To Date failed, date is null.", - gravity: ToastGravity.BOTTOM, - toastType: ToastType.error, - ); - return; - } - - // Search for the index of the exact date in the list - var index = widget.renderList.elements.indexWhere( - (e) => e.date.year == date.year && e.date.month == date.month && e.date.day == date.day, - ); - - // If the exact date is not found, the timeline is grouped by month, - // thus we search for the month - if (index == -1) { - index = widget.renderList.elements.indexWhere((e) => e.date.year == date.year && e.date.month == date.month); - } - - if (index < widget.renderList.elements.length) { - // Not sure why the index is shifted, but it works. :3 - _scrollToIndex(index + 1); - } else { - ImmichToast.show( - context: context, - msg: "The date (${DateFormat.yMd().format(date)}) could not be found in the timeline.", - gravity: ToastGravity.BOTTOM, - toastType: ToastType.error, - ); - } - } - - @override - void didUpdateWidget(ImmichAssetGridView oldWidget) { - super.didUpdateWidget(oldWidget); - if (!widget.selectionActive) { - setState(() { - _selectedAssets.clear(); - }); - } - } - - @override - void initState() { - super.initState(); - currentAssetLink = ref.read(currentAssetProvider.notifier).ref.keepAlive(); - scrollToTopNotifierProvider.addListener(_scrollToTop); - scrollToDateNotifierProvider.addListener(_scrollToDate); - - if (widget.visibleItemsListener != null) { - _itemPositionsListener.itemPositions.addListener(_positionListener); - } - if (widget.preselectedAssets != null) { - _selectedAssets.addAll(widget.preselectedAssets!); - } - - _itemPositionsListener.itemPositions.addListener(_hapticsListener); - } - - @override - void dispose() { - scrollToTopNotifierProvider.removeListener(_scrollToTop); - scrollToDateNotifierProvider.removeListener(_scrollToDate); - if (widget.visibleItemsListener != null) { - _itemPositionsListener.itemPositions.removeListener(_positionListener); - } - _itemPositionsListener.itemPositions.removeListener(_hapticsListener); - currentAssetLink.close(); - super.dispose(); - } - - void _positionListener() { - final values = _itemPositionsListener.itemPositions.value; - widget.visibleItemsListener?.call(values); - } - - void _hapticsListener() { - /// throttle interval for the haptic feedback in microseconds. - /// Currently set to 100ms. - const feedbackInterval = 100000; - - final values = _itemPositionsListener.itemPositions.value; - final start = values.firstOrNull; - - if (start != null) { - final pos = start.index; - final maxLength = widget.renderList.elements.length; - if (pos < 0 || pos >= maxLength) { - return; - } - - final date = widget.renderList.elements[pos].date; - - // only provide the feedback if the prev. date is known. - // Otherwise the app would provide the haptic feedback - // on startup. - if (_prevItemTime == null) { - _prevItemTime = date; - } else if (_prevItemTime?.year != date.year || _prevItemTime?.month != date.month) { - _prevItemTime = date; - - final now = Timeline.now; - if (now > (_hapticFeedbackTS + feedbackInterval)) { - _hapticFeedbackTS = now; - ref.read(hapticFeedbackProvider.notifier).mediumImpact(); - } - } - } - } - - void _scrollToTop() { - // for some reason, this is necessary as well in order - // to correctly reposition the drag thumb scroll bar - _itemScrollController.jumpTo(index: 0); - _itemScrollController.scrollTo(index: 0, duration: const Duration(milliseconds: 200)); - } - - void _setDragStartIndex(AssetIndex index) { - setState(() { - _scrollPhysics = const ClampingScrollPhysics(); - _dragAnchorAssetIndex = index.rowIndex; - _dragAnchorSectionIndex = index.sectionIndex; - _dragging = true; - }); - } - - void _stopDrag() { - WidgetsBinding.instance.addPostFrameCallback((_) { - // Update the physics post frame to prevent sudden change in physics on iOS. - setState(() { - _scrollPhysics = null; - }); - }); - setState(() { - _dragging = false; - _draggedAssets.clear(); - }); - } - - void _dragDragScroll(ScrollDirection direction) { - _scrollOffsetController.animateScroll( - offset: direction == ScrollDirection.forward ? 175 : -175, - duration: const Duration(milliseconds: 125), - ); - } - - void _handleDragAssetEnter(AssetIndex index) { - if (_dragAnchorSectionIndex == null || _dragAnchorAssetIndex == null) { - return; - } - - final dragAnchorSectionIndex = _dragAnchorSectionIndex!; - final dragAnchorAssetIndex = _dragAnchorAssetIndex!; - - late final int startSectionIndex; - late final int startSectionAssetIndex; - late final int endSectionIndex; - late final int endSectionAssetIndex; - - if (index.sectionIndex < dragAnchorSectionIndex) { - startSectionIndex = index.sectionIndex; - startSectionAssetIndex = index.rowIndex; - endSectionIndex = dragAnchorSectionIndex; - endSectionAssetIndex = dragAnchorAssetIndex; - } else if (index.sectionIndex > dragAnchorSectionIndex) { - startSectionIndex = dragAnchorSectionIndex; - startSectionAssetIndex = dragAnchorAssetIndex; - endSectionIndex = index.sectionIndex; - endSectionAssetIndex = index.rowIndex; - } else { - startSectionIndex = dragAnchorSectionIndex; - endSectionIndex = dragAnchorSectionIndex; - - // If same section, assign proper start / end asset Index - if (dragAnchorAssetIndex < index.rowIndex) { - startSectionAssetIndex = dragAnchorAssetIndex; - endSectionAssetIndex = index.rowIndex; - } else { - startSectionAssetIndex = index.rowIndex; - endSectionAssetIndex = dragAnchorAssetIndex; - } - } - - final selectedAssets = {}; - var currentSectionIndex = startSectionIndex; - while (currentSectionIndex < endSectionIndex) { - final section = widget.renderList.elements.elementAtOrNull(currentSectionIndex); - if (section == null) continue; - - final sectionAssets = widget.renderList.loadAssets(section.offset, section.count); - - if (currentSectionIndex == startSectionIndex) { - selectedAssets.addAll(sectionAssets.slice(startSectionAssetIndex, sectionAssets.length)); - } else { - selectedAssets.addAll(sectionAssets); - } - - currentSectionIndex += 1; - } - - final section = widget.renderList.elements.elementAtOrNull(endSectionIndex); - if (section != null) { - final sectionAssets = widget.renderList.loadAssets(section.offset, section.count); - if (startSectionIndex == endSectionIndex) { - selectedAssets.addAll(sectionAssets.slice(startSectionAssetIndex, endSectionAssetIndex + 1)); - } else { - selectedAssets.addAll(sectionAssets.slice(0, endSectionAssetIndex + 1)); - } - } - - _deselectAssets(_draggedAssets.toList()); - _draggedAssets.clear(); - _draggedAssets.addAll(selectedAssets); - _selectAssets(_draggedAssets.toList()); - } - - @override - Widget build(BuildContext context) { - return PopScope( - canPop: !(widget.selectionActive && _selectedAssets.isNotEmpty), - onPopInvokedWithResult: (didPop, _) { - if (didPop) { - return; - } else { - /// `preselectedAssets` is only present when opening the asset grid from the - /// "add to album" button. - /// - /// `_selectedAssets` includes `preselectedAssets` on initialization. - if (_selectedAssets.length > (widget.preselectedAssets?.length ?? 0)) { - /// `_deselectAll` only deselects the selected assets, - /// doesn't affect the preselected ones. - _deselectAll(); - return; - } else { - Navigator.of(context).canPop() ? Navigator.of(context).pop() : null; - } - } - }, - child: Stack( - children: [ - AssetDragRegion( - onStart: _setDragStartIndex, - onAssetEnter: _handleDragAssetEnter, - onEnd: _stopDrag, - onScroll: _dragDragScroll, - onScrollStart: () => - WidgetsBinding.instance.addPostFrameCallback((_) => controlBottomAppBarNotifier.minimize()), - child: _buildAssetGrid(), - ), - if (widget.showMultiSelectIndicator && widget.selectionActive) _buildMultiSelectIndicator(), - ], - ), - ); - } -} - -/// A single row of all placeholder widgets -class _PlaceholderRow extends StatelessWidget { - final int number; - final double width; - final double height; - final double margin; - - const _PlaceholderRow({ - super.key, - required this.number, - required this.width, - required this.height, - required this.margin, - }); - - @override - Widget build(BuildContext context) { - return Row( - children: [ - for (int i = 0; i < number; i++) - ThumbnailPlaceholder( - key: ValueKey(i), - width: width, - height: height, - margin: EdgeInsets.only(bottom: margin, right: i + 1 == number ? 0.0 : margin), - ), - ], - ); - } -} - -/// A section for the render grid -class _Section extends StatelessWidget { - final RenderAssetGridElement section; - final int sectionIndex; - final Set selectedAssets; - final bool scrolling; - final double margin; - final int assetsPerRow; - final RenderList renderList; - final bool selectionActive; - final bool dynamicLayout; - final void Function(List) selectAssets; - final void Function(List) deselectAssets; - final bool Function(List) allAssetsSelected; - final bool showStack; - final int heroOffset; - final bool showStorageIndicator; - final void Function(Asset) onAssetTap; - - const _Section({ - required this.section, - required this.sectionIndex, - required this.scrolling, - required this.margin, - required this.assetsPerRow, - required this.renderList, - required this.selectionActive, - required this.dynamicLayout, - required this.selectAssets, - required this.deselectAssets, - required this.allAssetsSelected, - required this.selectedAssets, - required this.showStack, - required this.heroOffset, - required this.showStorageIndicator, - required this.onAssetTap, - }); - - @override - Widget build(BuildContext context) { - return LayoutBuilder( - builder: (context, constraints) { - final width = constraints.maxWidth / assetsPerRow - margin * (assetsPerRow - 1) / assetsPerRow; - final rows = (section.count + assetsPerRow - 1) ~/ assetsPerRow; - final List assetsToRender = scrolling ? [] : renderList.loadAssets(section.offset, section.count); - return Column( - key: ValueKey(section.offset), - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (section.type == RenderAssetGridElementType.monthTitle) _MonthTitle(date: section.date), - if (section.type == RenderAssetGridElementType.groupDividerTitle || - section.type == RenderAssetGridElementType.monthTitle) - _Title( - selectionActive: selectionActive, - title: section.title!, - assets: scrolling ? [] : renderList.loadAssets(section.offset, section.totalCount), - allAssetsSelected: allAssetsSelected, - selectAssets: selectAssets, - deselectAssets: deselectAssets, - ), - for (int i = 0; i < rows; i++) - scrolling - ? _PlaceholderRow( - key: ValueKey(i), - number: i + 1 == rows ? section.count - i * assetsPerRow : assetsPerRow, - width: width, - height: width, - margin: margin, - ) - : _AssetRow( - key: ValueKey(i), - rowStartIndex: i * assetsPerRow, - sectionIndex: sectionIndex, - assets: assetsToRender.nestedSlice(i * assetsPerRow, min((i + 1) * assetsPerRow, section.count)), - absoluteOffset: section.offset + i * assetsPerRow, - width: width, - assetsPerRow: assetsPerRow, - margin: margin, - dynamicLayout: dynamicLayout, - renderList: renderList, - selectedAssets: selectedAssets, - isSelectionActive: selectionActive, - showStack: showStack, - heroOffset: heroOffset, - showStorageIndicator: showStorageIndicator, - selectionActive: selectionActive, - onSelect: (asset) => selectAssets([asset]), - onDeselect: (asset) => deselectAssets([asset]), - onAssetTap: onAssetTap, - ), - ], - ); - }, - ); - } -} - -/// The month title row for a section -class _MonthTitle extends StatelessWidget { - final DateTime date; - - const _MonthTitle({required this.date}); - - @override - Widget build(BuildContext context) { - final monthFormat = DateTime.now().year == date.year ? DateFormat.MMMM() : DateFormat.yMMMM(); - final String title = monthFormat.format(date); - return Padding( - key: Key("month-$title"), - padding: const EdgeInsets.only(left: 12.0, top: 24.0), - child: Text( - toBeginningOfSentenceCase(title, context.locale.languageCode), - style: const TextStyle(fontSize: 26, fontWeight: FontWeight.w500), - ), - ); - } -} - -/// A title row -class _Title extends StatelessWidget { - final String title; - final List assets; - final bool selectionActive; - final void Function(List) selectAssets; - final void Function(List) deselectAssets; - final bool Function(List) allAssetsSelected; - - const _Title({ - required this.title, - required this.assets, - required this.selectionActive, - required this.selectAssets, - required this.deselectAssets, - required this.allAssetsSelected, - }); - - @override - Widget build(BuildContext context) { - return GroupDividerTitle( - text: toBeginningOfSentenceCase(title, context.locale.languageCode), - multiselectEnabled: selectionActive, - onSelect: () => selectAssets(assets), - onDeselect: () => deselectAssets(assets), - selected: allAssetsSelected(assets), - ); - } -} - -/// The row of assets -class _AssetRow extends StatelessWidget { - final List assets; - final int rowStartIndex; - final int sectionIndex; - final Set selectedAssets; - final int absoluteOffset; - final double width; - final bool dynamicLayout; - final double margin; - final int assetsPerRow; - final RenderList renderList; - final bool selectionActive; - final bool showStorageIndicator; - final int heroOffset; - final bool showStack; - final void Function(Asset) onAssetTap; - final void Function(Asset)? onSelect; - final void Function(Asset)? onDeselect; - final bool isSelectionActive; - - const _AssetRow({ - super.key, - required this.rowStartIndex, - required this.sectionIndex, - required this.assets, - required this.absoluteOffset, - required this.width, - required this.dynamicLayout, - required this.margin, - required this.assetsPerRow, - required this.renderList, - required this.selectionActive, - required this.showStorageIndicator, - required this.heroOffset, - required this.showStack, - required this.isSelectionActive, - required this.selectedAssets, - required this.onAssetTap, - this.onSelect, - this.onDeselect, - }); - - @override - Widget build(BuildContext context) { - // Default: All assets have the same width - final widthDistribution = List.filled(assets.length, 1.0); - - if (dynamicLayout) { - final aspectRatios = assets.map((e) => (e.width ?? 1) / (e.height ?? 1)).toList(); - final meanAspectRatio = aspectRatios.sum / assets.length; - - // 1: mean width - // 0.5: width < mean - threshold - // 1.5: width > mean + threshold - final arConfiguration = aspectRatios.map((e) { - if (e - meanAspectRatio > 0.3) return 1.5; - if (e - meanAspectRatio < -0.3) return 0.5; - return 1.0; - }); - - // Normalize: - final sum = arConfiguration.sum; - widthDistribution.setRange(0, widthDistribution.length, arConfiguration.map((e) => (e * assets.length) / sum)); - } - return Row( - key: key, - children: assets.mapIndexed((int index, Asset asset) { - final bool last = index + 1 == assetsPerRow; - final isSelected = isSelectionActive && selectedAssets.contains(asset); - return Container( - width: width * widthDistribution[index], - height: width, - margin: EdgeInsets.only(bottom: margin, right: last ? 0.0 : margin), - child: GestureDetector( - onTap: () { - if (selectionActive) { - if (isSelected) { - onDeselect?.call(asset); - } else { - onSelect?.call(asset); - } - } else { - final asset = renderList.loadAsset(absoluteOffset + index); - onAssetTap(asset); - context.pushRoute( - GalleryViewerRoute( - renderList: renderList, - initialIndex: absoluteOffset + index, - heroOffset: heroOffset, - showStack: showStack, - ), - ); - } - }, - onLongPress: () { - onSelect?.call(asset); - HapticFeedback.heavyImpact(); - }, - child: AssetIndexWrapper( - rowIndex: rowStartIndex + index, - sectionIndex: sectionIndex, - child: ThumbnailImage( - asset: asset, - multiselectEnabled: selectionActive, - isSelected: isSelectionActive && selectedAssets.contains(asset), - showStorageIndicator: showStorageIndicator, - heroOffset: heroOffset, - showStack: showStack, - ), - ), - ), - ); - }).toList(), - ); - } -} diff --git a/mobile/lib/widgets/asset_grid/multiselect_grid.dart b/mobile/lib/widgets/asset_grid/multiselect_grid.dart deleted file mode 100644 index c0d8a6bea2..0000000000 --- a/mobile/lib/widgets/asset_grid/multiselect_grid.dart +++ /dev/null @@ -1,458 +0,0 @@ -import 'dart:async'; - -import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:fluttertoast/fluttertoast.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/constants/enums.dart'; -import 'package:immich_mobile/entities/album.entity.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/extensions/collection_extensions.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; -import 'package:immich_mobile/models/asset_selection_state.dart'; -import 'package:immich_mobile/providers/album/album.provider.dart'; -import 'package:immich_mobile/providers/asset.provider.dart'; -import 'package:immich_mobile/providers/asset_viewer/download.provider.dart'; -import 'package:immich_mobile/providers/backup/manual_upload.provider.dart'; -import 'package:immich_mobile/providers/multiselect.provider.dart'; -import 'package:immich_mobile/providers/routes.provider.dart'; -import 'package:immich_mobile/providers/user.provider.dart'; -import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/services/album.service.dart'; -import 'package:immich_mobile/services/stack.service.dart'; -import 'package:immich_mobile/utils/immich_loading_overlay.dart'; -import 'package:immich_mobile/utils/selection_handlers.dart'; -import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart'; -import 'package:immich_mobile/widgets/asset_grid/control_bottom_app_bar.dart'; -import 'package:immich_mobile/widgets/asset_grid/immich_asset_grid.dart'; -import 'package:immich_mobile/widgets/common/immich_toast.dart'; - -class MultiselectGrid extends HookConsumerWidget { - const MultiselectGrid({ - super.key, - required this.renderListProvider, - this.onRefresh, - this.buildLoadingIndicator, - this.onRemoveFromAlbum, - this.topWidget, - this.stackEnabled = false, - this.dragScrollLabelEnabled = true, - this.archiveEnabled = false, - this.deleteEnabled = true, - this.favoriteEnabled = true, - this.editEnabled = false, - this.unarchive = false, - this.unfavorite = false, - this.downloadEnabled = true, - this.emptyIndicator, - }); - - final ProviderListenable> renderListProvider; - final Future Function()? onRefresh; - final Widget Function()? buildLoadingIndicator; - final Future Function(Iterable)? onRemoveFromAlbum; - final Widget? topWidget; - final bool stackEnabled; - final bool dragScrollLabelEnabled; - final bool archiveEnabled; - final bool unarchive; - final bool deleteEnabled; - final bool downloadEnabled; - final bool favoriteEnabled; - final bool unfavorite; - final bool editEnabled; - final Widget? emptyIndicator; - Widget buildDefaultLoadingIndicator() => const Center(child: CircularProgressIndicator()); - - Widget buildEmptyIndicator() => emptyIndicator ?? Center(child: const Text("no_assets_to_show").tr()); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final multiselectEnabled = ref.watch(multiselectProvider.notifier); - final selectionEnabledHook = useState(false); - final selectionAssetState = useState(const AssetSelectionState()); - - final selection = useState({}); - final currentUser = ref.watch(currentUserProvider); - final processing = useProcessingOverlay(); - - useEffect(() { - selectionEnabledHook.addListener(() { - multiselectEnabled.state = selectionEnabledHook.value; - }); - - return () { - // This does not work in tests - if (kReleaseMode) { - selectionEnabledHook.dispose(); - } - }; - }, []); - - void selectionListener(bool multiselect, Set selectedAssets) { - selectionEnabledHook.value = multiselect; - selection.value = selectedAssets; - selectionAssetState.value = AssetSelectionState.fromSelection(selectedAssets); - } - - errorBuilder(String? msg) => msg != null && msg.isNotEmpty - ? () => ImmichToast.show(context: context, msg: msg, gravity: ToastGravity.BOTTOM) - : null; - - Iterable ownedRemoteSelection({String? localErrorMessage, String? ownerErrorMessage}) { - final assets = selection.value; - return assets - .remoteOnly(errorCallback: errorBuilder(localErrorMessage)) - .ownedOnly(currentUser, errorCallback: errorBuilder(ownerErrorMessage)); - } - - Iterable remoteSelection({String? errorMessage}) => - selection.value.remoteOnly(errorCallback: errorBuilder(errorMessage)); - - void onShareAssets(bool shareLocal) { - processing.value = true; - if (shareLocal) { - // Share = Download + Send to OS specific share sheet - handleShareAssets(ref, context, selection.value); - } else { - final ids = remoteSelection(errorMessage: "home_page_share_err_local".tr()).map((e) => e.remoteId!); - context.pushRoute(SharedLinkEditRoute(assetsList: ids.toList())); - } - processing.value = false; - selectionEnabledHook.value = false; - } - - void onFavoriteAssets() async { - processing.value = true; - try { - final remoteAssets = ownedRemoteSelection( - localErrorMessage: 'home_page_favorite_err_local'.tr(), - ownerErrorMessage: 'home_page_favorite_err_partner'.tr(), - ); - if (remoteAssets.isNotEmpty) { - await handleFavoriteAssets(ref, context, remoteAssets.toList()); - } - } finally { - processing.value = false; - selectionEnabledHook.value = false; - } - } - - void onArchiveAsset() async { - processing.value = true; - try { - final remoteAssets = ownedRemoteSelection( - localErrorMessage: 'home_page_archive_err_local'.tr(), - ownerErrorMessage: 'home_page_archive_err_partner'.tr(), - ); - await handleArchiveAssets(ref, context, remoteAssets.toList()); - } finally { - processing.value = false; - selectionEnabledHook.value = false; - } - } - - void onDelete([bool force = false]) async { - processing.value = true; - try { - final toDelete = selection.value - .ownedOnly(currentUser, errorCallback: errorBuilder('home_page_delete_err_partner'.tr())) - .toList(); - final isDeleted = await ref.read(assetProvider.notifier).deleteAssets(toDelete, force: force); - - if (isDeleted) { - ImmichToast.show( - context: context, - msg: force - ? 'assets_deleted_permanently'.tr(namedArgs: {'count': "${selection.value.length}"}) - : 'assets_trashed'.tr(namedArgs: {'count': "${selection.value.length}"}), - gravity: ToastGravity.BOTTOM, - ); - selectionEnabledHook.value = false; - } - } finally { - processing.value = false; - } - } - - void onDeleteLocal(bool isMergedAsset) async { - processing.value = true; - try { - final localAssets = selection.value.where((a) => a.isLocal).toList(); - - final toDelete = isMergedAsset ? localAssets.where((e) => e.storage == AssetState.merged) : localAssets; - - if (toDelete.isEmpty) { - return; - } - - final isDeleted = await ref.read(assetProvider.notifier).deleteLocalAssets(toDelete.toList()); - - if (isDeleted) { - final deletedCount = localAssets.where((e) => !isMergedAsset || e.isRemote).length; - - ImmichToast.show( - context: context, - msg: 'assets_removed_permanently_from_device'.tr(namedArgs: {'count': "$deletedCount"}), - gravity: ToastGravity.BOTTOM, - ); - - selectionEnabledHook.value = false; - } - } finally { - processing.value = false; - } - } - - void onDownload() async { - processing.value = true; - try { - final toDownload = selection.value.toList(); - - final results = await ref.read(downloadStateProvider.notifier).downloadAllAsset(toDownload); - - final totalCount = toDownload.length; - final successCount = results.where((e) => e).length; - final failedCount = totalCount - successCount; - - final msg = failedCount > 0 - ? 'assets_downloaded_failed'.t(context: context, args: {'count': successCount, 'error': failedCount}) - : 'assets_downloaded_successfully'.t(context: context, args: {'count': successCount}); - - ImmichToast.show(context: context, msg: msg, gravity: ToastGravity.BOTTOM); - } finally { - processing.value = false; - selectionEnabledHook.value = false; - } - } - - void onDeleteRemote([bool shouldDeletePermanently = false]) async { - processing.value = true; - try { - final toDelete = ownedRemoteSelection( - localErrorMessage: 'home_page_delete_remote_err_local'.tr(), - ownerErrorMessage: 'home_page_delete_err_partner'.tr(), - ).toList(); - - final isDeleted = await ref - .read(assetProvider.notifier) - .deleteRemoteAssets(toDelete, shouldDeletePermanently: shouldDeletePermanently); - if (isDeleted) { - ImmichToast.show( - context: context, - msg: shouldDeletePermanently - ? 'assets_deleted_permanently_from_server'.tr(namedArgs: {'count': "${toDelete.length}"}) - : 'assets_trashed_from_server'.tr(namedArgs: {'count': "${toDelete.length}"}), - gravity: ToastGravity.BOTTOM, - ); - } - } finally { - selectionEnabledHook.value = false; - processing.value = false; - } - } - - void onUpload() { - processing.value = true; - selectionEnabledHook.value = false; - try { - ref - .read(manualUploadProvider.notifier) - .uploadAssets(context, selection.value.where((a) => a.storage == AssetState.local)); - } finally { - processing.value = false; - } - } - - void onAddToAlbum(Album album) async { - processing.value = true; - try { - final Iterable assets = remoteSelection(errorMessage: "home_page_add_to_album_err_local".tr()); - if (assets.isEmpty) { - return; - } - final result = await ref.read(albumServiceProvider).addAssets(album, assets); - - if (result != null) { - if (result.alreadyInAlbum.isNotEmpty) { - ImmichToast.show( - context: context, - msg: "home_page_add_to_album_conflicts".tr( - namedArgs: { - "album": album.name, - "added": result.successfullyAdded.toString(), - "failed": result.alreadyInAlbum.length.toString(), - }, - ), - ); - } else { - ImmichToast.show( - context: context, - msg: "home_page_add_to_album_success".tr( - namedArgs: {"album": album.name, "added": result.successfullyAdded.toString()}, - ), - toastType: ToastType.success, - ); - } - } - } finally { - processing.value = false; - selectionEnabledHook.value = false; - } - } - - void onCreateNewAlbum() async { - processing.value = true; - try { - final Iterable assets = remoteSelection(errorMessage: "home_page_add_to_album_err_local".tr()); - if (assets.isEmpty) { - return; - } - final result = await ref.read(albumServiceProvider).createAlbumWithGeneratedName(assets); - - if (result != null) { - unawaited(ref.watch(albumProvider.notifier).refreshRemoteAlbums()); - selectionEnabledHook.value = false; - - unawaited(context.pushRoute(AlbumViewerRoute(albumId: result.id))); - } - } finally { - processing.value = false; - } - } - - void onStack() async { - try { - processing.value = true; - if (!selectionEnabledHook.value || selection.value.length < 2) { - return; - } - - await ref.read(stackServiceProvider).createStack(selection.value.map((e) => e.remoteId!).toList()); - } finally { - processing.value = false; - selectionEnabledHook.value = false; - } - } - - void onEditTime() async { - try { - final remoteAssets = ownedRemoteSelection( - localErrorMessage: 'home_page_favorite_err_local'.tr(), - ownerErrorMessage: 'home_page_favorite_err_partner'.tr(), - ); - - if (remoteAssets.isNotEmpty) { - unawaited(handleEditDateTime(ref, context, remoteAssets.toList())); - } - } finally { - selectionEnabledHook.value = false; - } - } - - void onEditLocation() async { - try { - final remoteAssets = ownedRemoteSelection( - localErrorMessage: 'home_page_favorite_err_local'.tr(), - ownerErrorMessage: 'home_page_favorite_err_partner'.tr(), - ); - - if (remoteAssets.isNotEmpty) { - unawaited(handleEditLocation(ref, context, remoteAssets.toList())); - } - } finally { - selectionEnabledHook.value = false; - } - } - - void onToggleLockedVisibility() async { - processing.value = true; - try { - final remoteAssets = ownedRemoteSelection( - localErrorMessage: 'home_page_locked_error_local'.tr(), - ownerErrorMessage: 'home_page_locked_error_partner'.tr(), - ); - if (remoteAssets.isNotEmpty) { - final isInLockedView = ref.read(inLockedViewProvider); - final visibility = isInLockedView ? AssetVisibilityEnum.timeline : AssetVisibilityEnum.locked; - - await handleSetAssetsVisibility(ref, context, visibility, remoteAssets.toList()); - } - } finally { - processing.value = false; - selectionEnabledHook.value = false; - } - } - - Future Function() wrapLongRunningFun(Future Function() fun, {bool showOverlay = true}) => () async { - if (showOverlay) processing.value = true; - try { - final result = await fun(); - if (result.runtimeType != bool || result == true) { - selectionEnabledHook.value = false; - } - return result; - } finally { - if (showOverlay) processing.value = false; - } - }; - - return SafeArea( - top: true, - bottom: false, - child: Stack( - children: [ - ref - .watch(renderListProvider) - .when( - data: (data) => data.isEmpty && (buildLoadingIndicator != null || topWidget == null) - ? (buildLoadingIndicator ?? buildEmptyIndicator)() - : ImmichAssetGrid( - renderList: data, - listener: selectionListener, - selectionActive: selectionEnabledHook.value, - onRefresh: onRefresh == null ? null : wrapLongRunningFun(onRefresh!, showOverlay: false), - topWidget: topWidget, - showStack: stackEnabled, - showDragScrollLabel: dragScrollLabelEnabled, - ), - error: (error, _) => Center(child: Text(error.toString())), - loading: buildLoadingIndicator ?? buildDefaultLoadingIndicator, - ), - if (selectionEnabledHook.value) - ControlBottomAppBar( - key: const ValueKey("controlBottomAppBar"), - onShare: onShareAssets, - onFavorite: favoriteEnabled ? onFavoriteAssets : null, - onArchive: archiveEnabled ? onArchiveAsset : null, - onDelete: deleteEnabled ? onDelete : null, - onDeleteServer: deleteEnabled ? onDeleteRemote : null, - onDownload: downloadEnabled ? onDownload : null, - - /// local file deletion is allowed irrespective of [deleteEnabled] since it has - /// nothing to do with the state of the asset in the Immich server - onDeleteLocal: onDeleteLocal, - onAddToAlbum: onAddToAlbum, - onCreateNewAlbum: onCreateNewAlbum, - onUpload: onUpload, - enabled: !processing.value, - selectionAssetState: selectionAssetState.value, - selectedAssets: selection.value.toList(), - onStack: stackEnabled ? onStack : null, - onEditTime: editEnabled ? onEditTime : null, - onEditLocation: editEnabled ? onEditLocation : null, - unfavorite: unfavorite, - unarchive: unarchive, - onToggleLocked: onToggleLockedVisibility, - onRemoveFromAlbum: onRemoveFromAlbum != null - ? wrapLongRunningFun(() => onRemoveFromAlbum!(selection.value)) - : null, - ), - ], - ), - ); - } -} diff --git a/mobile/lib/widgets/asset_grid/multiselect_grid_status_indicator.dart b/mobile/lib/widgets/asset_grid/multiselect_grid_status_indicator.dart deleted file mode 100644 index 3a1fa82a28..0000000000 --- a/mobile/lib/widgets/asset_grid/multiselect_grid_status_indicator.dart +++ /dev/null @@ -1,26 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/providers/asset_viewer/render_list_status_provider.dart'; -import 'package:immich_mobile/widgets/common/delayed_loading_indicator.dart'; - -class MultiselectGridStatusIndicator extends HookConsumerWidget { - const MultiselectGridStatusIndicator({super.key, this.buildLoadingIndicator, this.emptyIndicator}); - - final Widget Function()? buildLoadingIndicator; - final Widget? emptyIndicator; - - @override - Widget build(BuildContext context, WidgetRef ref) { - final renderListStatus = ref.watch(renderListStatusProvider); - return switch (renderListStatus) { - RenderListStatusEnum.loading => - buildLoadingIndicator == null - ? const Center(child: DelayedLoadingIndicator(delay: Duration(milliseconds: 500))) - : buildLoadingIndicator!(), - RenderListStatusEnum.empty => emptyIndicator ?? Center(child: const Text("no_assets_to_show").tr()), - RenderListStatusEnum.error => Center(child: const Text("error_loading_assets").tr()), - RenderListStatusEnum.complete => const SizedBox(), - }; - } -} diff --git a/mobile/lib/widgets/asset_grid/thumbnail_image.dart b/mobile/lib/widgets/asset_grid/thumbnail_image.dart deleted file mode 100644 index 93385b88b3..0000000000 --- a/mobile/lib/widgets/asset_grid/thumbnail_image.dart +++ /dev/null @@ -1,259 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:immich_mobile/constants/constants.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/extensions/duration_extensions.dart'; -import 'package:immich_mobile/extensions/theme_extensions.dart'; -import 'package:immich_mobile/widgets/common/immich_thumbnail.dart'; - -class ThumbnailImage extends StatelessWidget { - /// The asset to show the thumbnail image for - final Asset asset; - - /// Whether to show the storage indicator icont over the image or not - final bool showStorageIndicator; - - /// Whether to show the show stack icon over the image or not - final bool showStack; - - /// Whether to show the checkmark indicating that this image is selected - final bool isSelected; - - /// Can override [isSelected] and never show the selection indicator - final bool multiselectEnabled; - - /// If we are allowed to deselect this image - final bool canDeselect; - - /// The offset index to apply to this hero tag for animation - final int heroOffset; - - const ThumbnailImage({ - super.key, - required this.asset, - this.showStorageIndicator = true, - this.showStack = false, - this.isSelected = false, - this.multiselectEnabled = false, - this.heroOffset = 0, - this.canDeselect = true, - }); - - @override - Widget build(BuildContext context) { - final assetContainerColor = context.isDarkTheme - ? context.primaryColor.darken(amount: 0.6) - : context.primaryColor.lighten(amount: 0.8); - - return Stack( - children: [ - AnimatedContainer( - duration: const Duration(milliseconds: 300), - curve: Curves.decelerate, - decoration: BoxDecoration( - border: multiselectEnabled && isSelected - ? canDeselect - ? Border.all(color: assetContainerColor, width: 8) - : const Border( - top: BorderSide(color: Colors.grey, width: 8), - right: BorderSide(color: Colors.grey, width: 8), - bottom: BorderSide(color: Colors.grey, width: 8), - left: BorderSide(color: Colors.grey, width: 8), - ) - : const Border(), - ), - child: Stack( - children: [ - _ImageIcon( - heroOffset: heroOffset, - asset: asset, - assetContainerColor: assetContainerColor, - multiselectEnabled: multiselectEnabled, - canDeselect: canDeselect, - isSelected: isSelected, - ), - if (showStorageIndicator) _StorageIcon(storage: asset.storage), - if (asset.isFavorite) - const Positioned(left: 8, bottom: 5, child: Icon(Icons.favorite, color: Colors.white, size: 16)), - if (asset.isVideo) _VideoIcon(duration: asset.duration), - if (asset.stackCount > 0) _StackIcon(isVideo: asset.isVideo, stackCount: asset.stackCount), - ], - ), - ), - if (multiselectEnabled) - isSelected - ? const Padding( - padding: EdgeInsets.all(3.0), - child: Align(alignment: Alignment.topLeft, child: _SelectedIcon()), - ) - : const Icon(Icons.circle_outlined, color: Colors.white), - ], - ); - } -} - -class _SelectedIcon extends StatelessWidget { - const _SelectedIcon(); - - @override - Widget build(BuildContext context) { - final assetContainerColor = context.isDarkTheme - ? context.primaryColor.darken(amount: 0.6) - : context.primaryColor.lighten(amount: 0.8); - - return DecoratedBox( - decoration: BoxDecoration(shape: BoxShape.circle, color: assetContainerColor), - child: Icon(Icons.check_circle_rounded, color: context.primaryColor), - ); - } -} - -class _VideoIcon extends StatelessWidget { - final Duration duration; - - const _VideoIcon({required this.duration}); - - @override - Widget build(BuildContext context) { - return Positioned( - top: 5, - right: 8, - child: Row( - children: [ - Text( - duration.format(), - style: const TextStyle(color: Colors.white, fontSize: 10, fontWeight: FontWeight.bold), - ), - const SizedBox(width: 3), - const Icon(Icons.play_circle_fill_rounded, color: Colors.white, size: 18), - ], - ), - ); - } -} - -class _StackIcon extends StatelessWidget { - final bool isVideo; - final int stackCount; - - const _StackIcon({required this.isVideo, required this.stackCount}); - - @override - Widget build(BuildContext context) { - return Positioned( - top: isVideo ? 28 : 5, - right: 8, - child: Row( - children: [ - if (stackCount > 1) - Text( - "$stackCount", - style: const TextStyle(color: Colors.white, fontSize: 10, fontWeight: FontWeight.bold), - ), - if (stackCount > 1) const SizedBox(width: 3), - const Icon(Icons.burst_mode_rounded, color: Colors.white, size: 18), - ], - ), - ); - } -} - -class _StorageIcon extends StatelessWidget { - final AssetState storage; - - const _StorageIcon({required this.storage}); - - @override - Widget build(BuildContext context) { - return switch (storage) { - AssetState.local => const Positioned( - right: 8, - bottom: 5, - child: Icon( - Icons.cloud_off_outlined, - color: Color.fromRGBO(255, 255, 255, 0.8), - size: 16, - shadows: [Shadow(blurRadius: 5.0, color: Color.fromRGBO(0, 0, 0, 0.6), offset: Offset(0.0, 0.0))], - ), - ), - AssetState.remote => const Positioned( - right: 8, - bottom: 5, - child: Icon( - Icons.cloud_outlined, - color: Color.fromRGBO(255, 255, 255, 0.8), - size: 16, - shadows: [Shadow(blurRadius: 5.0, color: Color.fromRGBO(0, 0, 0, 0.6), offset: Offset(0.0, 0.0))], - ), - ), - AssetState.merged => const Positioned( - right: 8, - bottom: 5, - child: Icon( - Icons.cloud_done_outlined, - color: Color.fromRGBO(255, 255, 255, 0.8), - size: 16, - shadows: [Shadow(blurRadius: 5.0, color: Color.fromRGBO(0, 0, 0, 0.6), offset: Offset(0.0, 0.0))], - ), - ), - }; - } -} - -class _ImageIcon extends StatelessWidget { - final int heroOffset; - final Asset asset; - final Color assetContainerColor; - final bool multiselectEnabled; - final bool canDeselect; - final bool isSelected; - - const _ImageIcon({ - required this.heroOffset, - required this.asset, - required this.assetContainerColor, - required this.multiselectEnabled, - required this.canDeselect, - required this.isSelected, - }); - - @override - Widget build(BuildContext context) { - // Assets from response DTOs do not have an isar id, querying which would give us the default autoIncrement id - final isDto = asset.id == noDbId; - final image = SizedBox.expand( - child: Hero( - tag: isDto ? '${asset.remoteId}-$heroOffset' : asset.id + heroOffset, - child: Stack( - children: [ - SizedBox.expand(child: ImmichThumbnail(asset: asset, height: 250, width: 250)), - const DecoratedBox( - decoration: BoxDecoration( - gradient: LinearGradient( - colors: [ - Color.fromRGBO(0, 0, 0, 0.1), - Colors.transparent, - Colors.transparent, - Color.fromRGBO(0, 0, 0, 0.1), - ], - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - stops: [0, 0.3, 0.6, 1], - ), - ), - ), - ], - ), - ), - ); - - if (!multiselectEnabled || !isSelected) { - return image; - } - - return DecoratedBox( - decoration: canDeselect ? BoxDecoration(color: assetContainerColor) : const BoxDecoration(color: Colors.grey), - child: ClipRRect(borderRadius: const BorderRadius.all(Radius.circular(15.0)), child: image), - ); - } -} diff --git a/mobile/lib/widgets/asset_grid/upload_dialog.dart b/mobile/lib/widgets/asset_grid/upload_dialog.dart deleted file mode 100644 index 86e2759566..0000000000 --- a/mobile/lib/widgets/asset_grid/upload_dialog.dart +++ /dev/null @@ -1,14 +0,0 @@ -import 'package:immich_mobile/widgets/common/confirm_dialog.dart'; - -class UploadDialog extends ConfirmDialog { - final Function onUpload; - - const UploadDialog({super.key, required this.onUpload}) - : super( - title: 'upload_dialog_title', - content: 'upload_dialog_info', - cancel: 'cancel', - ok: 'upload', - onOk: onUpload, - ); -} diff --git a/mobile/lib/widgets/asset_viewer/advanced_bottom_sheet.dart b/mobile/lib/widgets/asset_viewer/advanced_bottom_sheet.dart deleted file mode 100644 index 1a3ef3eac3..0000000000 --- a/mobile/lib/widgets/asset_viewer/advanced_bottom_sheet.dart +++ /dev/null @@ -1,77 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; - -class AdvancedBottomSheet extends HookConsumerWidget { - final Asset assetDetail; - final ScrollController? scrollController; - - const AdvancedBottomSheet({super.key, required this.assetDetail, this.scrollController}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - return SingleChildScrollView( - controller: scrollController, - child: Container( - margin: const EdgeInsets.symmetric(horizontal: 8.0), - child: LayoutBuilder( - builder: (context, constraints) { - // One column - return Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - const Align(child: Text("ADVANCED INFO", style: TextStyle(fontSize: 12.0))), - const SizedBox(height: 32.0), - Container( - decoration: BoxDecoration( - color: context.isDarkTheme ? Colors.grey[900] : Colors.grey[200], - borderRadius: const BorderRadius.all(Radius.circular(15.0)), - ), - child: Padding( - padding: const EdgeInsets.only(right: 16.0, left: 16, top: 8, bottom: 16), - child: ListView( - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - children: [ - Align( - alignment: Alignment.centerRight, - child: IconButton( - onPressed: () { - Clipboard.setData(ClipboardData(text: assetDetail.toString())).then((_) { - context.scaffoldMessenger.showSnackBar( - SnackBar( - content: Text( - "Copied to clipboard", - style: context.textTheme.bodyLarge?.copyWith(color: context.primaryColor), - ), - ), - ); - }); - }, - icon: Icon(Icons.copy, size: 16.0, color: context.primaryColor), - ), - ), - SelectableText( - assetDetail.toString(), - style: const TextStyle( - fontSize: 12.0, - fontWeight: FontWeight.bold, - fontFamily: "GoogleSansCode", - ), - showCursor: true, - ), - ], - ), - ), - ), - const SizedBox(height: 32.0), - ], - ); - }, - ), - ), - ); - } -} diff --git a/mobile/lib/widgets/asset_viewer/bottom_gallery_bar.dart b/mobile/lib/widgets/asset_viewer/bottom_gallery_bar.dart deleted file mode 100644 index 22a7deffff..0000000000 --- a/mobile/lib/widgets/asset_viewer/bottom_gallery_bar.dart +++ /dev/null @@ -1,362 +0,0 @@ -import 'dart:async'; -import 'dart:io'; - -import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:fluttertoast/fluttertoast.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/pages/editing/edit.page.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/asset.provider.dart'; -import 'package:immich_mobile/providers/asset_viewer/asset_stack.provider.dart'; -import 'package:immich_mobile/providers/asset_viewer/current_asset.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/routes.provider.dart'; -import 'package:immich_mobile/providers/server_info.provider.dart'; -import 'package:immich_mobile/providers/user.provider.dart'; -import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/services/stack.service.dart'; -import 'package:immich_mobile/utils/hash.dart'; -import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart'; -import 'package:immich_mobile/widgets/asset_grid/delete_dialog.dart'; -import 'package:immich_mobile/widgets/asset_viewer/video_controls.dart'; -import 'package:immich_mobile/widgets/common/immich_image.dart'; -import 'package:immich_mobile/widgets/common/immich_toast.dart'; - -class BottomGalleryBar extends ConsumerWidget { - final ValueNotifier assetIndex; - final bool showStack; - final ValueNotifier stackIndex; - final ValueNotifier totalAssets; - final PageController controller; - final RenderList renderList; - - const BottomGalleryBar({ - super.key, - required this.showStack, - required this.stackIndex, - required this.assetIndex, - required this.controller, - required this.totalAssets, - required this.renderList, - }); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final isInLockedView = ref.watch(inLockedViewProvider); - final asset = ref.watch(currentAssetProvider); - if (asset == null) { - return const SizedBox(); - } - final isOwner = asset.ownerId == fastHash(ref.watch(currentUserProvider)?.id ?? ''); - final showControls = ref.watch(showControlsProvider); - final stackId = asset.stackId; - - final stackItems = showStack && stackId != null ? ref.watch(assetStackStateProvider(stackId)) : []; - bool isStackPrimaryAsset = asset.stackPrimaryAssetId == null; - final navStack = AutoRouter.of(context).stackData; - final isTrashEnabled = ref.watch(serverInfoProvider.select((v) => v.serverFeatures.trash)); - final isFromTrash = - isTrashEnabled && navStack.length > 2 && navStack.elementAt(navStack.length - 2).name == TrashRoute.name; - final isInAlbum = ref.watch(currentAlbumProvider)?.isRemote ?? false; - - void removeAssetFromStack() { - if (stackIndex.value > 0 && showStack && stackId != null) { - ref.read(assetStackStateProvider(stackId).notifier).removeChild(stackIndex.value - 1); - } - } - - void handleDelete() async { - Future onDelete(bool force) async { - final isDeleted = await ref.read(assetProvider.notifier).deleteAssets({asset}, force: force); - if (isDeleted && isStackPrimaryAsset) { - // Workaround for asset remaining in the gallery - renderList.deleteAsset(asset); - - // `assetIndex == totalAssets.value - 1` handle the case of removing the last asset - // to not throw the error when the next preCache index is called - if (totalAssets.value == 1 || assetIndex.value == totalAssets.value - 1) { - // Handle only one asset - await context.maybePop(); - } - - totalAssets.value -= 1; - } - if (isDeleted) { - ref.read(currentAssetProvider.notifier).set(renderList.loadAsset(assetIndex.value)); - } - return isDeleted; - } - - // Asset is trashed - if (isTrashEnabled && !isFromTrash) { - final isDeleted = await onDelete(false); - if (isDeleted) { - // Can only trash assets stored in server. Local assets are always permanently removed for now - if (context.mounted && asset.isRemote && isStackPrimaryAsset) { - ImmichToast.show( - durationInSecond: 1, - context: context, - msg: 'asset_trashed'.tr(), - gravity: ToastGravity.BOTTOM, - ); - } - removeAssetFromStack(); - } - return; - } - - // Asset is permanently removed - unawaited( - showDialog( - context: context, - builder: (BuildContext _) { - return DeleteDialog( - onDelete: () async { - final isDeleted = await onDelete(true); - if (isDeleted) { - removeAssetFromStack(); - } - }, - ); - }, - ), - ); - } - - unStack() async { - if (asset.stackId == null) { - return; - } - - await ref.read(stackServiceProvider).deleteStack(asset.stackId!, stackItems); - } - - void showStackActionItems() { - showModalBottomSheet( - context: context, - enableDrag: false, - builder: (BuildContext ctx) { - return SafeArea( - child: Padding( - padding: const EdgeInsets.only(top: 24.0), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - ListTile( - leading: const Icon(Icons.filter_none_outlined, size: 18), - onTap: () async { - await unStack(); - ctx.pop(); - await context.maybePop(); - }, - title: const Text("viewer_unstack", style: TextStyle(fontWeight: FontWeight.bold)).tr(), - ), - ], - ), - ), - ); - }, - ); - } - - shareAsset() { - if (asset.isOffline) { - ImmichToast.show( - durationInSecond: 1, - context: context, - msg: 'asset_action_share_err_offline'.tr(), - gravity: ToastGravity.BOTTOM, - ); - return; - } - ref.read(downloadStateProvider.notifier).shareAsset(asset, context); - } - - void handleEdit() async { - final image = Image(image: ImmichImage.imageProvider(asset: asset)); - - unawaited( - context.navigator.push( - MaterialPageRoute( - builder: (context) => EditImagePage(asset: asset, image: image, isEdited: false), - ), - ), - ); - } - - handleArchive() { - ref.read(assetProvider.notifier).toggleArchive([asset]); - if (isStackPrimaryAsset) { - context.maybePop(); - return; - } - removeAssetFromStack(); - } - - handleDownload() { - if (asset.isLocal) { - return; - } - if (asset.isOffline) { - ImmichToast.show( - durationInSecond: 1, - context: context, - msg: 'asset_action_share_err_offline'.tr(), - gravity: ToastGravity.BOTTOM, - ); - return; - } - - ref.read(downloadStateProvider.notifier).downloadAsset(asset); - } - - handleRemoveFromAlbum() async { - final album = ref.read(currentAlbumProvider); - final bool isSuccess = album != null && await ref.read(albumProvider.notifier).removeAsset(album, [asset]); - - if (isSuccess) { - // Workaround for asset remaining in the gallery - renderList.deleteAsset(asset); - - if (totalAssets.value == 1) { - // Handle empty viewer - await context.maybePop(); - } else { - // changing this also for the last asset causes the parent to rebuild with an error - totalAssets.value -= 1; - } - if (assetIndex.value == totalAssets.value && assetIndex.value > 0) { - // handle the case of removing the last asset in the list - assetIndex.value -= 1; - } - } else { - ImmichToast.show( - context: context, - msg: "album_viewer_appbar_share_err_remove".tr(), - toastType: ToastType.error, - gravity: ToastGravity.BOTTOM, - ); - } - } - - final List> albumActions = [ - { - BottomNavigationBarItem( - icon: Icon(Platform.isAndroid ? Icons.share_rounded : Icons.ios_share_rounded), - label: 'share'.tr(), - tooltip: 'share'.tr(), - ): (_) => - shareAsset(), - }, - if (asset.isImage && !isInLockedView) - { - BottomNavigationBarItem( - icon: const Icon(Icons.tune_outlined), - label: 'edit'.tr(), - tooltip: 'edit'.tr(), - ): (_) => - handleEdit(), - }, - if (isOwner && !isInLockedView) - { - asset.isArchived - ? BottomNavigationBarItem( - icon: const Icon(Icons.unarchive_rounded), - label: 'unarchive'.tr(), - tooltip: 'unarchive'.tr(), - ) - : BottomNavigationBarItem( - icon: const Icon(Icons.archive_outlined), - label: 'archive'.tr(), - tooltip: 'archive'.tr(), - ): (_) => - handleArchive(), - }, - if (isOwner && asset.stackCount > 0 && !isInLockedView) - { - BottomNavigationBarItem( - icon: const Icon(Icons.burst_mode_outlined), - label: 'stack'.tr(), - tooltip: 'stack'.tr(), - ): (_) => - showStackActionItems(), - }, - if (isOwner && !isInAlbum) - { - BottomNavigationBarItem( - icon: const Icon(Icons.delete_outline), - label: 'delete'.tr(), - tooltip: 'delete'.tr(), - ): (_) => - handleDelete(), - }, - if (!isOwner) - { - BottomNavigationBarItem( - icon: const Icon(Icons.download_outlined), - label: 'download'.tr(), - tooltip: 'download'.tr(), - ): (_) => - handleDownload(), - }, - if (isInAlbum) - { - BottomNavigationBarItem( - icon: const Icon(Icons.remove_circle_outline), - label: 'remove_from_album'.tr(), - tooltip: 'remove_from_album'.tr(), - ): (_) => - handleRemoveFromAlbum(), - }, - ]; - return IgnorePointer( - ignoring: !showControls, - child: AnimatedOpacity( - duration: const Duration(milliseconds: 100), - opacity: showControls ? 1.0 : 0.0, - child: DecoratedBox( - decoration: const BoxDecoration( - gradient: LinearGradient( - begin: Alignment.bottomCenter, - end: Alignment.topCenter, - colors: [Colors.black, Colors.transparent], - ), - ), - position: DecorationPosition.background, - child: Padding( - padding: const EdgeInsets.only(top: 40.0), - child: Column( - children: [ - if (asset.isVideo) VideoControls(videoPlayerName: asset.id.toString()), - BottomNavigationBar( - elevation: 0.0, - backgroundColor: Colors.transparent, - unselectedIconTheme: const IconThemeData(color: Colors.white), - selectedIconTheme: const IconThemeData(color: Colors.white), - unselectedLabelStyle: const TextStyle(color: Colors.white, fontWeight: FontWeight.w500, height: 2.3), - selectedLabelStyle: const TextStyle(color: Colors.white, fontWeight: FontWeight.w500, height: 2.3), - unselectedFontSize: 14, - selectedFontSize: 14, - selectedItemColor: Colors.white, - unselectedItemColor: Colors.white, - showSelectedLabels: true, - showUnselectedLabels: true, - items: albumActions.map((e) => e.keys.first).toList(growable: false), - onTap: (index) { - albumActions[index].values.first.call(index); - }, - ), - ], - ), - ), - ), - ), - ); - } -} diff --git a/mobile/lib/widgets/asset_viewer/center_play_button.dart b/mobile/lib/widgets/asset_viewer/center_play_button.dart deleted file mode 100644 index 55d8be8095..0000000000 --- a/mobile/lib/widgets/asset_viewer/center_play_button.dart +++ /dev/null @@ -1,44 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:immich_mobile/widgets/asset_viewer/animated_play_pause.dart'; - -class CenterPlayButton extends StatelessWidget { - const CenterPlayButton({ - super.key, - required this.backgroundColor, - this.iconColor, - required this.show, - required this.isPlaying, - required this.isFinished, - this.onPressed, - }); - - final Color backgroundColor; - final Color? iconColor; - final bool show; - final bool isPlaying; - final bool isFinished; - final VoidCallback? onPressed; - - @override - Widget build(BuildContext context) { - return Center( - child: UnconstrainedBox( - child: AnimatedOpacity( - opacity: show ? 1.0 : 0.0, - duration: const Duration(milliseconds: 100), - child: DecoratedBox( - decoration: BoxDecoration(color: backgroundColor, shape: BoxShape.circle), - child: IconButton( - iconSize: 32, - padding: const EdgeInsets.all(12.0), - icon: isFinished - ? Icon(Icons.replay, color: iconColor) - : AnimatedPlayPause(color: iconColor, playing: isPlaying), - onPressed: onPressed, - ), - ), - ), - ), - ); - } -} diff --git a/mobile/lib/widgets/asset_viewer/custom_video_player_controls.dart b/mobile/lib/widgets/asset_viewer/custom_video_player_controls.dart deleted file mode 100644 index 09c0e9d091..0000000000 --- a/mobile/lib/widgets/asset_viewer/custom_video_player_controls.dart +++ /dev/null @@ -1,114 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/models/cast/cast_manager_state.dart'; -import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart'; -import 'package:immich_mobile/providers/asset_viewer/show_controls.provider.dart'; -import 'package:immich_mobile/providers/asset_viewer/video_player_provider.dart'; -import 'package:immich_mobile/providers/cast.provider.dart'; -import 'package:immich_mobile/utils/hooks/timer_hook.dart'; -import 'package:immich_mobile/widgets/asset_viewer/center_play_button.dart'; -import 'package:immich_mobile/widgets/common/delayed_loading_indicator.dart'; - -class CustomVideoPlayerControls extends HookConsumerWidget { - final String videoId; - final Duration hideTimerDuration; - - const CustomVideoPlayerControls({ - super.key, - required this.videoId, - this.hideTimerDuration = const Duration(seconds: 5), - }); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final assetIsVideo = ref.watch(currentAssetProvider.select((asset) => asset != null && asset.isVideo)); - final showControls = ref.watch(showControlsProvider); - final status = ref.watch(videoPlayerProvider(videoId).select((value) => value.status)); - - final cast = ref.watch(castProvider); - - // A timer to hide the controls - final hideTimer = useTimer(hideTimerDuration, () { - if (!context.mounted) { - return; - } - final s = ref.read(videoPlayerProvider(videoId)).status; - - // Do not hide on paused - if (s != VideoPlaybackStatus.paused && s != VideoPlaybackStatus.completed && assetIsVideo) { - ref.read(showControlsProvider.notifier).show = false; - } - }); - final showBuffering = status == VideoPlaybackStatus.buffering && !cast.isCasting; - - /// Shows the controls and starts the timer to hide them - void showControlsAndStartHideTimer() { - hideTimer.reset(); - ref.read(showControlsProvider.notifier).show = true; - } - - // When playback starts, reset the hide timer - ref.listen(videoPlayerProvider(videoId).select((v) => v.status), (previous, next) { - if (next == VideoPlaybackStatus.playing) { - hideTimer.reset(); - } - }); - - /// Toggles between playing and pausing depending on the state of the video - void togglePlay() { - showControlsAndStartHideTimer(); - - if (cast.isCasting) { - if (cast.castState == CastState.playing) { - ref.read(castProvider.notifier).pause(); - } else if (cast.castState == CastState.paused) { - ref.read(castProvider.notifier).play(); - } else if (cast.castState == CastState.idle) { - // resend the play command since its finished - final asset = ref.read(currentAssetProvider); - if (asset == null) { - return; - } - ref.read(castProvider.notifier).loadMediaOld(asset, true); - } - return; - } - - final notifier = ref.read(videoPlayerProvider(videoId).notifier); - if (status == VideoPlaybackStatus.playing) { - notifier.pause(); - } else if (status == VideoPlaybackStatus.completed) { - notifier.restart(); - } else { - notifier.play(); - } - } - - return GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: showControlsAndStartHideTimer, - child: AbsorbPointer( - absorbing: !showControls, - child: Stack( - children: [ - if (showBuffering) - const Center(child: DelayedLoadingIndicator(fadeInDuration: Duration(milliseconds: 400))) - else - GestureDetector( - onTap: () => ref.read(showControlsProvider.notifier).show = false, - child: CenterPlayButton( - backgroundColor: Colors.black54, - iconColor: Colors.white, - isFinished: status == VideoPlaybackStatus.completed, - isPlaying: - status == VideoPlaybackStatus.playing || (cast.isCasting && cast.castState == CastState.playing), - show: assetIsVideo && showControls, - onPressed: togglePlay, - ), - ), - ], - ), - ), - ); - } -} diff --git a/mobile/lib/widgets/asset_viewer/description_input.dart b/mobile/lib/widgets/asset_viewer/description_input.dart deleted file mode 100644 index b0cefd63fa..0000000000 --- a/mobile/lib/widgets/asset_viewer/description_input.dart +++ /dev/null @@ -1,106 +0,0 @@ -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/domain/models/exif.model.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/extensions/theme_extensions.dart'; -import 'package:immich_mobile/providers/asset.provider.dart'; -import 'package:immich_mobile/providers/user.provider.dart'; -import 'package:immich_mobile/services/asset.service.dart'; -import 'package:immich_mobile/utils/hash.dart'; -import 'package:immich_mobile/widgets/common/immich_toast.dart'; -import 'package:logging/logging.dart'; - -class DescriptionInput extends HookConsumerWidget { - DescriptionInput({super.key, required this.asset, this.exifInfo}); - - final Asset asset; - final ExifInfo? exifInfo; - final Logger _log = Logger('DescriptionInput'); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final controller = useTextEditingController(); - final focusNode = useFocusNode(); - final isFocus = useState(false); - final isTextEmpty = useState(controller.text.isEmpty); - final assetService = ref.watch(assetServiceProvider); - final owner = ref.watch(currentUserProvider); - final hasError = useState(false); - final assetWithExif = ref.watch(assetDetailProvider(asset)); - final hasDescription = useState(false); - final isOwner = fastHash(owner?.id ?? '') == asset.ownerId; - - useEffect(() { - assetService.getDescription(asset).then((value) { - controller.text = value; - hasDescription.value = value.isNotEmpty; - }); - return null; - }, [assetWithExif.value]); - - if (!isOwner && !hasDescription.value) { - return const SizedBox.shrink(); - } - - submitDescription(String description) async { - hasError.value = false; - try { - await assetService.setDescription(asset, description); - controller.text = description; - } catch (error, stack) { - hasError.value = true; - _log.severe("Error updating description", error, stack); - ImmichToast.show(context: context, msg: "description_input_submit_error".tr(), toastType: ToastType.error); - } - } - - Widget? suffixIcon; - if (hasError.value) { - suffixIcon = const Icon(Icons.warning_outlined); - } else if (!isTextEmpty.value && isFocus.value) { - suffixIcon = IconButton( - onPressed: () { - controller.clear(); - isTextEmpty.value = true; - }, - icon: Icon(Icons.cancel_rounded, color: context.colorScheme.onSurfaceSecondary), - splashRadius: 10, - ); - } - - return TextField( - enabled: isOwner, - focusNode: focusNode, - onTap: () => isFocus.value = true, - onChanged: (value) { - isTextEmpty.value = false; - }, - onTapOutside: (a) async { - isFocus.value = false; - focusNode.unfocus(); - - if (exifInfo?.description != controller.text) { - await submitDescription(controller.text); - } - }, - autofocus: false, - maxLines: null, - keyboardType: TextInputType.multiline, - controller: controller, - style: context.textTheme.labelLarge, - decoration: InputDecoration( - hintText: 'description_input_hint_text'.tr(), - border: InputBorder.none, - suffixIcon: suffixIcon, - enabledBorder: InputBorder.none, - focusedBorder: InputBorder.none, - disabledBorder: InputBorder.none, - errorBorder: InputBorder.none, - focusedErrorBorder: InputBorder.none, - ), - ); - } -} diff --git a/mobile/lib/widgets/asset_viewer/detail_panel/asset_date_time.dart b/mobile/lib/widgets/asset_viewer/detail_panel/asset_date_time.dart deleted file mode 100644 index df8f6593df..0000000000 --- a/mobile/lib/widgets/asset_viewer/detail_panel/asset_date_time.dart +++ /dev/null @@ -1,44 +0,0 @@ -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/extensions/asset_extensions.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/extensions/duration_extensions.dart'; -import 'package:immich_mobile/providers/asset.provider.dart'; -import 'package:immich_mobile/utils/selection_handlers.dart'; - -class AssetDateTime extends ConsumerWidget { - final Asset asset; - - const AssetDateTime({super.key, required this.asset}); - - String getDateTimeString(Asset a) { - final (deltaTime, timeZone) = a.getTZAdjustedTimeAndOffset(); - final date = DateFormat.yMMMEd().format(deltaTime); - final time = DateFormat.jm().format(deltaTime); - return '$date • $time GMT${timeZone.formatAsOffset()}'; - } - - @override - Widget build(BuildContext context, WidgetRef ref) { - final watchedAsset = ref.watch(assetDetailProvider(asset)); - String formattedDateTime = getDateTimeString(asset); - - void editDateTime() async { - await handleEditDateTime(ref, context, [asset]); - - if (watchedAsset.value != null) { - formattedDateTime = getDateTimeString(watchedAsset.value!); - } - } - - return Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text(formattedDateTime, style: context.textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.w600)), - if (asset.isRemote) IconButton(onPressed: editDateTime, icon: const Icon(Icons.edit_outlined), iconSize: 20), - ], - ); - } -} diff --git a/mobile/lib/widgets/asset_viewer/detail_panel/asset_details.dart b/mobile/lib/widgets/asset_viewer/detail_panel/asset_details.dart deleted file mode 100644 index f0f9a2efcb..0000000000 --- a/mobile/lib/widgets/asset_viewer/detail_panel/asset_details.dart +++ /dev/null @@ -1,40 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/domain/models/exif.model.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/providers/asset.provider.dart'; -import 'package:immich_mobile/widgets/asset_viewer/detail_panel/camera_info.dart'; -import 'package:immich_mobile/widgets/asset_viewer/detail_panel/file_info.dart'; - -class AssetDetails extends ConsumerWidget { - final Asset asset; - final ExifInfo? exifInfo; - - const AssetDetails({super.key, required this.asset, this.exifInfo}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final assetWithExif = ref.watch(assetDetailProvider(asset)); - final ExifInfo? exifInfo = (assetWithExif.value ?? asset).exifInfo; - - return Padding( - padding: const EdgeInsets.only(top: 24.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - "exif_bottom_sheet_details", - style: context.textTheme.labelMedium?.copyWith( - color: context.textTheme.labelMedium?.color?.withAlpha(200), - fontWeight: FontWeight.w600, - ), - ).tr(), - FileInfo(asset: asset), - if (exifInfo?.make != null) CameraInfo(exifInfo: exifInfo!), - ], - ), - ); - } -} diff --git a/mobile/lib/widgets/asset_viewer/detail_panel/asset_location.dart b/mobile/lib/widgets/asset_viewer/detail_panel/asset_location.dart deleted file mode 100644 index 6edf226e8b..0000000000 --- a/mobile/lib/widgets/asset_viewer/detail_panel/asset_location.dart +++ /dev/null @@ -1,88 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/domain/models/exif.model.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/providers/asset.provider.dart'; -import 'package:immich_mobile/utils/selection_handlers.dart'; -import 'package:immich_mobile/widgets/asset_viewer/detail_panel/exif_map.dart'; - -class AssetLocation extends HookConsumerWidget { - final Asset asset; - - const AssetLocation({super.key, required this.asset}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final assetWithExif = ref.watch(assetDetailProvider(asset)); - final ExifInfo? exifInfo = (assetWithExif.value ?? asset).exifInfo; - final hasCoordinates = exifInfo?.hasCoordinates ?? false; - - void editLocation() { - handleEditLocation(ref, context, [assetWithExif.value ?? asset]); - } - - // Guard no lat/lng - if (!hasCoordinates) { - return asset.isRemote - ? ListTile( - minLeadingWidth: 0, - contentPadding: const EdgeInsets.all(0), - leading: const Icon(Icons.location_on), - title: Text( - "add_a_location", - style: context.textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.w600, color: context.primaryColor), - ).tr(), - onTap: editLocation, - ) - : const SizedBox.shrink(); - } - - Widget getLocationName() { - if (exifInfo == null) { - return const SizedBox.shrink(); - } - - final cityName = exifInfo.city; - final stateName = exifInfo.state; - - bool hasLocationName = (cityName != null && stateName != null); - - return hasLocationName - ? Text("$cityName, $stateName", style: context.textTheme.labelLarge) - : const SizedBox.shrink(); - } - - return Padding( - padding: EdgeInsets.only(top: asset.isRemote ? 0 : 16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - "exif_bottom_sheet_location", - style: context.textTheme.labelMedium?.copyWith( - color: context.textTheme.labelMedium?.color?.withAlpha(200), - fontWeight: FontWeight.w600, - ), - ).tr(), - if (asset.isRemote) - IconButton(onPressed: editLocation, icon: const Icon(Icons.edit_outlined), iconSize: 20), - ], - ), - asset.isRemote ? const SizedBox.shrink() : const SizedBox(height: 16), - ExifMap(exifInfo: exifInfo!, markerId: asset.remoteId, markerAssetThumbhash: asset.thumbhash), - const SizedBox(height: 16), - getLocationName(), - Text( - "${exifInfo.latitude!.toStringAsFixed(4)}, ${exifInfo.longitude!.toStringAsFixed(4)}", - style: context.textTheme.labelMedium?.copyWith(color: context.textTheme.labelMedium?.color?.withAlpha(150)), - ), - ], - ), - ); - } -} diff --git a/mobile/lib/widgets/asset_viewer/detail_panel/camera_info.dart b/mobile/lib/widgets/asset_viewer/detail_panel/camera_info.dart deleted file mode 100644 index 5ae29d32c7..0000000000 --- a/mobile/lib/widgets/asset_viewer/detail_panel/camera_info.dart +++ /dev/null @@ -1,26 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:immich_mobile/domain/models/exif.model.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; - -class CameraInfo extends StatelessWidget { - final ExifInfo exifInfo; - - const CameraInfo({super.key, required this.exifInfo}); - - @override - Widget build(BuildContext context) { - final textColor = context.isDarkTheme ? Colors.white : Colors.black; - return ListTile( - contentPadding: const EdgeInsets.all(0), - dense: true, - leading: Icon(Icons.camera, color: textColor.withAlpha(200)), - title: Text("${exifInfo.make} ${exifInfo.model}", style: context.textTheme.labelLarge), - subtitle: exifInfo.f != null || exifInfo.exposureSeconds != null || exifInfo.mm != null || exifInfo.iso != null - ? Text( - "ƒ/${exifInfo.fNumber} ${exifInfo.exposureTime} ${exifInfo.focalLength} mm ISO ${exifInfo.iso ?? ''} ", - style: context.textTheme.bodySmall, - ) - : null, - ); - } -} diff --git a/mobile/lib/widgets/asset_viewer/detail_panel/detail_panel.dart b/mobile/lib/widgets/asset_viewer/detail_panel/detail_panel.dart deleted file mode 100644 index 97c9477c97..0000000000 --- a/mobile/lib/widgets/asset_viewer/detail_panel/detail_panel.dart +++ /dev/null @@ -1,37 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/widgets/asset_viewer/description_input.dart'; -import 'package:immich_mobile/widgets/asset_viewer/detail_panel/asset_date_time.dart'; -import 'package:immich_mobile/widgets/asset_viewer/detail_panel/asset_details.dart'; -import 'package:immich_mobile/widgets/asset_viewer/detail_panel/asset_location.dart'; -import 'package:immich_mobile/widgets/asset_viewer/detail_panel/people_info.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; - -class DetailPanel extends HookConsumerWidget { - final Asset asset; - final ScrollController? scrollController; - - const DetailPanel({super.key, required this.asset, this.scrollController}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - return ListView( - controller: scrollController, - shrinkWrap: true, - children: [ - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0), - child: Column( - children: [ - AssetDateTime(asset: asset), - asset.isRemote ? DescriptionInput(asset: asset) : const SizedBox.shrink(), - PeopleInfo(asset: asset), - AssetLocation(asset: asset), - AssetDetails(asset: asset), - ], - ), - ), - ], - ); - } -} diff --git a/mobile/lib/widgets/asset_viewer/detail_panel/file_info.dart b/mobile/lib/widgets/asset_viewer/detail_panel/file_info.dart deleted file mode 100644 index 78d9ac1776..0000000000 --- a/mobile/lib/widgets/asset_viewer/detail_panel/file_info.dart +++ /dev/null @@ -1,47 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/utils/bytes_units.dart'; - -class FileInfo extends StatelessWidget { - final Asset asset; - - const FileInfo({super.key, required this.asset}); - - @override - Widget build(BuildContext context) { - final textColor = context.isDarkTheme ? Colors.white : Colors.black; - - final height = asset.orientatedHeight ?? asset.height; - final width = asset.orientatedWidth ?? asset.width; - String resolution = height != null && width != null ? "$width x $height " : ""; - String fileSize = asset.exifInfo?.fileSize != null ? formatBytes(asset.exifInfo!.fileSize!) : ""; - String text = resolution + fileSize; - final imgSizeString = text.isNotEmpty ? text : null; - - String? title; - String? subtitle; - - if (imgSizeString == null && asset.fileName.isNotEmpty) { - // There is only filename - title = asset.fileName; - } else if (imgSizeString != null && asset.fileName.isNotEmpty) { - // There is both filename and size information - title = asset.fileName; - subtitle = imgSizeString; - } else if (imgSizeString != null && asset.fileName.isEmpty) { - title = imgSizeString; - } else { - return const SizedBox.shrink(); - } - - return ListTile( - contentPadding: const EdgeInsets.all(0), - dense: true, - leading: Icon(Icons.image, color: textColor.withAlpha(200)), - titleAlignment: ListTileTitleAlignment.center, - title: Text(title, style: context.textTheme.labelLarge), - subtitle: subtitle == null ? null : Text(subtitle), - ); - } -} diff --git a/mobile/lib/widgets/asset_viewer/detail_panel/people_info.dart b/mobile/lib/widgets/asset_viewer/detail_panel/people_info.dart deleted file mode 100644 index b96cbc777d..0000000000 --- a/mobile/lib/widgets/asset_viewer/detail_panel/people_info.dart +++ /dev/null @@ -1,91 +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/entities/asset.entity.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/models/search/search_curated_content.model.dart'; -import 'package:immich_mobile/providers/asset_viewer/asset_people.provider.dart'; -import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/utils/people.utils.dart'; -import 'package:immich_mobile/widgets/search/curated_people_row.dart'; -import 'package:immich_mobile/widgets/search/person_name_edit_form.dart'; - -class PeopleInfo extends ConsumerWidget { - final Asset asset; - final EdgeInsets? padding; - - const PeopleInfo({super.key, required this.asset, this.padding}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final peopleProvider = ref.watch(assetPeopleNotifierProvider(asset).notifier); - final people = ref.watch(assetPeopleNotifierProvider(asset)).value?.where((p) => !p.isHidden); - - showPersonNameEditModel(String personId, String personName) { - return showDialog( - context: context, - useRootNavigator: false, - builder: (BuildContext context) { - return PersonNameEditForm(personId: personId, personName: personName); - }, - ).then((_) { - // ensure the people list is up-to-date. - peopleProvider.refresh(); - }); - } - - final curatedPeople = - people - ?.map( - (p) => SearchCuratedContent( - id: p.id, - label: p.name, - subtitle: p.birthDate != null && p.birthDate!.isBefore(asset.fileCreatedAt) - ? formatAge(p.birthDate!, asset.fileCreatedAt) - : null, - ), - ) - .toList() ?? - []; - - return AnimatedCrossFade( - crossFadeState: (people?.isEmpty ?? true) ? CrossFadeState.showFirst : CrossFadeState.showSecond, - duration: const Duration(milliseconds: 200), - firstChild: Container(), - secondChild: Padding( - padding: const EdgeInsets.only(top: 8.0), - child: Column( - children: [ - Padding( - padding: padding ?? EdgeInsets.zero, - child: Align( - alignment: Alignment.topLeft, - child: Text( - "exif_bottom_sheet_people", - style: context.textTheme.labelMedium?.copyWith( - color: context.textTheme.labelMedium?.color?.withAlpha(200), - fontWeight: FontWeight.w600, - ), - ).tr(), - ), - ), - Padding( - padding: const EdgeInsets.only(top: 16.0), - child: CuratedPeopleRow( - padding: padding, - content: curatedPeople, - onTap: (content, index) { - context - .pushRoute(PersonResultRoute(personId: content.id, personName: content.label)) - .then((_) => peopleProvider.refresh()); - }, - onNameTap: (person, index) => {showPersonNameEditModel(person.id, person.label)}, - ), - ), - ], - ), - ), - ); - } -} diff --git a/mobile/lib/widgets/asset_viewer/gallery_app_bar.dart b/mobile/lib/widgets/asset_viewer/gallery_app_bar.dart deleted file mode 100644 index dcb0334801..0000000000 --- a/mobile/lib/widgets/asset_viewer/gallery_app_bar.dart +++ /dev/null @@ -1,119 +0,0 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:fluttertoast/fluttertoast.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/providers/album/current_album.provider.dart'; -import 'package:immich_mobile/providers/asset.provider.dart'; -import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart'; -import 'package:immich_mobile/providers/asset_viewer/download.provider.dart'; -import 'package:immich_mobile/providers/asset_viewer/scroll_to_date_notifier.provider.dart'; -import 'package:immich_mobile/providers/asset_viewer/show_controls.provider.dart'; -import 'package:immich_mobile/providers/backup/manual_upload.provider.dart'; -import 'package:immich_mobile/providers/partner.provider.dart'; -import 'package:immich_mobile/providers/tab.provider.dart'; -import 'package:immich_mobile/providers/trash.provider.dart'; -import 'package:immich_mobile/providers/user.provider.dart'; -import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/utils/hash.dart'; -import 'package:immich_mobile/widgets/album/add_to_album_bottom_sheet.dart'; -import 'package:immich_mobile/widgets/asset_grid/upload_dialog.dart'; -import 'package:immich_mobile/widgets/asset_viewer/top_control_app_bar.dart'; -import 'package:immich_mobile/widgets/common/immich_toast.dart'; - -class GalleryAppBar extends ConsumerWidget { - final void Function() showInfo; - - const GalleryAppBar({super.key, required this.showInfo}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final asset = ref.watch(currentAssetProvider); - if (asset == null) { - return const SizedBox(); - } - final album = ref.watch(currentAlbumProvider); - final isOwner = asset.ownerId == fastHash(ref.watch(currentUserProvider)?.id ?? ''); - final showControls = ref.watch(showControlsProvider); - - final isPartner = ref.watch(partnerSharedWithProvider).map((e) => fastHash(e.id)).contains(asset.ownerId); - - toggleFavorite(Asset asset) => ref.read(assetProvider.notifier).toggleFavorite([asset]); - - handleActivities() { - if (album != null && album.shared && album.remoteId != null) { - context.pushRoute(const ActivitiesRoute()); - } - } - - handleRestore(Asset asset) async { - final result = await ref.read(trashProvider.notifier).restoreAssets([asset]); - - if (result && context.mounted) { - ImmichToast.show(context: context, msg: 'asset_restored_successfully'.tr(), gravity: ToastGravity.BOTTOM); - } - } - - handleUpload(Asset asset) { - showDialog( - context: context, - builder: (BuildContext _) { - return UploadDialog( - onUpload: () { - ref.read(manualUploadProvider.notifier).uploadAssets(context, [asset]); - }, - ); - }, - ); - } - - addToAlbum(Asset addToAlbumAsset) { - showModalBottomSheet( - elevation: 0, - shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(15.0))), - context: context, - builder: (BuildContext _) { - return AddToAlbumBottomSheet(assets: [addToAlbumAsset]); - }, - ); - } - - handleDownloadAsset() { - ref.read(downloadStateProvider.notifier).downloadAsset(asset); - } - - handleLocateAsset() async { - // Go back to the gallery - await context.maybePop(); - await context.navigateTo(const TabControllerRoute(children: [PhotosRoute()])); - ref.read(tabProvider.notifier).update((state) => state = TabEnum.home); - // Scroll to the asset's date - scrollToDateNotifierProvider.scrollToDate(asset.fileCreatedAt); - } - - return IgnorePointer( - ignoring: !showControls, - child: AnimatedOpacity( - duration: const Duration(milliseconds: 100), - opacity: showControls ? 1.0 : 0.0, - child: Container( - color: Colors.black.withValues(alpha: 0.4), - child: TopControlAppBar( - isOwner: isOwner, - isPartner: isPartner, - asset: asset, - onMoreInfoPressed: showInfo, - onLocatePressed: handleLocateAsset, - onFavorite: toggleFavorite, - onRestorePressed: () => handleRestore(asset), - onUploadPressed: asset.isLocal ? () => handleUpload(asset) : null, - onDownloadPressed: asset.isLocal ? null : handleDownloadAsset, - onAddToAlbumPressed: () => addToAlbum(asset), - onActivitiesPressed: handleActivities, - ), - ), - ), - ); - } -} diff --git a/mobile/lib/widgets/asset_viewer/motion_photo_button.dart b/mobile/lib/widgets/asset_viewer/motion_photo_button.dart deleted file mode 100644 index f5479ab86e..0000000000 --- a/mobile/lib/widgets/asset_viewer/motion_photo_button.dart +++ /dev/null @@ -1,22 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/constants/colors.dart'; -import 'package:immich_mobile/providers/asset_viewer/is_motion_video_playing.provider.dart'; - -class MotionPhotoButton extends ConsumerWidget { - const MotionPhotoButton({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final isPlaying = ref.watch(isPlayingMotionVideoProvider); - - return IconButton( - onPressed: () { - ref.read(isPlayingMotionVideoProvider.notifier).toggle(); - }, - icon: isPlaying - ? const Icon(Icons.motion_photos_pause_outlined, color: grey200) - : const Icon(Icons.play_circle_outline_rounded, color: grey200), - ); - } -} diff --git a/mobile/lib/widgets/asset_viewer/top_control_app_bar.dart b/mobile/lib/widgets/asset_viewer/top_control_app_bar.dart deleted file mode 100644 index 35f3840797..0000000000 --- a/mobile/lib/widgets/asset_viewer/top_control_app_bar.dart +++ /dev/null @@ -1,182 +0,0 @@ -import 'package:auto_route/auto_route.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/activity_statistics.provider.dart'; -import 'package:immich_mobile/providers/album/current_album.provider.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/providers/asset.provider.dart'; -import 'package:immich_mobile/providers/routes.provider.dart'; -import 'package:immich_mobile/providers/cast.provider.dart'; -import 'package:immich_mobile/providers/tab.provider.dart'; -import 'package:immich_mobile/providers/websocket.provider.dart'; -import 'package:immich_mobile/widgets/asset_viewer/cast_dialog.dart'; -import 'package:immich_mobile/widgets/asset_viewer/motion_photo_button.dart'; -import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart'; - -class TopControlAppBar extends HookConsumerWidget { - const TopControlAppBar({ - super.key, - required this.asset, - required this.onMoreInfoPressed, - required this.onDownloadPressed, - required this.onLocatePressed, - required this.onAddToAlbumPressed, - required this.onRestorePressed, - required this.onFavorite, - required this.onUploadPressed, - required this.isOwner, - required this.onActivitiesPressed, - required this.isPartner, - }); - - final Asset asset; - final Function onMoreInfoPressed; - final VoidCallback? onUploadPressed; - final VoidCallback? onDownloadPressed; - final VoidCallback onLocatePressed; - final VoidCallback onAddToAlbumPressed; - final VoidCallback onRestorePressed; - final VoidCallback onActivitiesPressed; - final Function(Asset) onFavorite; - final bool isOwner; - final bool isPartner; - - @override - Widget build(BuildContext context, WidgetRef ref) { - final isInLockedView = ref.watch(inLockedViewProvider); - const double iconSize = 22.0; - final a = ref.watch(assetWatcher(asset)).value ?? asset; - final album = ref.watch(currentAlbumProvider); - final isCasting = ref.watch(castProvider.select((c) => c.isCasting)); - final websocketConnected = ref.watch(websocketProvider.select((c) => c.isConnected)); - - final comments = album != null && album.remoteId != null && asset.remoteId != null - ? ref.watch(activityStatisticsProvider(album.remoteId!, asset.remoteId)) - : 0; - - Widget buildFavoriteButton(a) { - return IconButton( - onPressed: () => onFavorite(a), - icon: Icon(a.isFavorite ? Icons.favorite : Icons.favorite_border, color: Colors.grey[200]), - ); - } - - Widget buildLocateButton() { - return IconButton( - onPressed: () { - onLocatePressed(); - }, - icon: Icon(Icons.image_search, color: Colors.grey[200]), - ); - } - - Widget buildMoreInfoButton() { - return IconButton( - onPressed: () { - onMoreInfoPressed(); - }, - icon: Icon(Icons.info_outline_rounded, color: Colors.grey[200]), - ); - } - - Widget buildDownloadButton() { - return IconButton( - onPressed: onDownloadPressed, - icon: Icon(Icons.cloud_download_outlined, color: Colors.grey[200]), - ); - } - - Widget buildAddToAlbumButton() { - return IconButton( - onPressed: () { - onAddToAlbumPressed(); - }, - icon: Icon(Icons.add, color: Colors.grey[200]), - ); - } - - Widget buildRestoreButton() { - return IconButton( - onPressed: () { - onRestorePressed(); - }, - icon: Icon(Icons.history_rounded, color: Colors.grey[200]), - ); - } - - Widget buildActivitiesButton() { - return IconButton( - onPressed: () { - onActivitiesPressed(); - }, - icon: Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Icon(Icons.mode_comment_outlined, color: Colors.grey[200]), - if (comments != 0) - Padding( - padding: const EdgeInsets.only(left: 5), - child: Text( - comments.toString(), - style: TextStyle(fontWeight: FontWeight.bold, color: Colors.grey[200]), - ), - ), - ], - ), - ); - } - - Widget buildUploadButton() { - return IconButton( - onPressed: onUploadPressed, - icon: Icon(Icons.backup_outlined, color: Colors.grey[200]), - ); - } - - Widget buildBackButton() { - return IconButton( - onPressed: () { - context.maybePop(); - }, - icon: Icon(Icons.arrow_back_ios_new_rounded, size: 20.0, color: Colors.grey[200]), - ); - } - - Widget buildCastButton() { - return IconButton( - onPressed: () { - showDialog(context: context, builder: (context) => const CastDialog()); - }, - icon: Icon( - isCasting ? Icons.cast_connected_rounded : Icons.cast_rounded, - size: 20.0, - color: isCasting ? context.primaryColor : Colors.grey[200], - ), - ); - } - - bool isInHomePage = ref.read(tabProvider.notifier).state == TabEnum.home; - bool? isInTrash = ref.read(currentAssetProvider)?.isTrashed; - - return AppBar( - foregroundColor: Colors.grey[100], - backgroundColor: Colors.transparent, - leading: buildBackButton(), - actionsIconTheme: const IconThemeData(size: iconSize), - shape: const Border(), - actions: [ - if (asset.isRemote && isOwner) buildFavoriteButton(a), - if (isOwner && !isInHomePage && !(isInTrash ?? false) && !isInLockedView) buildLocateButton(), - if (asset.livePhotoVideoId != null) const MotionPhotoButton(), - if (asset.isLocal && !asset.isRemote) buildUploadButton(), - if (asset.isRemote && !asset.isLocal && isOwner) buildDownloadButton(), - if (asset.isRemote && (isOwner || isPartner) && !asset.isTrashed && !isInLockedView) buildAddToAlbumButton(), - if (isCasting || (asset.isRemote && websocketConnected)) buildCastButton(), - if (asset.isTrashed) buildRestoreButton(), - if (album != null && album.shared && !isInLockedView) buildActivitiesButton(), - buildMoreInfoButton(), - ], - ); - } -} diff --git a/mobile/lib/widgets/backup/album_info_card.dart b/mobile/lib/widgets/backup/album_info_card.dart deleted file mode 100644 index d635e136bc..0000000000 --- a/mobile/lib/widgets/backup/album_info_card.dart +++ /dev/null @@ -1,185 +0,0 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:fluttertoast/fluttertoast.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/models/backup/available_album.model.dart'; -import 'package:immich_mobile/providers/album/album.provider.dart'; -import 'package:immich_mobile/providers/app_settings.provider.dart'; -import 'package:immich_mobile/providers/backup/backup.provider.dart'; -import 'package:immich_mobile/providers/haptic_feedback.provider.dart'; -import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/services/app_settings.service.dart'; -import 'package:immich_mobile/widgets/common/immich_toast.dart'; - -class AlbumInfoCard extends HookConsumerWidget { - final AvailableAlbum album; - - const AlbumInfoCard({super.key, required this.album}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final bool isSelected = ref.watch(backupProvider).selectedBackupAlbums.contains(album); - final bool isExcluded = ref.watch(backupProvider).excludedBackupAlbums.contains(album); - final syncAlbum = ref.watch(appSettingsServiceProvider).getSetting(AppSettingsEnum.syncAlbums); - - final isDarkTheme = context.isDarkTheme; - - ColorFilter selectedFilter = ColorFilter.mode(context.primaryColor.withAlpha(100), BlendMode.darken); - ColorFilter excludedFilter = ColorFilter.mode(Colors.red.withAlpha(75), BlendMode.darken); - ColorFilter unselectedFilter = const ColorFilter.mode(Colors.black, BlendMode.color); - - buildSelectedTextBox() { - if (isSelected) { - return Chip( - visualDensity: VisualDensity.compact, - shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(5))), - label: Text( - "album_info_card_backup_album_included", - style: TextStyle( - fontSize: 10, - color: isDarkTheme ? Colors.black : Colors.white, - fontWeight: FontWeight.bold, - ), - ).tr(), - backgroundColor: context.primaryColor, - ); - } else if (isExcluded) { - return Chip( - visualDensity: VisualDensity.compact, - shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(5))), - label: Text( - "album_info_card_backup_album_excluded", - style: TextStyle( - fontSize: 10, - color: isDarkTheme ? Colors.black : Colors.white, - fontWeight: FontWeight.bold, - ), - ).tr(), - backgroundColor: Colors.red[300], - ); - } - - return const SizedBox(); - } - - buildImageFilter() { - if (isSelected) { - return selectedFilter; - } else if (isExcluded) { - return excludedFilter; - } else { - return unselectedFilter; - } - } - - return GestureDetector( - onTap: () { - ref.read(hapticFeedbackProvider.notifier).selectionClick(); - - if (isSelected) { - ref.read(backupProvider.notifier).removeAlbumForBackup(album); - } else { - ref.read(backupProvider.notifier).addAlbumForBackup(album); - if (syncAlbum) { - ref.read(albumProvider.notifier).createSyncAlbum(album.name); - } - } - }, - onDoubleTap: () { - ref.read(hapticFeedbackProvider.notifier).selectionClick(); - - if (isExcluded) { - // Remove from exclude album list - ref.read(backupProvider.notifier).removeExcludedAlbumForBackup(album); - } else { - // Add to exclude album list - - if (album.id == 'isAll' || album.name == 'Recents') { - ImmichToast.show( - context: context, - msg: 'Cannot exclude album contains all assets', - toastType: ToastType.error, - gravity: ToastGravity.BOTTOM, - ); - return; - } - - ref.read(backupProvider.notifier).addExcludedAlbumForBackup(album); - } - }, - child: Card( - clipBehavior: Clip.hardEdge, - margin: const EdgeInsets.all(1), - shape: RoundedRectangleBorder( - borderRadius: const BorderRadius.all( - Radius.circular(12), // if you need this - ), - side: BorderSide( - color: isDarkTheme ? const Color.fromARGB(255, 37, 35, 35) : const Color(0xFFC9C9C9), - width: 1, - ), - ), - elevation: 0, - borderOnForeground: false, - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Expanded( - child: Stack( - clipBehavior: Clip.hardEdge, - children: [ - ColorFiltered( - colorFilter: buildImageFilter(), - child: const Image( - width: double.infinity, - height: double.infinity, - image: AssetImage('assets/immich-logo.png'), - fit: BoxFit.cover, - ), - ), - Positioned(bottom: 10, right: 25, child: buildSelectedTextBox()), - ], - ), - ), - Padding( - padding: const EdgeInsets.only(left: 25), - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - album.name, - style: TextStyle(fontSize: 14, color: context.primaryColor, fontWeight: FontWeight.bold), - ), - Padding( - padding: const EdgeInsets.only(top: 2.0), - child: Text( - album.assetCount.toString() + (album.isAll ? " (${'all'.tr()})" : ""), - style: TextStyle(fontSize: 12, color: Colors.grey[600]), - ), - ), - ], - ), - ), - IconButton( - onPressed: () { - context.pushRoute(AlbumPreviewRoute(album: album.album)); - }, - icon: Icon(Icons.image_outlined, color: context.primaryColor, size: 24), - splashRadius: 25, - ), - ], - ), - ), - ], - ), - ), - ); - } -} diff --git a/mobile/lib/widgets/backup/album_info_list_tile.dart b/mobile/lib/widgets/backup/album_info_list_tile.dart deleted file mode 100644 index 9796f45e8b..0000000000 --- a/mobile/lib/widgets/backup/album_info_list_tile.dart +++ /dev/null @@ -1,98 +0,0 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:flutter/material.dart'; -import 'package:fluttertoast/fluttertoast.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/models/backup/available_album.model.dart'; -import 'package:immich_mobile/providers/album/album.provider.dart'; -import 'package:immich_mobile/providers/app_settings.provider.dart'; -import 'package:immich_mobile/providers/backup/backup.provider.dart'; -import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/providers/haptic_feedback.provider.dart'; -import 'package:immich_mobile/services/app_settings.service.dart'; -import 'package:immich_mobile/widgets/common/immich_toast.dart'; - -class AlbumInfoListTile extends HookConsumerWidget { - final AvailableAlbum album; - - const AlbumInfoListTile({super.key, required this.album}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final bool isSelected = ref.watch(backupProvider).selectedBackupAlbums.contains(album); - final bool isExcluded = ref.watch(backupProvider).excludedBackupAlbums.contains(album); - final syncAlbum = ref.watch(appSettingsServiceProvider).getSetting(AppSettingsEnum.syncAlbums); - - buildTileColor() { - if (isSelected) { - return context.isDarkTheme ? context.primaryColor.withAlpha(100) : context.primaryColor.withAlpha(25); - } else if (isExcluded) { - return context.isDarkTheme ? Colors.red[300]?.withAlpha(150) : Colors.red[100]?.withAlpha(150); - } else { - return Colors.transparent; - } - } - - buildIcon() { - if (isSelected) { - return Icon(Icons.check_circle_rounded, color: context.colorScheme.primary); - } - - if (isExcluded) { - return Icon(Icons.remove_circle_rounded, color: context.colorScheme.error); - } - - return Icon(Icons.circle, color: context.colorScheme.surfaceContainerHighest); - } - - return GestureDetector( - onDoubleTap: () { - ref.watch(hapticFeedbackProvider.notifier).selectionClick(); - - if (isExcluded) { - // Remove from exclude album list - ref.read(backupProvider.notifier).removeExcludedAlbumForBackup(album); - } else { - // Add to exclude album list - - if (album.id == 'isAll' || album.name == 'Recents') { - ImmichToast.show( - context: context, - msg: 'Cannot exclude album contains all assets', - toastType: ToastType.error, - gravity: ToastGravity.BOTTOM, - ); - return; - } - - ref.read(backupProvider.notifier).addExcludedAlbumForBackup(album); - } - }, - child: ListTile( - tileColor: buildTileColor(), - contentPadding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16), - onTap: () { - ref.read(hapticFeedbackProvider.notifier).selectionClick(); - if (isSelected) { - ref.read(backupProvider.notifier).removeAlbumForBackup(album); - } else { - ref.read(backupProvider.notifier).addAlbumForBackup(album); - if (syncAlbum) { - ref.read(albumProvider.notifier).createSyncAlbum(album.name); - } - } - }, - leading: buildIcon(), - title: Text(album.name, style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold)), - subtitle: Text(album.assetCount.toString()), - trailing: IconButton( - onPressed: () { - context.pushRoute(AlbumPreviewRoute(album: album.album)); - }, - icon: Icon(Icons.image_outlined, color: context.primaryColor, size: 24), - splashRadius: 25, - ), - ), - ); - } -} diff --git a/mobile/lib/widgets/backup/asset_info_table.dart b/mobile/lib/widgets/backup/asset_info_table.dart deleted file mode 100644 index 2cccded2bb..0000000000 --- a/mobile/lib/widgets/backup/asset_info_table.dart +++ /dev/null @@ -1,105 +0,0 @@ -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/extensions/theme_extensions.dart'; -import 'package:immich_mobile/models/backup/backup_state.model.dart'; -import 'package:immich_mobile/models/backup/current_upload_asset.model.dart'; -import 'package:immich_mobile/providers/backup/backup.provider.dart'; -import 'package:immich_mobile/providers/backup/manual_upload.provider.dart'; - -class BackupAssetInfoTable extends ConsumerWidget { - const BackupAssetInfoTable({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final isManualUpload = ref.watch( - backupProvider.select((value) => value.backupProgress == BackUpProgressEnum.manualInProgress), - ); - - final isUploadInProgress = ref.watch( - backupProvider.select( - (value) => - value.backupProgress == BackUpProgressEnum.inProgress || - value.backupProgress == BackUpProgressEnum.inBackground || - value.backupProgress == BackUpProgressEnum.manualInProgress, - ), - ); - - final asset = isManualUpload - ? ref.watch(manualUploadProvider.select((value) => value.currentUploadAsset)) - : ref.watch(backupProvider.select((value) => value.currentUploadAsset)); - - return Padding( - padding: const EdgeInsets.only(top: 8.0), - child: Table( - border: TableBorder.all(color: context.colorScheme.outlineVariant, width: 1), - children: [ - TableRow( - children: [ - TableCell( - verticalAlignment: TableCellVerticalAlignment.middle, - child: Padding( - padding: const EdgeInsets.all(6.0), - child: - Text( - 'backup_controller_page_filename', - style: TextStyle( - color: context.colorScheme.onSurfaceSecondary, - fontWeight: FontWeight.bold, - fontSize: 10.0, - ), - ).tr( - namedArgs: isUploadInProgress - ? {'filename': asset.fileName, 'size': asset.fileType.toLowerCase()} - : {'filename': "-", 'size': "-"}, - ), - ), - ), - ], - ), - TableRow( - children: [ - TableCell( - verticalAlignment: TableCellVerticalAlignment.middle, - child: Padding( - padding: const EdgeInsets.all(6.0), - child: Text( - "backup_controller_page_created", - style: TextStyle( - color: context.colorScheme.onSurfaceSecondary, - fontWeight: FontWeight.bold, - fontSize: 10.0, - ), - ).tr(namedArgs: {'date': isUploadInProgress ? _getAssetCreationDate(asset) : "-"}), - ), - ), - ], - ), - TableRow( - children: [ - TableCell( - child: Padding( - padding: const EdgeInsets.all(6.0), - child: Text( - "backup_controller_page_id", - style: TextStyle( - color: context.colorScheme.onSurfaceSecondary, - fontWeight: FontWeight.bold, - fontSize: 10.0, - ), - ).tr(namedArgs: {'id': isUploadInProgress ? asset.id : "-"}), - ), - ), - ], - ), - ], - ), - ); - } - - @pragma('vm:prefer-inline') - String _getAssetCreationDate(CurrentUploadAsset asset) { - return DateFormat.yMMMMd().format(asset.fileCreatedAt.toLocal()); - } -} diff --git a/mobile/lib/widgets/backup/current_backup_asset_info_box.dart b/mobile/lib/widgets/backup/current_backup_asset_info_box.dart deleted file mode 100644 index c2f94e706a..0000000000 --- a/mobile/lib/widgets/backup/current_backup_asset_info_box.dart +++ /dev/null @@ -1,37 +0,0 @@ -import 'dart:io'; - -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/widgets/backup/asset_info_table.dart'; -import 'package:immich_mobile/widgets/backup/error_chip.dart'; -import 'package:immich_mobile/widgets/backup/icloud_download_progress_bar.dart'; -import 'package:immich_mobile/widgets/backup/upload_progress_bar.dart'; -import 'package:immich_mobile/widgets/backup/upload_stats.dart'; - -class CurrentUploadingAssetInfoBox extends StatelessWidget { - const CurrentUploadingAssetInfoBox({super.key}); - - @override - Widget build(BuildContext context) { - return ListTile( - isThreeLine: true, - leading: Icon(Icons.image_outlined, color: context.primaryColor, size: 30), - title: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text("backup_controller_page_uploading_file_info", style: context.textTheme.titleSmall).tr(), - const BackupErrorChip(), - ], - ), - subtitle: Column( - children: [ - if (Platform.isIOS) const IcloudDownloadProgressBar(), - const BackupUploadProgressBar(), - const BackupUploadStats(), - const BackupAssetInfoTable(), - ], - ), - ); - } -} diff --git a/mobile/lib/widgets/backup/error_chip.dart b/mobile/lib/widgets/backup/error_chip.dart deleted file mode 100644 index 191049cd75..0000000000 --- a/mobile/lib/widgets/backup/error_chip.dart +++ /dev/null @@ -1,28 +0,0 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/constants/colors.dart'; -import 'package:immich_mobile/providers/backup/error_backup_list.provider.dart'; -import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/widgets/backup/error_chip_text.dart'; - -class BackupErrorChip extends ConsumerWidget { - const BackupErrorChip({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final hasErrors = ref.watch(errorBackupListProvider.select((value) => value.isNotEmpty)); - if (!hasErrors) { - return const SizedBox(); - } - - return ActionChip( - avatar: const Icon(Icons.info, color: red400), - elevation: 1, - visualDensity: VisualDensity.compact, - label: const BackupErrorChipText(), - backgroundColor: Colors.white, - onPressed: () => context.pushRoute(const FailedBackupStatusRoute()), - ); - } -} diff --git a/mobile/lib/widgets/backup/error_chip_text.dart b/mobile/lib/widgets/backup/error_chip_text.dart deleted file mode 100644 index c987dfd331..0000000000 --- a/mobile/lib/widgets/backup/error_chip_text.dart +++ /dev/null @@ -1,22 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/constants/colors.dart'; -import 'package:immich_mobile/providers/backup/error_backup_list.provider.dart'; - -class BackupErrorChipText extends ConsumerWidget { - const BackupErrorChipText({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final count = ref.watch(errorBackupListProvider).length; - if (count == 0) { - return const SizedBox(); - } - - return const Text( - "backup_controller_page_failed", - style: TextStyle(color: red400, fontWeight: FontWeight.bold, fontSize: 11), - ).tr(namedArgs: {'count': count.toString()}); - } -} diff --git a/mobile/lib/widgets/backup/icloud_download_progress_bar.dart b/mobile/lib/widgets/backup/icloud_download_progress_bar.dart deleted file mode 100644 index 9f0f7ec3eb..0000000000 --- a/mobile/lib/widgets/backup/icloud_download_progress_bar.dart +++ /dev/null @@ -1,43 +0,0 @@ -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/models/backup/backup_state.model.dart'; -import 'package:immich_mobile/providers/backup/backup.provider.dart'; -import 'package:immich_mobile/providers/backup/manual_upload.provider.dart'; - -class IcloudDownloadProgressBar extends ConsumerWidget { - const IcloudDownloadProgressBar({super.key}); - @override - Widget build(BuildContext context, WidgetRef ref) { - final isManualUpload = ref.watch( - backupProvider.select((value) => value.backupProgress == BackUpProgressEnum.manualInProgress), - ); - - final isIcloudAsset = isManualUpload - ? ref.watch(manualUploadProvider.select((value) => value.currentUploadAsset.isIcloudAsset)) - : ref.watch(backupProvider.select((value) => value.currentUploadAsset.isIcloudAsset)); - - if (!isIcloudAsset) { - return const SizedBox(); - } - - final iCloudDownloadProgress = ref.watch(backupProvider.select((value) => value.iCloudDownloadProgress)); - - return Padding( - padding: const EdgeInsets.only(top: 8.0), - child: Row( - children: [ - SizedBox(width: 110, child: Text("iCloud Download", style: context.textTheme.labelSmall)), - Expanded( - child: LinearProgressIndicator( - minHeight: 10.0, - value: iCloudDownloadProgress / 100.0, - borderRadius: const BorderRadius.all(Radius.circular(10.0)), - ), - ), - Text(" ${iCloudDownloadProgress ~/ 1}%", style: const TextStyle(fontSize: 12)), - ], - ), - ); - } -} diff --git a/mobile/lib/widgets/backup/ios_debug_info_tile.dart b/mobile/lib/widgets/backup/ios_debug_info_tile.dart deleted file mode 100644 index be333c6460..0000000000 --- a/mobile/lib/widgets/backup/ios_debug_info_tile.dart +++ /dev/null @@ -1,49 +0,0 @@ -import 'package:intl/intl.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/extensions/translate_extensions.dart'; -import 'package:immich_mobile/providers/backup/ios_background_settings.provider.dart'; - -/// This is a simple debug widget which should be removed later on when we are -/// more confident about background sync -class IosDebugInfoTile extends HookConsumerWidget { - final IOSBackgroundSettings settings; - const IosDebugInfoTile({super.key, required this.settings}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final fetch = settings.timeOfLastFetch; - final processing = settings.timeOfLastProcessing; - final processes = settings.numberOfBackgroundTasksQueued; - - final String title; - if (processes == 0) { - title = 'ios_debug_info_no_processes_queued'.t(context: context); - } else { - title = 'ios_debug_info_processes_queued'.t(context: context, args: {'count': processes}); - } - - final df = DateFormat.yMd().add_jm(); - final String subtitle; - if (fetch == null && processing == null) { - subtitle = 'ios_debug_info_no_sync_yet'.t(context: context); - } else if (fetch != null && processing == null) { - subtitle = 'ios_debug_info_fetch_ran_at'.t(context: context, args: {'dateTime': df.format(fetch)}); - } else if (processing != null && fetch == null) { - subtitle = 'ios_debug_info_processing_ran_at'.t(context: context, args: {'dateTime': df.format(processing)}); - } else { - final fetchOrProcessing = fetch!.isAfter(processing!) ? fetch : processing; - subtitle = 'ios_debug_info_last_sync_at'.t(context: context, args: {'dateTime': df.format(fetchOrProcessing)}); - } - - return ListTile( - title: Text( - title, - style: TextStyle(fontWeight: FontWeight.bold, fontSize: 14, color: context.primaryColor), - ), - subtitle: Text(subtitle, style: const TextStyle(fontSize: 14)), - leading: Icon(Icons.bug_report, color: context.primaryColor), - ); - } -} diff --git a/mobile/lib/widgets/backup/upload_progress_bar.dart b/mobile/lib/widgets/backup/upload_progress_bar.dart deleted file mode 100644 index 641ed14878..0000000000 --- a/mobile/lib/widgets/backup/upload_progress_bar.dart +++ /dev/null @@ -1,45 +0,0 @@ -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/models/backup/backup_state.model.dart'; -import 'package:immich_mobile/providers/backup/backup.provider.dart'; -import 'package:immich_mobile/providers/backup/manual_upload.provider.dart'; - -class BackupUploadProgressBar extends ConsumerWidget { - const BackupUploadProgressBar({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final isManualUpload = ref.watch( - backupProvider.select((value) => value.backupProgress == BackUpProgressEnum.manualInProgress), - ); - - final isIcloudAsset = isManualUpload - ? ref.watch(manualUploadProvider.select((value) => value.currentUploadAsset.isIcloudAsset)) - : ref.watch(backupProvider.select((value) => value.currentUploadAsset.isIcloudAsset)); - - final uploadProgress = isManualUpload - ? ref.watch(manualUploadProvider.select((value) => value.progressInPercentage)) - : ref.watch(backupProvider.select((value) => value.progressInPercentage)); - - return Padding( - padding: const EdgeInsets.only(top: 8.0), - child: Row( - children: [ - if (isIcloudAsset) SizedBox(width: 110, child: Text("Immich Upload", style: context.textTheme.labelSmall)), - Expanded( - child: LinearProgressIndicator( - minHeight: 10.0, - value: uploadProgress / 100.0, - borderRadius: const BorderRadius.all(Radius.circular(10.0)), - ), - ), - Text( - " ${uploadProgress.toStringAsFixed(0)}%", - style: const TextStyle(fontSize: 12, fontFamily: "GoogleSansCode"), - ), - ], - ), - ); - } -} diff --git a/mobile/lib/widgets/backup/upload_stats.dart b/mobile/lib/widgets/backup/upload_stats.dart deleted file mode 100644 index 38f99e53fc..0000000000 --- a/mobile/lib/widgets/backup/upload_stats.dart +++ /dev/null @@ -1,51 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.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/manual_upload.provider.dart'; - -class BackupUploadStats extends ConsumerWidget { - const BackupUploadStats({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final isManualUpload = ref.watch( - backupProvider.select((value) => value.backupProgress == BackUpProgressEnum.manualInProgress), - ); - - final uploadFileProgress = isManualUpload - ? ref.watch(manualUploadProvider.select((value) => value.progressInFileSize)) - : ref.watch(backupProvider.select((value) => value.progressInFileSize)); - - final uploadFileSpeed = isManualUpload - ? ref.watch(manualUploadProvider.select((value) => value.progressInFileSpeed)) - : ref.watch(backupProvider.select((value) => value.progressInFileSpeed)); - - return Padding( - padding: const EdgeInsets.only(top: 2.0, bottom: 2.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text(uploadFileProgress, style: const TextStyle(fontSize: 10, fontFamily: "GoogleSansCode")), - Text( - _formatUploadFileSpeed(uploadFileSpeed), - style: const TextStyle(fontSize: 10, fontFamily: "GoogleSansCode"), - ), - ], - ), - ); - } - - @pragma('vm:prefer-inline') - String _formatUploadFileSpeed(double uploadFileSpeed) { - if (uploadFileSpeed < 1024) { - return '${uploadFileSpeed.toStringAsFixed(2)} B/s'; - } else if (uploadFileSpeed < 1024 * 1024) { - return '${(uploadFileSpeed / 1024).toStringAsFixed(2)} KB/s'; - } else if (uploadFileSpeed < 1024 * 1024 * 1024) { - return '${(uploadFileSpeed / (1024 * 1024)).toStringAsFixed(2)} MB/s'; - } else { - return '${(uploadFileSpeed / (1024 * 1024 * 1024)).toStringAsFixed(2)} GB/s'; - } - } -} diff --git a/mobile/lib/widgets/common/app_bar_dialog/app_bar_dialog.dart b/mobile/lib/widgets/common/app_bar_dialog/app_bar_dialog.dart index c330fb4649..c6c6b2cff1 100644 --- a/mobile/lib/widgets/common/app_bar_dialog/app_bar_dialog.dart +++ b/mobile/lib/widgets/common/app_bar_dialog/app_bar_dialog.dart @@ -5,18 +5,15 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart' hide Store; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/models/backup/backup_state.model.dart'; -import 'package:immich_mobile/providers/asset.provider.dart'; +import 'package:immich_mobile/models/server_info/server_disk_info.model.dart'; +import 'package:immich_mobile/pages/common/settings.page.dart'; import 'package:immich_mobile/providers/auth.provider.dart'; import 'package:immich_mobile/providers/backup/backup.provider.dart'; -import 'package:immich_mobile/providers/backup/manual_upload.provider.dart'; import 'package:immich_mobile/providers/infrastructure/readonly_mode.provider.dart'; import 'package:immich_mobile/providers/locale_provider.dart'; import 'package:immich_mobile/providers/user.provider.dart'; import 'package:immich_mobile/providers/websocket.provider.dart'; -import 'package:immich_mobile/pages/common/settings.page.dart'; import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/utils/bytes_units.dart'; import 'package:immich_mobile/widgets/common/app_bar_dialog/app_bar_profile_info.dart'; @@ -32,7 +29,7 @@ class ImmichAppBarDialog extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { ref.watch(localeProvider); - BackUpState backupState = ref.watch(backupProvider); + ServerDiskInfo backupState = ref.watch(backupProvider); final theme = context.themeData; bool isHorizontal = !context.isMobile; final horizontalPadding = isHorizontal ? 100.0 : 20.0; @@ -128,9 +125,6 @@ class ImmichAppBarDialog extends HookConsumerWidget { isLoggingOut.value = true; await ref.read(authProvider.notifier).logout().whenComplete(() => isLoggingOut.value = false); - ref.read(manualUploadProvider.notifier).cancelBackup(); - ref.read(backupProvider.notifier).cancelBackup(); - unawaited(ref.read(assetProvider.notifier).clearAllAssets()); ref.read(websocketProvider.notifier).disconnect(); unawaited(context.replaceRoute(const LoginRoute())); }, @@ -146,9 +140,9 @@ class ImmichAppBarDialog extends HookConsumerWidget { } Widget buildStorageInformation() { - var percentage = backupState.serverInfo.diskUsagePercentage / 100; - var usedDiskSpace = backupState.serverInfo.diskUse; - var totalDiskSpace = backupState.serverInfo.diskSize; + var percentage = backupState.diskUsagePercentage / 100; + var usedDiskSpace = backupState.diskUse; + var totalDiskSpace = backupState.diskSize; if (user != null && user.hasQuota) { usedDiskSpace = formatBytes(user.quotaUsageInBytes); @@ -275,7 +269,7 @@ class ImmichAppBarDialog extends HookConsumerWidget { ], ), ), - if (Store.isBetaTimelineEnabled && isReadonlyModeEnabled) buildReadonlyMessage(), + if (isReadonlyModeEnabled) buildReadonlyMessage(), buildAppLogButton(), buildFreeUpSpaceButton(), buildSettingButton(), diff --git a/mobile/lib/widgets/common/app_bar_dialog/app_bar_profile_info.dart b/mobile/lib/widgets/common/app_bar_dialog/app_bar_profile_info.dart index a9fdb9a43f..d6881f519a 100644 --- a/mobile/lib/widgets/common/app_bar_dialog/app_bar_profile_info.dart +++ b/mobile/lib/widgets/common/app_bar_dialog/app_bar_profile_info.dart @@ -4,7 +4,6 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:image_picker/image_picker.dart'; -import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/theme_extensions.dart'; import 'package:immich_mobile/providers/auth.provider.dart'; @@ -62,10 +61,6 @@ class AppBarProfileInfoBox extends HookConsumerWidget { } void toggleReadonlyMode() { - // read only mode is only supported int he beta experience - // TODO: remove this check when the beta UI goes stable - if (!Store.isBetaTimelineEnabled) return; - final isReadonlyModeEnabled = ref.watch(readonlyModeProvider); ref.read(readonlyModeProvider.notifier).toggleReadonlyMode(); diff --git a/mobile/lib/widgets/common/drag_sheet.dart b/mobile/lib/widgets/common/drag_sheet.dart deleted file mode 100644 index 5d1fda1beb..0000000000 --- a/mobile/lib/widgets/common/drag_sheet.dart +++ /dev/null @@ -1,54 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; - -class CustomDraggingHandle extends StatelessWidget { - const CustomDraggingHandle({super.key}); - - @override - Widget build(BuildContext context) { - return Container( - height: 4, - width: 30, - decoration: BoxDecoration( - color: context.themeData.dividerColor, - borderRadius: const BorderRadius.all(Radius.circular(20)), - ), - ); - } -} - -class ControlBoxButton extends StatelessWidget { - const ControlBoxButton({super.key, required this.label, required this.iconData, this.onPressed, this.onLongPressed}); - - final String label; - final IconData iconData; - final void Function()? onPressed; - final void Function()? onLongPressed; - - @override - Widget build(BuildContext context) { - final minWidth = context.isMobile ? MediaQuery.sizeOf(context).width / 4.5 : 75.0; - - return MaterialButton( - padding: const EdgeInsets.all(10), - shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(20))), - onPressed: onPressed, - onLongPress: onLongPressed, - minWidth: minWidth, - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Icon(iconData, size: 24), - const SizedBox(height: 8), - Text( - label, - style: const TextStyle(fontSize: 14.0, fontWeight: FontWeight.w400), - maxLines: 3, - textAlign: TextAlign.center, - ), - ], - ), - ); - } -} diff --git a/mobile/lib/widgets/common/immich_app_bar.dart b/mobile/lib/widgets/common/immich_app_bar.dart deleted file mode 100644 index 56b7e91eec..0000000000 --- a/mobile/lib/widgets/common/immich_app_bar.dart +++ /dev/null @@ -1,170 +0,0 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_svg/svg.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.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/cast.provider.dart'; -import 'package:immich_mobile/providers/server_info.provider.dart'; -import 'package:immich_mobile/providers/user.provider.dart'; -import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/widgets/asset_viewer/cast_dialog.dart'; -import 'package:immich_mobile/widgets/common/app_bar_dialog/app_bar_dialog.dart'; -import 'package:immich_mobile/widgets/common/user_circle_avatar.dart'; - -class ImmichAppBar extends ConsumerWidget implements PreferredSizeWidget { - @override - Size get preferredSize => const Size.fromHeight(kToolbarHeight); - final List? actions; - final bool showUploadButton; - - const ImmichAppBar({super.key, this.actions, this.showUploadButton = true}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final BackUpState backupState = ref.watch(backupProvider); - final bool isEnableAutoBackup = backupState.backgroundBackup || backupState.autoBackup; - final user = ref.watch(currentUserProvider); - final bool versionWarningPresent = ref.watch(versionWarningPresentProvider(user)); - final isDarkTheme = context.isDarkTheme; - const widgetSize = 30.0; - final isCasting = ref.watch(castProvider.select((c) => c.isCasting)); - - buildProfileIndicator() { - return InkWell( - onTap: () => - showDialog(context: context, useRootNavigator: false, builder: (ctx) => const ImmichAppBarDialog()), - borderRadius: const BorderRadius.all(Radius.circular(12)), - child: Badge( - label: Container( - decoration: BoxDecoration(color: Colors.black, borderRadius: BorderRadius.circular(widgetSize / 2)), - child: const Icon(Icons.info, color: Color.fromARGB(255, 243, 188, 106), size: widgetSize / 2), - ), - backgroundColor: Colors.transparent, - alignment: Alignment.bottomRight, - isLabelVisible: versionWarningPresent, - offset: const Offset(-2, -12), - child: user == null - ? const Icon(Icons.face_outlined, size: widgetSize) - : Semantics( - label: "logged_in_as".tr(namedArgs: {"user": user.name}), - child: UserCircleAvatar(size: 32, user: user), - ), - ), - ); - } - - getBackupBadgeIcon() { - final iconColor = isDarkTheme ? Colors.white : Colors.black; - - if (isEnableAutoBackup) { - if (backupState.backupProgress == BackUpProgressEnum.inProgress) { - return Container( - padding: const EdgeInsets.all(3.5), - child: CircularProgressIndicator( - strokeWidth: 2, - strokeCap: StrokeCap.round, - valueColor: AlwaysStoppedAnimation(iconColor), - semanticsLabel: 'backup_controller_page_backup'.tr(), - ), - ); - } else if (backupState.backupProgress != BackUpProgressEnum.inBackground && - backupState.backupProgress != BackUpProgressEnum.manualInProgress) { - return Icon( - Icons.check_outlined, - size: 9, - color: iconColor, - semanticLabel: 'backup_controller_page_backup'.tr(), - ); - } - } - - if (!isEnableAutoBackup) { - return Icon( - Icons.cloud_off_rounded, - size: 9, - color: iconColor, - semanticLabel: 'backup_controller_page_backup'.tr(), - ); - } - } - - buildBackupIndicator() { - final indicatorIcon = getBackupBadgeIcon(); - final badgeBackground = context.colorScheme.surfaceContainer; - - return InkWell( - onTap: () => context.pushRoute(const BackupControllerRoute()), - borderRadius: const BorderRadius.all(Radius.circular(12)), - child: Badge( - label: Container( - width: widgetSize / 2, - height: widgetSize / 2, - decoration: BoxDecoration( - color: badgeBackground, - border: Border.all(color: context.colorScheme.outline.withValues(alpha: .3)), - borderRadius: BorderRadius.circular(widgetSize / 2), - ), - child: indicatorIcon, - ), - backgroundColor: Colors.transparent, - alignment: Alignment.bottomRight, - isLabelVisible: indicatorIcon != null, - offset: const Offset(-2, -12), - child: Icon(Icons.backup_rounded, size: widgetSize, color: context.primaryColor), - ), - ); - } - - return AppBar( - backgroundColor: context.themeData.appBarTheme.backgroundColor, - shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(5))), - automaticallyImplyLeading: false, - centerTitle: false, - title: Builder( - builder: (BuildContext context) { - return Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Padding( - padding: const EdgeInsets.only(top: 3.0), - child: SvgPicture.asset( - context.isDarkTheme ? 'assets/immich-logo-inline-dark.svg' : 'assets/immich-logo-inline-light.svg', - height: 40, - ), - ), - const Tooltip( - triggerMode: TooltipTriggerMode.tap, - showDuration: Duration(seconds: 4), - message: - "The old timeline is deprecated and will be removed in a future release. Kindly switch to the new timeline under Advanced Settings.", - child: Padding( - padding: EdgeInsets.only(top: 3.0), - child: Icon(Icons.error_rounded, fill: 1, color: Colors.amber, size: 20), - ), - ), - ], - ); - }, - ), - actions: [ - if (actions != null) - ...actions!.map((action) => Padding(padding: const EdgeInsets.only(right: 16), child: action)), - if (isCasting) - Padding( - padding: const EdgeInsets.only(right: 12), - child: IconButton( - onPressed: () { - showDialog(context: context, builder: (context) => const CastDialog()); - }, - icon: Icon(isCasting ? Icons.cast_connected_rounded : Icons.cast_rounded), - ), - ), - if (showUploadButton) Padding(padding: const EdgeInsets.only(right: 20), child: buildBackupIndicator()), - Padding(padding: const EdgeInsets.only(right: 20), child: buildProfileIndicator()), - ], - ); - } -} diff --git a/mobile/lib/widgets/common/immich_image.dart b/mobile/lib/widgets/common/immich_image.dart deleted file mode 100644 index 57978e83ff..0000000000 --- a/mobile/lib/widgets/common/immich_image.dart +++ /dev/null @@ -1,100 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:immich_mobile/domain/models/asset/base_asset.model.dart' as base_asset; -import 'package:immich_mobile/domain/models/store.model.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/presentation/widgets/images/local_image_provider.dart'; -import 'package:immich_mobile/presentation/widgets/images/remote_image_provider.dart'; -import 'package:immich_mobile/widgets/asset_grid/thumbnail_placeholder.dart'; -import 'package:octo_image/octo_image.dart'; - -class ImmichImage extends StatelessWidget { - const ImmichImage( - this.asset, { - this.width, - this.height, - this.fit = BoxFit.cover, - this.placeholder = const ThumbnailPlaceholder(), - super.key, - }); - - final Asset? asset; - final Widget? placeholder; - final double? width; - final double? height; - final BoxFit fit; - - // Helper function to return the image provider for the asset - // either by using the asset ID or the asset itself - /// [asset] is the Asset to request, or else use [assetId] to get a remote - /// image provider - static ImageProvider imageProvider({Asset? asset, String? assetId, double width = 1080, double height = 1920}) { - if (asset == null && assetId == null) { - throw Exception('Must supply either asset or assetId'); - } - - if (asset == null) { - return RemoteFullImageProvider( - assetId: assetId!, - thumbhash: '', - assetType: base_asset.AssetType.video, - isAnimated: false, - ); - } - - if (useLocal(asset)) { - return LocalFullImageProvider( - id: asset.localId!, - assetType: base_asset.AssetType.video, - size: Size(width, height), - isAnimated: false, - ); - } else { - return RemoteFullImageProvider( - assetId: asset.remoteId!, - thumbhash: asset.thumbhash ?? '', - assetType: base_asset.AssetType.video, - isAnimated: false, - ); - } - } - - // Whether to use the local asset image provider or a remote one - static bool useLocal(Asset asset) => - !asset.isRemote || asset.isLocal && !Store.get(StoreKey.preferRemoteImage, false); - - @override - Widget build(BuildContext context) { - if (asset == null) { - return Container( - color: Colors.grey, - width: width, - height: height, - child: const Center(child: Icon(Icons.no_photography)), - ); - } - - final imageProviderInstance = ImmichImage.imageProvider(asset: asset, width: context.width, height: context.height); - - return OctoImage( - fadeInDuration: const Duration(milliseconds: 0), - fadeOutDuration: const Duration(milliseconds: 100), - placeholderBuilder: (context) { - if (placeholder != null) { - return placeholder!; - } - return const SizedBox(); - }, - image: imageProviderInstance, - width: width, - height: height, - fit: fit, - errorBuilder: (context, error, stackTrace) { - imageProviderInstance.evict(); - - return Icon(Icons.image_not_supported_outlined, size: 32, color: Colors.red[200]); - }, - ); - } -} diff --git a/mobile/lib/widgets/common/immich_thumbnail.dart b/mobile/lib/widgets/common/immich_thumbnail.dart deleted file mode 100644 index f17353c3aa..0000000000 --- a/mobile/lib/widgets/common/immich_thumbnail.dart +++ /dev/null @@ -1,88 +0,0 @@ -import 'dart:typed_data'; - -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/presentation/widgets/images/local_image_provider.dart'; -import 'package:immich_mobile/presentation/widgets/images/remote_image_provider.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/utils/hooks/blurhash_hook.dart'; -import 'package:immich_mobile/utils/thumbnail_utils.dart'; -import 'package:immich_mobile/widgets/common/immich_image.dart'; -import 'package:immich_mobile/widgets/common/thumbhash_placeholder.dart'; -import 'package:octo_image/octo_image.dart'; -import 'package:immich_mobile/domain/models/asset/base_asset.model.dart' as base_asset; - -class ImmichThumbnail extends HookConsumerWidget { - const ImmichThumbnail({this.asset, this.width = 250, this.height = 250, this.fit = BoxFit.cover, super.key}); - - final Asset? asset; - final double width; - final double height; - final BoxFit fit; - - /// Helper function to return the image provider for the asset thumbnail - /// either by using the asset ID or the asset itself - /// [asset] is the Asset to request, or else use [assetId] to get a remote - /// image provider - static ImageProvider imageProvider({Asset? asset, String? assetId, int thumbnailSize = 256}) { - if (asset == null && assetId == null) { - throw Exception('Must supply either asset or assetId'); - } - - if (asset == null) { - return RemoteImageProvider.thumbnail(assetId: assetId!, thumbhash: ""); - } - - if (ImmichImage.useLocal(asset)) { - return LocalThumbProvider( - id: asset.localId!, - assetType: base_asset.AssetType.video, - size: Size(thumbnailSize.toDouble(), thumbnailSize.toDouble()), - ); - } else { - return RemoteImageProvider.thumbnail(assetId: asset.remoteId!, thumbhash: asset.thumbhash ?? ""); - } - } - - @override - Widget build(BuildContext context, WidgetRef ref) { - Uint8List? blurhash = useBlurHashRef(asset).value; - - if (asset == null) { - return Container( - color: Colors.grey, - width: width, - height: height, - child: const Center(child: Icon(Icons.no_photography)), - ); - } - - final assetAltText = getAltText(asset!.exifInfo, asset!.fileCreatedAt, asset!.type, []); - - final thumbnailProviderInstance = ImmichThumbnail.imageProvider(asset: asset); - - customErrorBuilder(BuildContext ctx, Object error, StackTrace? stackTrace) { - thumbnailProviderInstance.evict(); - - final originalErrorWidgetBuilder = blurHashErrorBuilder(blurhash, fit: fit); - return originalErrorWidgetBuilder(ctx, error, stackTrace); - } - - return Semantics( - label: assetAltText, - child: OctoImage.fromSet( - placeholderFadeInDuration: Duration.zero, - fadeInDuration: Duration.zero, - fadeOutDuration: const Duration(milliseconds: 100), - octoSet: OctoSet( - placeholderBuilder: blurHashPlaceholderBuilder(blurhash, fit: fit), - errorBuilder: customErrorBuilder, - ), - image: thumbnailProviderInstance, - width: width, - height: height, - fit: fit, - ), - ); - } -} diff --git a/mobile/lib/widgets/common/share_dialog.dart b/mobile/lib/widgets/common/share_dialog.dart deleted file mode 100644 index 625390c4b7..0000000000 --- a/mobile/lib/widgets/common/share_dialog.dart +++ /dev/null @@ -1,19 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; - -class ShareDialog extends StatelessWidget { - const ShareDialog({super.key}); - - @override - Widget build(BuildContext context) { - return AlertDialog( - content: Column( - mainAxisSize: MainAxisSize.min, - children: [ - const CircularProgressIndicator(), - Container(margin: const EdgeInsets.only(top: 12), child: const Text('share_dialog_preparing').tr()), - ], - ), - ); - } -} diff --git a/mobile/lib/widgets/common/thumbhash_placeholder.dart b/mobile/lib/widgets/common/thumbhash_placeholder.dart index 0cb1222989..8a9c2eb928 100644 --- a/mobile/lib/widgets/common/thumbhash_placeholder.dart +++ b/mobile/lib/widgets/common/thumbhash_placeholder.dart @@ -4,15 +4,6 @@ import 'package:immich_mobile/widgets/asset_grid/thumbnail_placeholder.dart'; import 'package:immich_mobile/widgets/common/fade_in_placeholder_image.dart'; import 'package:octo_image/octo_image.dart'; -/// Simple set to show [OctoPlaceholder.circularProgressIndicator] as -/// placeholder and [OctoError.icon] as error. -OctoSet blurHashOrPlaceholder(Uint8List? blurhash, {BoxFit? fit, Text? errorMessage}) { - return OctoSet( - placeholderBuilder: blurHashPlaceholderBuilder(blurhash, fit: fit), - errorBuilder: blurHashErrorBuilder(blurhash, fit: fit, message: errorMessage), - ); -} - OctoPlaceholderBuilder blurHashPlaceholderBuilder(Uint8List? blurhash, {BoxFit? fit}) { return (context) => blurhash == null ? const ThumbnailPlaceholder() diff --git a/mobile/lib/widgets/forms/change_password_form.dart b/mobile/lib/widgets/forms/change_password_form.dart index 179b05a712..7ed9fa5f1c 100644 --- a/mobile/lib/widgets/forms/change_password_form.dart +++ b/mobile/lib/widgets/forms/change_password_form.dart @@ -1,14 +1,11 @@ -import 'package:easy_localization/easy_localization.dart'; 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:fluttertoast/fluttertoast.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/providers/backup/backup.provider.dart'; -import 'package:immich_mobile/providers/backup/manual_upload.provider.dart'; import 'package:immich_mobile/providers/auth.provider.dart'; -import 'package:immich_mobile/providers/asset.provider.dart'; import 'package:immich_mobile/providers/websocket.provider.dart'; import 'package:immich_mobile/widgets/common/immich_toast.dart'; @@ -64,10 +61,6 @@ class ChangePasswordForm extends HookConsumerWidget { if (isSuccess) { await ref.read(authProvider.notifier).logout(); - - ref.read(manualUploadProvider.notifier).cancelBackup(); - ref.read(backupProvider.notifier).cancelBackup(); - await ref.read(assetProvider.notifier).clearAllAssets(); ref.read(websocketProvider.notifier).disconnect(); AutoRouter.of(context).back(); diff --git a/mobile/lib/widgets/forms/login/email_input.dart b/mobile/lib/widgets/forms/login/email_input.dart deleted file mode 100644 index 4d90d918ac..0000000000 --- a/mobile/lib/widgets/forms/login/email_input.dart +++ /dev/null @@ -1,41 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; - -class EmailInput extends StatelessWidget { - final TextEditingController controller; - final FocusNode? focusNode; - final Function()? onSubmit; - - const EmailInput({super.key, required this.controller, this.focusNode, this.onSubmit}); - - String? _validateInput(String? email) { - if (email == null || email == '') return null; - if (email.endsWith(' ')) return 'login_form_err_trailing_whitespace'.tr(); - if (email.startsWith(' ')) return 'login_form_err_leading_whitespace'.tr(); - if (email.contains(' ') || !email.contains('@')) { - return 'login_form_err_invalid_email'.tr(); - } - return null; - } - - @override - Widget build(BuildContext context) { - return TextFormField( - autofocus: true, - controller: controller, - decoration: InputDecoration( - labelText: 'email'.tr(), - border: const OutlineInputBorder(), - hintText: 'login_form_email_hint'.tr(), - hintStyle: const TextStyle(fontWeight: FontWeight.normal, fontSize: 14), - ), - validator: _validateInput, - autovalidateMode: AutovalidateMode.always, - autofillHints: const [AutofillHints.email], - keyboardType: TextInputType.emailAddress, - onFieldSubmitted: (_) => onSubmit?.call(), - focusNode: focusNode, - textInputAction: TextInputAction.next, - ); - } -} diff --git a/mobile/lib/widgets/forms/login/loading_icon.dart b/mobile/lib/widgets/forms/login/loading_icon.dart deleted file mode 100644 index 052ce43ac7..0000000000 --- a/mobile/lib/widgets/forms/login/loading_icon.dart +++ /dev/null @@ -1,13 +0,0 @@ -import 'package:flutter/material.dart'; - -class LoadingIcon extends StatelessWidget { - const LoadingIcon({super.key}); - - @override - Widget build(BuildContext context) { - return const Padding( - padding: EdgeInsets.only(top: 18.0), - child: SizedBox(width: 24, height: 24, child: FittedBox(child: CircularProgressIndicator(strokeWidth: 2))), - ); - } -} diff --git a/mobile/lib/widgets/forms/login/login_button.dart b/mobile/lib/widgets/forms/login/login_button.dart deleted file mode 100644 index 0f9fb21d8f..0000000000 --- a/mobile/lib/widgets/forms/login/login_button.dart +++ /dev/null @@ -1,19 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; - -class LoginButton extends ConsumerWidget { - final Function() onPressed; - - const LoginButton({super.key, required this.onPressed}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - return ElevatedButton.icon( - style: ElevatedButton.styleFrom(padding: const EdgeInsets.symmetric(vertical: 12)), - onPressed: onPressed, - icon: const Icon(Icons.login_rounded), - label: const Text("login", style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold)).tr(), - ); - } -} diff --git a/mobile/lib/widgets/forms/login/login_form.dart b/mobile/lib/widgets/forms/login/login_form.dart index 2aa770f104..fb3b9c5977 100644 --- a/mobile/lib/widgets/forms/login/login_form.dart +++ b/mobile/lib/widgets/forms/login/login_form.dart @@ -17,7 +17,6 @@ import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/providers/auth.provider.dart'; import 'package:immich_mobile/providers/background_sync.provider.dart'; -import 'package:immich_mobile/providers/backup/backup.provider.dart'; import 'package:immich_mobile/providers/gallery_permission.provider.dart'; import 'package:immich_mobile/providers/oauth.provider.dart'; import 'package:immich_mobile/providers/server_info.provider.dart'; @@ -34,7 +33,6 @@ import 'package:immich_ui/immich_ui.dart'; import 'package:logging/logging.dart'; import 'package:openapi/api.dart'; import 'package:package_info_plus/package_info_plus.dart'; -import 'package:permission_handler/permission_handler.dart'; class LoginForm extends HookConsumerWidget { LoginForm({super.key}); @@ -246,18 +244,14 @@ class LoginForm extends HookConsumerWidget { if (result.shouldChangePassword && !result.isAdmin) { unawaited(context.pushRoute(const ChangePasswordRoute())); } else { - final isBeta = Store.isBetaTimelineEnabled; - if (isBeta) { - await ref.read(galleryPermissionNotifier.notifier).requestGalleryPermission(); - if (isSyncRemoteDeletionsMode()) { - await getManageMediaPermission(); - } - unawaited(handleSyncFlow()); - ref.read(websocketProvider.notifier).connect(); - unawaited(context.replaceRoute(const TabShellRoute())); - return; + await ref.read(galleryPermissionNotifier.notifier).requestGalleryPermission(); + if (isSyncRemoteDeletionsMode()) { + await getManageMediaPermission(); } - unawaited(context.replaceRoute(const TabControllerRoute())); + unawaited(handleSyncFlow()); + ref.read(websocketProvider.notifier).connect(); + unawaited(context.replaceRoute(const TabShellRoute())); + return; } } catch (error) { ImmichToast.show( @@ -338,21 +332,13 @@ class LoginForm extends HookConsumerWidget { .saveAuthInfo(accessToken: loginResponseDto.accessToken); if (isSuccess) { - final permission = ref.watch(galleryPermissionNotifier); - final isBeta = Store.isBetaTimelineEnabled; - if (!isBeta && (permission.isGranted || permission.isLimited)) { - unawaited(ref.watch(backupProvider.notifier).resumeBackup()); + await ref.read(galleryPermissionNotifier.notifier).requestGalleryPermission(); + if (isSyncRemoteDeletionsMode()) { + await getManageMediaPermission(); } - if (isBeta) { - await ref.read(galleryPermissionNotifier.notifier).requestGalleryPermission(); - if (isSyncRemoteDeletionsMode()) { - await getManageMediaPermission(); - } - unawaited(handleSyncFlow()); - unawaited(context.replaceRoute(const TabShellRoute())); - return; - } - unawaited(context.replaceRoute(const TabControllerRoute())); + unawaited(handleSyncFlow()); + unawaited(context.replaceRoute(const TabShellRoute())); + return; } } catch (error, stack) { log.severe('Error logging in with OAuth: $error', stack); diff --git a/mobile/lib/widgets/map/map_app_bar.dart b/mobile/lib/widgets/map/map_app_bar.dart deleted file mode 100644 index 73706c7661..0000000000 --- a/mobile/lib/widgets/map/map_app_bar.dart +++ /dev/null @@ -1,128 +0,0 @@ -import 'dart:async'; - -import 'package:auto_route/auto_route.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/providers/map/map_state.provider.dart'; -import 'package:immich_mobile/utils/immich_loading_overlay.dart'; -import 'package:immich_mobile/utils/selection_handlers.dart'; -import 'package:immich_mobile/widgets/map/map_settings_sheet.dart'; - -class MapAppBar extends HookWidget implements PreferredSizeWidget { - final ValueNotifier> selectedAssets; - - const MapAppBar({super.key, required this.selectedAssets}); - - @override - Widget build(BuildContext context) { - return Padding( - padding: EdgeInsets.only(top: context.padding.top + 25), - child: ValueListenableBuilder( - valueListenable: selectedAssets, - builder: (ctx, value, child) => - value.isNotEmpty ? _SelectionRow(selectedAssets: selectedAssets) : const _NonSelectionRow(), - ), - ); - } - - @override - Size get preferredSize => const Size.fromHeight(100); -} - -class _NonSelectionRow extends StatelessWidget { - const _NonSelectionRow(); - - @override - Widget build(BuildContext context) { - void onSettingsPressed() { - showModalBottomSheet( - elevation: 0.0, - showDragHandle: true, - isScrollControlled: true, - context: context, - builder: (_) => const MapSettingsSheet(), - ); - } - - return Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - ElevatedButton( - onPressed: () => context.maybePop(), - style: ElevatedButton.styleFrom(shape: const CircleBorder()), - child: const Icon(Icons.arrow_back_ios_new_rounded), - ), - ElevatedButton( - onPressed: onSettingsPressed, - style: ElevatedButton.styleFrom(shape: const CircleBorder()), - child: const Icon(Icons.more_vert_rounded), - ), - ], - ); - } -} - -class _SelectionRow extends HookConsumerWidget { - final ValueNotifier> selectedAssets; - - const _SelectionRow({required this.selectedAssets}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final isProcessing = useProcessingOverlay(); - - Future handleProcessing(FutureOr Function() action, [bool reloadMarkers = false]) async { - isProcessing.value = true; - await action(); - // Reset state - selectedAssets.value = {}; - isProcessing.value = false; - if (reloadMarkers) { - ref.read(mapStateNotifierProvider.notifier).setRefetchMarkers(true); - } - } - - return Row( - children: [ - Padding( - padding: const EdgeInsets.only(left: 20), - child: ElevatedButton.icon( - onPressed: () => selectedAssets.value = {}, - icon: const Icon(Icons.close_rounded), - label: Text( - '${selectedAssets.value.length}', - style: context.textTheme.titleMedium?.copyWith(color: context.colorScheme.onPrimary), - ), - ), - ), - Expanded( - child: Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - ElevatedButton( - onPressed: () => handleProcessing(() => handleShareAssets(ref, context, selectedAssets.value.toList())), - style: ElevatedButton.styleFrom(shape: const CircleBorder()), - child: const Icon(Icons.ios_share_rounded), - ), - ElevatedButton( - onPressed: () => - handleProcessing(() => handleFavoriteAssets(ref, context, selectedAssets.value.toList())), - style: ElevatedButton.styleFrom(shape: const CircleBorder()), - child: const Icon(Icons.favorite), - ), - ElevatedButton( - onPressed: () => - handleProcessing(() => handleArchiveAssets(ref, context, selectedAssets.value.toList()), true), - style: ElevatedButton.styleFrom(shape: const CircleBorder()), - child: const Icon(Icons.archive), - ), - ], - ), - ), - ], - ); - } -} diff --git a/mobile/lib/widgets/map/map_asset_grid.dart b/mobile/lib/widgets/map/map_asset_grid.dart deleted file mode 100644 index b6c1e708a7..0000000000 --- a/mobile/lib/widgets/map/map_asset_grid.dart +++ /dev/null @@ -1,289 +0,0 @@ -import 'dart:math' as math; - -import 'package:collection/collection.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/entities/asset.entity.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/extensions/collection_extensions.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; -import 'package:immich_mobile/models/map/map_event.model.dart'; -import 'package:immich_mobile/providers/db.provider.dart'; -import 'package:immich_mobile/providers/timeline.provider.dart'; -import 'package:immich_mobile/utils/color_filter_generator.dart'; -import 'package:immich_mobile/utils/throttle.dart'; -import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart'; -import 'package:immich_mobile/widgets/asset_grid/immich_asset_grid.dart'; -import 'package:immich_mobile/widgets/common/drag_sheet.dart'; -import 'package:logging/logging.dart'; -import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; - -class MapAssetGrid extends HookConsumerWidget { - final Stream mapEventStream; - final Function(String)? onGridAssetChanged; - final Function(String)? onZoomToAsset; - final Function(bool, Set)? onAssetsSelected; - final ValueNotifier> selectedAssets; - final ScrollController controller; - - const MapAssetGrid({ - required this.mapEventStream, - this.onGridAssetChanged, - this.onZoomToAsset, - this.onAssetsSelected, - required this.selectedAssets, - required this.controller, - super.key, - }); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final log = Logger("MapAssetGrid"); - final assetsInBounds = useState>([]); - final cachedRenderList = useRef(null); - final lastRenderElementIndex = useRef(null); - final assetInSheet = useValueNotifier(null); - final gridScrollThrottler = useThrottler(interval: const Duration(milliseconds: 300)); - - // Add a cache for assets we've already loaded - final assetCache = useRef>({}); - - void handleMapEvents(MapEvent event) async { - if (event is MapAssetsInBoundsUpdated) { - final assetIds = event.assetRemoteIds; - final missingIds = []; - final currentAssets = []; - - for (final id in assetIds) { - final asset = assetCache.value[id]; - if (asset != null) { - currentAssets.add(asset); - } else { - missingIds.add(id); - } - } - - // Only fetch missing assets - if (missingIds.isNotEmpty) { - final newAssets = await ref.read(dbProvider).assets.getAllByRemoteId(missingIds); - - // Add new assets to cache and current list - for (final asset in newAssets) { - if (asset.remoteId != null) { - assetCache.value[asset.remoteId!] = asset; - currentAssets.add(asset); - } - } - } - - assetsInBounds.value = currentAssets; - return; - } - } - - useOnStreamChange(mapEventStream, onData: handleMapEvents); - - // Hard-restrict to 4 assets / row in portrait mode - const assetsPerRow = 4; - - void handleVisibleItems(Iterable positions) { - final orderedPos = positions.sortedByField((p) => p.index); - // Index of row where the items are mostly visible - const partialOffset = 0.20; - final item = orderedPos.firstWhereOrNull((p) => p.itemTrailingEdge > partialOffset); - - // Guard no elements, reset state - // Also fail fast when the sheet is just opened and the user is yet to scroll (i.e leading = 0) - if (item == null || item.itemLeadingEdge == 0) { - lastRenderElementIndex.value = null; - return; - } - - final renderElement = cachedRenderList.value?.elements.elementAtOrNull(item.index); - // Guard no render list or render element - if (renderElement == null) { - return; - } - // Reset index - lastRenderElementIndex.value == item.index; - - // - // | 1 | 2 | 3 | 4 | 5 | 6 | - // - // | 7 | 8 | 9 | - // - // | 10 | - - // Skip through the assets from the previous row - final rowOffset = renderElement.offset; - // Column offset = (total trailingEdge - trailingEdge crossed) / offset for each asset - final totalOffset = item.itemTrailingEdge - item.itemLeadingEdge; - final edgeOffset = - (totalOffset - partialOffset) / - // Round the total count to the next multiple of [assetsPerRow] - ((renderElement.totalCount / assetsPerRow) * assetsPerRow).floor(); - - // trailing should never be above the totalOffset - final columnOffset = (totalOffset - math.min(item.itemTrailingEdge, totalOffset)) ~/ edgeOffset; - final assetOffset = rowOffset + columnOffset; - final selectedAsset = cachedRenderList.value?.allAssets?.elementAtOrNull(assetOffset)?.remoteId; - - if (selectedAsset != null) { - onGridAssetChanged?.call(selectedAsset); - assetInSheet.value = selectedAsset; - } - } - - return Card( - margin: EdgeInsets.zero, - child: Stack( - children: [ - /// The Align and FractionallySizedBox are to prevent the Asset Grid from going behind the - /// _MapSheetDragRegion and thereby displaying content behind the top right and top left curves - Align( - alignment: Alignment.bottomCenter, - child: FractionallySizedBox( - // Place it just below the drag handle - heightFactor: 0.87, - child: assetsInBounds.value.isNotEmpty - ? ref - .watch(assetsTimelineProvider(assetsInBounds.value)) - .when( - data: (renderList) { - // Cache render list here to use it back during visibleItemsListener - cachedRenderList.value = renderList; - return ValueListenableBuilder( - valueListenable: selectedAssets, - builder: (_, value, __) => ImmichAssetGrid( - shrinkWrap: true, - renderList: renderList, - showDragScroll: false, - assetsPerRow: assetsPerRow, - showMultiSelectIndicator: false, - selectionActive: value.isNotEmpty, - listener: onAssetsSelected, - visibleItemsListener: (pos) => gridScrollThrottler.run(() => handleVisibleItems(pos)), - ), - ); - }, - error: (error, stackTrace) { - log.warning("Cannot get assets in the current map bounds", error, stackTrace); - return const SizedBox.shrink(); - }, - loading: () => const SizedBox.shrink(), - ) - : const _MapNoAssetsInSheet(), - ), - ), - _MapSheetDragRegion( - controller: controller, - assetsInBoundCount: assetsInBounds.value.length, - assetInSheet: assetInSheet, - onZoomToAsset: onZoomToAsset, - ), - ], - ), - ); - } -} - -class _MapNoAssetsInSheet extends StatelessWidget { - const _MapNoAssetsInSheet(); - - @override - Widget build(BuildContext context) { - const image = Image(height: 150, width: 150, image: AssetImage('assets/lighthouse.png')); - - return Center( - child: ListView( - shrinkWrap: true, - children: [ - context.isDarkTheme - ? const InvertionFilter( - child: SaturationFilter(saturation: -1, child: BrightnessFilter(brightness: -5, child: image)), - ) - : image, - const SizedBox(height: 20), - Center( - child: Text("map_zoom_to_see_photos".tr(), style: context.textTheme.displayLarge?.copyWith(fontSize: 18)), - ), - ], - ), - ); - } -} - -class _MapSheetDragRegion extends StatelessWidget { - final ScrollController controller; - final int assetsInBoundCount; - final ValueNotifier assetInSheet; - final Function(String)? onZoomToAsset; - - const _MapSheetDragRegion({ - required this.controller, - required this.assetsInBoundCount, - required this.assetInSheet, - this.onZoomToAsset, - }); - - @override - Widget build(BuildContext context) { - final assetsInBoundsText = "map_assets_in_bounds".t(context: context, args: {'count': assetsInBoundCount}); - - return SingleChildScrollView( - controller: controller, - physics: const ClampingScrollPhysics(), - child: Card( - margin: EdgeInsets.zero, - shape: context.isMobile - ? const RoundedRectangleBorder( - borderRadius: BorderRadius.only(topRight: Radius.circular(20), topLeft: Radius.circular(20)), - ) - : const BeveledRectangleBorder(), - elevation: 0.0, - child: Stack( - children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const SizedBox(height: 15), - const CustomDraggingHandle(), - const SizedBox(height: 15), - Center( - child: Text( - assetsInBoundsText, - style: TextStyle( - fontSize: 20, - color: context.textTheme.displayLarge?.color?.withValues(alpha: 0.75), - fontWeight: FontWeight.w500, - ), - ), - ), - const SizedBox(height: 8), - ], - ), - ValueListenableBuilder( - valueListenable: assetInSheet, - builder: (_, value, __) => Visibility( - visible: value != null, - child: Positioned( - right: 18, - top: 24, - child: IconButton( - icon: Icon(Icons.map_outlined, color: context.textTheme.displayLarge?.color), - iconSize: 24, - tooltip: 'zoom_to_bounds'.tr(), - onPressed: () => onZoomToAsset?.call(value!), - ), - ), - ), - ), - ], - ), - ), - ); - } -} diff --git a/mobile/lib/widgets/map/map_bottom_sheet.dart b/mobile/lib/widgets/map/map_bottom_sheet.dart deleted file mode 100644 index fba9e9a041..0000000000 --- a/mobile/lib/widgets/map/map_bottom_sheet.dart +++ /dev/null @@ -1,98 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/models/map/map_event.model.dart'; -import 'package:immich_mobile/utils/draggable_scroll_controller.dart'; -import 'package:immich_mobile/widgets/map/map_asset_grid.dart'; - -class MapBottomSheet extends HookConsumerWidget { - final Stream mapEventStream; - final Function(String)? onGridAssetChanged; - final Function(String)? onZoomToAsset; - final Function()? onZoomToLocation; - final Function(bool, Set)? onAssetsSelected; - final ValueNotifier> selectedAssets; - - const MapBottomSheet({ - required this.mapEventStream, - this.onGridAssetChanged, - this.onZoomToAsset, - this.onAssetsSelected, - this.onZoomToLocation, - required this.selectedAssets, - super.key, - }); - - @override - Widget build(BuildContext context, WidgetRef ref) { - const sheetMinExtent = 0.1; - final sheetController = useDraggableScrollController(); - final bottomSheetOffset = useValueNotifier(sheetMinExtent); - final isBottomSheetOpened = useRef(false); - - void handleMapEvents(MapEvent event) async { - if (event is MapCloseBottomSheet) { - await sheetController.animateTo( - 0.1, - duration: const Duration(milliseconds: 200), - curve: Curves.linearToEaseOut, - ); - } - } - - useOnStreamChange(mapEventStream, onData: handleMapEvents); - - bool onScrollNotification(DraggableScrollableNotification notification) { - isBottomSheetOpened.value = notification.extent > (notification.maxExtent * 0.9); - bottomSheetOffset.value = notification.extent; - // do not bubble - return true; - } - - return Stack( - children: [ - NotificationListener( - onNotification: onScrollNotification, - child: DraggableScrollableSheet( - controller: sheetController, - minChildSize: sheetMinExtent, - maxChildSize: 0.8, - initialChildSize: sheetMinExtent, - snap: true, - snapSizes: [sheetMinExtent, 0.5, 0.8], - shouldCloseOnMinExtent: false, - builder: (ctx, scrollController) => MapAssetGrid( - controller: scrollController, - mapEventStream: mapEventStream, - selectedAssets: selectedAssets, - onAssetsSelected: onAssetsSelected, - // Do not bother with the event if the bottom sheet is not user scrolled - onGridAssetChanged: (assetId) => isBottomSheetOpened.value ? onGridAssetChanged?.call(assetId) : null, - onZoomToAsset: onZoomToAsset, - ), - ), - ), - ValueListenableBuilder( - valueListenable: bottomSheetOffset, - builder: (context, value, child) { - return Positioned( - right: 0, - bottom: context.height * (value + 0.02), - child: AnimatedOpacity( - opacity: value < 0.8 ? 1 : 0, - duration: const Duration(milliseconds: 150), - child: ElevatedButton( - onPressed: onZoomToLocation, - style: ElevatedButton.styleFrom(shape: const CircleBorder()), - child: const Icon(Icons.my_location), - ), - ), - ); - }, - ), - ], - ); - } -} diff --git a/mobile/lib/widgets/map/map_settings_sheet.dart b/mobile/lib/widgets/map/map_settings_sheet.dart deleted file mode 100644 index 644056d153..0000000000 --- a/mobile/lib/widgets/map/map_settings_sheet.dart +++ /dev/null @@ -1,61 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/providers/map/map_state.provider.dart'; -import 'package:immich_mobile/widgets/map/map_settings/map_settings_list_tile.dart'; -import 'package:immich_mobile/widgets/map/map_settings/map_settings_time_dropdown.dart'; -import 'package:immich_mobile/widgets/map/map_settings/map_theme_picker.dart'; - -class MapSettingsSheet extends HookConsumerWidget { - const MapSettingsSheet({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final mapState = ref.watch(mapStateNotifierProvider); - - return DraggableScrollableSheet( - expand: false, - initialChildSize: 0.6, - builder: (ctx, scrollController) => SingleChildScrollView( - controller: scrollController, - child: Card( - elevation: 0.0, - shadowColor: Colors.transparent, - margin: EdgeInsets.zero, - child: Column( - mainAxisSize: MainAxisSize.max, - children: [ - MapThemePicker( - themeMode: mapState.themeMode, - onThemeChange: (mode) => ref.read(mapStateNotifierProvider.notifier).switchTheme(mode), - ), - const Divider(height: 30, thickness: 2), - MapSettingsListTile( - title: "map_settings_only_show_favorites", - selected: mapState.showFavoriteOnly, - onChanged: (favoriteOnly) => - ref.read(mapStateNotifierProvider.notifier).switchFavoriteOnly(favoriteOnly), - ), - MapSettingsListTile( - title: "map_settings_include_show_archived", - selected: mapState.includeArchived, - onChanged: (includeArchive) => - ref.read(mapStateNotifierProvider.notifier).switchIncludeArchived(includeArchive), - ), - MapSettingsListTile( - title: "map_settings_include_show_partners", - selected: mapState.withPartners, - onChanged: (withPartners) => - ref.read(mapStateNotifierProvider.notifier).switchWithPartners(withPartners), - ), - MapTimeDropDown( - relativeTime: mapState.relativeTime, - onTimeChange: (time) => ref.read(mapStateNotifierProvider.notifier).setRelativeTime(time), - ), - const SizedBox(height: 20), - ], - ), - ), - ), - ); - } -} diff --git a/mobile/lib/widgets/map/positioned_asset_marker_icon.dart b/mobile/lib/widgets/map/positioned_asset_marker_icon.dart deleted file mode 100644 index b6d7241cf4..0000000000 --- a/mobile/lib/widgets/map/positioned_asset_marker_icon.dart +++ /dev/null @@ -1,43 +0,0 @@ -import 'dart:io'; -import 'dart:math'; - -import 'package:flutter/material.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/widgets/map/asset_marker_icon.dart'; - -class PositionedAssetMarkerIcon extends StatelessWidget { - final Point point; - final String assetRemoteId; - final String assetThumbhash; - final double size; - final int durationInMilliseconds; - - final Function()? onTap; - - const PositionedAssetMarkerIcon({ - required this.point, - required this.assetRemoteId, - required this.assetThumbhash, - this.size = 100, - this.durationInMilliseconds = 100, - this.onTap, - super.key, - }); - - @override - Widget build(BuildContext context) { - final ratio = Platform.isIOS ? 1.0 : context.devicePixelRatio; - return AnimatedPositioned( - left: point.x / ratio - size / 2, - top: point.y / ratio - size, - duration: Duration(milliseconds: durationInMilliseconds), - child: GestureDetector( - onTap: () => onTap?.call(), - child: SizedBox.square( - dimension: size, - child: AssetMarkerIcon(id: assetRemoteId, thumbhash: assetThumbhash, key: Key(assetRemoteId)), - ), - ), - ); - } -} diff --git a/mobile/lib/widgets/memories/memory_bottom_info.dart b/mobile/lib/widgets/memories/memory_bottom_info.dart deleted file mode 100644 index 4b43821782..0000000000 --- a/mobile/lib/widgets/memories/memory_bottom_info.dart +++ /dev/null @@ -1,50 +0,0 @@ -// ignore_for_file: require_trailing_commas - -import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:immich_mobile/models/memories/memory.model.dart'; -import 'package:immich_mobile/providers/asset_viewer/scroll_to_date_notifier.provider.dart'; - -class MemoryBottomInfo extends StatelessWidget { - final Memory memory; - - const MemoryBottomInfo({super.key, required this.memory}); - - @override - Widget build(BuildContext context) { - final df = DateFormat.yMMMMd(); - return Padding( - padding: const EdgeInsets.all(16.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - memory.title, - style: TextStyle(color: Colors.grey[400], fontSize: 13.0, fontWeight: FontWeight.w500), - ), - Text( - df.format(memory.assets[0].fileCreatedAt), - style: const TextStyle(color: Colors.white, fontSize: 15.0, fontWeight: FontWeight.w500), - ), - ], - ), - MaterialButton( - minWidth: 0, - onPressed: () { - context.maybePop(); - scrollToDateNotifierProvider.scrollToDate(memory.assets[0].fileCreatedAt); - }, - shape: const CircleBorder(), - color: Colors.white.withValues(alpha: 0.2), - elevation: 0, - child: const Icon(Icons.open_in_new, color: Colors.white), - ), - ], - ), - ); - } -} diff --git a/mobile/lib/widgets/memories/memory_card.dart b/mobile/lib/widgets/memories/memory_card.dart deleted file mode 100644 index 189cc67428..0000000000 --- a/mobile/lib/widgets/memories/memory_card.dart +++ /dev/null @@ -1,117 +0,0 @@ -import 'dart:ui'; - -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/pages/common/native_video_viewer.page.dart'; -import 'package:immich_mobile/utils/hooks/blurhash_hook.dart'; -import 'package:immich_mobile/widgets/common/immich_image.dart'; - -class MemoryCard extends StatelessWidget { - final Asset asset; - final String title; - final bool showTitle; - final Function()? onVideoEnded; - - const MemoryCard({required this.asset, required this.title, required this.showTitle, this.onVideoEnded, super.key}); - - @override - Widget build(BuildContext context) { - return Card( - color: Colors.black, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.all(Radius.circular(25.0)), - side: BorderSide(color: Colors.black, width: 1.0), - ), - clipBehavior: Clip.hardEdge, - child: Stack( - children: [ - SizedBox.expand(child: _BlurredBackdrop(asset: asset)), - LayoutBuilder( - builder: (context, constraints) { - // Determine the fit using the aspect ratio - BoxFit fit = BoxFit.contain; - if (asset.width != null && asset.height != null) { - final aspectRatio = asset.width! / asset.height!; - final phoneAspectRatio = constraints.maxWidth / constraints.maxHeight; - // Look for a 25% difference in either direction - if (phoneAspectRatio * .75 < aspectRatio && phoneAspectRatio * 1.25 > aspectRatio) { - // Cover to look nice if we have nearly the same aspect ratio - fit = BoxFit.cover; - } - } - - if (asset.isImage) { - return Hero( - tag: 'memory-${asset.id}', - child: ImmichImage(asset, fit: fit, height: double.infinity, width: double.infinity), - ); - } else { - return Hero( - tag: 'memory-${asset.id}', - child: SizedBox( - width: context.width, - height: context.height, - child: NativeVideoViewerPage( - key: ValueKey(asset.id), - asset: asset, - showControls: false, - playbackDelayFactor: 2, - image: ImmichImage(asset, width: context.width, height: context.height, fit: BoxFit.contain), - ), - ), - ); - } - }, - ), - if (showTitle) - Positioned( - left: 18.0, - bottom: 18.0, - child: Text( - title, - style: context.textTheme.headlineMedium?.copyWith(color: Colors.white, fontWeight: FontWeight.w500), - ), - ), - ], - ), - ); - } -} - -class _BlurredBackdrop extends HookWidget { - final Asset asset; - - const _BlurredBackdrop({required this.asset}); - - @override - Widget build(BuildContext context) { - final blurhash = useBlurHashRef(asset).value; - if (blurhash != null) { - // Use a nice cheap blur hash image decoration - return Container( - decoration: BoxDecoration( - image: DecorationImage(image: MemoryImage(blurhash), fit: BoxFit.cover), - ), - child: Container(color: Colors.black.withValues(alpha: 0.2)), - ); - } else { - // Fall back to using a more expensive image filtered - // Since the ImmichImage is already precached, we can - // safely use that as the image provider - return ImageFiltered( - imageFilter: ImageFilter.blur(sigmaX: 30, sigmaY: 30), - child: Container( - decoration: BoxDecoration( - image: DecorationImage( - image: ImmichImage.imageProvider(asset: asset, height: context.height, width: context.width), - fit: BoxFit.cover, - ), - ), - child: Container(color: Colors.black.withValues(alpha: 0.2)), - ), - ); - } - } -} diff --git a/mobile/lib/widgets/memories/memory_lane.dart b/mobile/lib/widgets/memories/memory_lane.dart deleted file mode 100644 index 4cba83bea7..0000000000 --- a/mobile/lib/widgets/memories/memory_lane.dart +++ /dev/null @@ -1,91 +0,0 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:collection/collection.dart'; -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/models/memories/memory.model.dart'; -import 'package:immich_mobile/widgets/asset_grid/thumbnail_placeholder.dart'; -import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart'; -import 'package:immich_mobile/providers/memory.provider.dart'; -import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/providers/haptic_feedback.provider.dart'; -import 'package:immich_mobile/widgets/common/immich_image.dart'; - -class MemoryLane extends HookConsumerWidget { - const MemoryLane({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final memoryLaneFutureProvider = ref.watch(memoryFutureProvider); - - final memoryLane = memoryLaneFutureProvider - .whenData( - (memories) => memories != null - ? ConstrainedBox( - constraints: const BoxConstraints(maxHeight: 200), - child: CarouselView( - itemExtent: 145.0, - shrinkExtent: 1.0, - elevation: 2, - backgroundColor: Colors.black, - overlayColor: WidgetStateProperty.all(Colors.white.withValues(alpha: 0.1)), - onTap: (memoryIndex) { - ref.read(hapticFeedbackProvider.notifier).heavyImpact(); - if (memories[memoryIndex].assets.isNotEmpty) { - final asset = memories[memoryIndex].assets[0]; - ref.read(currentAssetProvider.notifier).set(asset); - } - context.pushRoute(MemoryRoute(memories: memories, memoryIndex: memoryIndex)); - }, - children: memories - .mapIndexed((index, memory) => MemoryCard(index: index, memory: memory)) - .toList(), - ), - ) - : const SizedBox(), - ) - .value; - - return memoryLane ?? const SizedBox(); - } -} - -class MemoryCard extends ConsumerWidget { - const MemoryCard({super.key, required this.index, required this.memory}); - - final int index; - final Memory memory; - - @override - Widget build(BuildContext context, WidgetRef ref) { - return Center( - child: Stack( - children: [ - ColorFiltered( - colorFilter: ColorFilter.mode(Colors.black.withValues(alpha: 0.2), BlendMode.darken), - child: Hero( - tag: 'memory-${memory.assets[0].id}', - child: ImmichImage( - memory.assets[0], - fit: BoxFit.cover, - width: 205, - height: 200, - placeholder: const ThumbnailPlaceholder(width: 105, height: 200), - ), - ), - ), - Positioned( - bottom: 16, - left: 16, - child: ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 114), - child: Text( - memory.title, - style: const TextStyle(fontWeight: FontWeight.w600, color: Colors.white, fontSize: 15), - ), - ), - ), - ], - ), - ); - } -} diff --git a/mobile/lib/widgets/search/curated_people_row.dart b/mobile/lib/widgets/search/curated_people_row.dart deleted file mode 100644 index 9155de2131..0000000000 --- a/mobile/lib/widgets/search/curated_people_row.dart +++ /dev/null @@ -1,89 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/models/search/search_curated_content.model.dart'; -import 'package:immich_mobile/presentation/widgets/images/remote_image_provider.dart'; -import 'package:immich_mobile/utils/image_url_builder.dart'; - -class CuratedPeopleRow extends StatelessWidget { - static const double imageSize = 60.0; - - final List content; - final EdgeInsets? padding; - - /// Callback with the content and the index when tapped - final Function(SearchCuratedContent, int)? onTap; - final Function(SearchCuratedContent, int)? onNameTap; - - const CuratedPeopleRow({super.key, required this.content, this.onTap, this.padding, required this.onNameTap}); - - @override - Widget build(BuildContext context) { - return SizedBox( - width: double.infinity, - child: SingleChildScrollView( - padding: padding, - scrollDirection: Axis.horizontal, - child: Row( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, - children: List.generate(content.length, (index) { - final person = content[index]; - return Padding( - padding: const EdgeInsets.only(right: 16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - GestureDetector( - onTap: () => onTap?.call(person, index), - child: SizedBox( - height: imageSize, - child: Material( - shape: const CircleBorder(side: BorderSide.none), - elevation: 3, - child: CircleAvatar( - maxRadius: imageSize / 2, - backgroundImage: RemoteImageProvider(url: getFaceThumbnailUrl(person.id)), - ), - ), - ), - ), - const SizedBox(height: 8), - SizedBox(width: imageSize, child: _buildPersonLabel(context, person, index)), - ], - ), - ); - }), - ), - ), - ); - } - - Widget _buildPersonLabel(BuildContext context, SearchCuratedContent person, int index) { - if (person.label.isEmpty) { - return GestureDetector( - onTap: () => onNameTap?.call(person, index), - child: Text( - "exif_bottom_sheet_person_add_person", - style: context.textTheme.labelLarge?.copyWith(color: context.primaryColor), - maxLines: 2, - overflow: TextOverflow.ellipsis, - textAlign: TextAlign.center, - ).tr(), - ); - } - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - person.label, - textAlign: TextAlign.center, - overflow: TextOverflow.ellipsis, - style: context.textTheme.labelLarge, - maxLines: 2, - ), - if (person.subtitle != null) Text(person.subtitle!, textAlign: TextAlign.center), - ], - ); - } -} diff --git a/mobile/lib/widgets/search/curated_places_row.dart b/mobile/lib/widgets/search/curated_places_row.dart deleted file mode 100644 index 9d21292bde..0000000000 --- a/mobile/lib/widgets/search/curated_places_row.dart +++ /dev/null @@ -1,60 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:immich_mobile/domain/models/store.model.dart'; -import 'package:immich_mobile/entities/store.entity.dart'; -import 'package:immich_mobile/models/search/search_curated_content.model.dart'; -import 'package:immich_mobile/widgets/search/search_map_thumbnail.dart'; -import 'package:immich_mobile/widgets/search/thumbnail_with_info.dart'; - -class CuratedPlacesRow extends StatelessWidget { - const CuratedPlacesRow({ - super.key, - required this.content, - required this.imageSize, - this.isMapEnabled = true, - this.onTap, - }); - - final bool isMapEnabled; - final List content; - final double imageSize; - - /// Callback with the content and the index when tapped - final Function(SearchCuratedContent, int)? onTap; - - @override - Widget build(BuildContext context) { - // Calculating the actual index of the content based on the whether map is enabled or not. - // If enabled, inject map as the first item in the list (index 0) and so the actual content will start from index 1 - final int actualContentIndex = isMapEnabled ? 1 : 0; - - return SizedBox( - height: imageSize, - child: ListView.separated( - scrollDirection: Axis.horizontal, - padding: const EdgeInsets.symmetric(horizontal: 16), - separatorBuilder: (context, index) => const SizedBox(width: 10), - itemBuilder: (context, index) { - // Injecting Map thumbnail as the first element - if (isMapEnabled && index == 0) { - return SizedBox.square( - dimension: imageSize, - child: SearchMapThumbnail(size: imageSize), - ); - } - final actualIndex = index - actualContentIndex; - final object = content[actualIndex]; - final thumbnailRequestUrl = '${Store.get(StoreKey.serverEndpoint)}/assets/${object.id}/thumbnail'; - return SizedBox.square( - dimension: imageSize, - child: ThumbnailWithInfo( - imageUrl: thumbnailRequestUrl, - textInfo: object.label, - onTap: () => onTap?.call(object, actualIndex), - ), - ); - }, - itemCount: content.length + actualContentIndex, - ), - ); - } -} diff --git a/mobile/lib/widgets/search/explore_grid.dart b/mobile/lib/widgets/search/explore_grid.dart deleted file mode 100644 index 6af20df029..0000000000 --- a/mobile/lib/widgets/search/explore_grid.dart +++ /dev/null @@ -1,69 +0,0 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:flutter/material.dart'; -import 'package:immich_mobile/domain/models/store.model.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/entities/store.entity.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/routing/router.dart'; -import 'package:immich_mobile/utils/image_url_builder.dart'; -import 'package:immich_mobile/widgets/search/thumbnail_with_info.dart'; - -class ExploreGrid extends StatelessWidget { - final List curatedContent; - final bool isPeople; - - const ExploreGrid({super.key, required this.curatedContent, this.isPeople = false}); - - @override - Widget build(BuildContext context) { - if (curatedContent.isEmpty) { - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0), - child: SizedBox( - height: 100, - width: 100, - child: ThumbnailWithInfo(textInfo: '', onTap: () {}), - ), - ); - } - - return GridView.builder( - gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( - maxCrossAxisExtent: 140, - mainAxisSpacing: 4, - crossAxisSpacing: 4, - ), - itemBuilder: (context, index) { - final content = curatedContent[index]; - final thumbnailRequestUrl = isPeople - ? getFaceThumbnailUrl(content.id) - : '${Store.get(StoreKey.serverEndpoint)}/assets/${content.id}/thumbnail'; - - return ThumbnailWithInfo( - imageUrl: thumbnailRequestUrl, - textInfo: content.label, - borderRadius: 0, - onTap: () { - isPeople - ? context.pushRoute(PersonResultRoute(personId: content.id, personName: content.label)) - : context.pushRoute( - SearchRoute( - prefilter: SearchFilter( - people: {}, - location: SearchLocationFilter(city: content.label), - camera: SearchCameraFilter(), - date: SearchDateFilter(), - display: SearchDisplayFilters(isNotInAlbum: false, isArchive: false, isFavorite: false), - rating: SearchRatingFilter(), - mediaType: AssetType.other, - ), - ), - ); - }, - ); - }, - itemCount: curatedContent.length, - ); - } -} diff --git a/mobile/lib/widgets/search/person_name_edit_form.dart b/mobile/lib/widgets/search/person_name_edit_form.dart deleted file mode 100644 index 3fa443121a..0000000000 --- a/mobile/lib/widgets/search/person_name_edit_form.dart +++ /dev/null @@ -1,65 +0,0 @@ -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/providers/search/people.provider.dart'; - -class PersonNameEditFormResult { - final bool success; - final String updatedName; - - const PersonNameEditFormResult(this.success, this.updatedName); -} - -class PersonNameEditForm extends HookConsumerWidget { - final String personId; - final String personName; - - const PersonNameEditForm({super.key, required this.personId, required this.personName}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final controller = useTextEditingController(text: personName); - final isError = useState(false); - - return AlertDialog( - title: const Text("add_a_name", style: TextStyle(fontWeight: FontWeight.bold)).tr(), - content: SingleChildScrollView( - child: TextFormField( - controller: controller, - textCapitalization: TextCapitalization.words, - autofocus: true, - decoration: InputDecoration( - hintText: 'name'.tr(), - border: const OutlineInputBorder(), - errorText: isError.value ? 'Error occurred' : null, - ), - ), - ), - actions: [ - TextButton( - onPressed: () => context.pop(const PersonNameEditFormResult(false, '')), - child: Text( - "cancel", - style: TextStyle(color: Colors.red[300], fontWeight: FontWeight.bold), - ).tr(), - ), - TextButton( - onPressed: () async { - isError.value = false; - final result = await ref.read(updatePersonNameProvider(personId, controller.text).future); - isError.value = !result; - if (result) { - context.pop(PersonNameEditFormResult(true, controller.text)); - } - }, - child: Text( - "save", - style: TextStyle(color: context.primaryColor, fontWeight: FontWeight.bold), - ).tr(), - ), - ], - ); - } -} diff --git a/mobile/lib/widgets/search/search_filter/media_type_picker.dart b/mobile/lib/widgets/search/search_filter/media_type_picker.dart index e0e34b654e..ac89de8190 100644 --- a/mobile/lib/widgets/search/search_filter/media_type_picker.dart +++ b/mobile/lib/widgets/search/search_filter/media_type_picker.dart @@ -1,7 +1,7 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; class MediaTypePicker extends HookWidget { const MediaTypePicker({super.key, required this.onSelect, this.filter}); diff --git a/mobile/lib/widgets/search/search_map_thumbnail.dart b/mobile/lib/widgets/search/search_map_thumbnail.dart deleted file mode 100644 index 7533e46f1a..0000000000 --- a/mobile/lib/widgets/search/search_map_thumbnail.dart +++ /dev/null @@ -1,27 +0,0 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/widgets/map/map_thumbnail.dart'; -import 'package:immich_mobile/widgets/search/thumbnail_with_info_container.dart'; -import 'package:maplibre_gl/maplibre_gl.dart'; - -class SearchMapThumbnail extends StatelessWidget { - const SearchMapThumbnail({super.key, this.size = 60.0}); - - final double size; - final bool showTitle = true; - - @override - Widget build(BuildContext context) { - return ThumbnailWithInfoContainer( - label: 'search_page_your_map'.tr(), - onTap: () { - context.pushRoute(MapRoute()); - }, - child: IgnorePointer( - child: MapThumbnail(zoom: 2, centre: const LatLng(47, 5), height: size, width: size, showAttribution: false), - ), - ); - } -} diff --git a/mobile/lib/widgets/search/search_row_section.dart b/mobile/lib/widgets/search/search_row_section.dart deleted file mode 100644 index b8584fefef..0000000000 --- a/mobile/lib/widgets/search/search_row_section.dart +++ /dev/null @@ -1,34 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:immich_mobile/widgets/search/search_row_title.dart'; - -class SearchRowSection extends StatelessWidget { - const SearchRowSection({ - super.key, - required this.onViewAllPressed, - required this.title, - this.isEmpty = false, - required this.child, - }); - - final Function() onViewAllPressed; - final String title; - final bool isEmpty; - final Widget child; - - @override - Widget build(BuildContext context) { - if (isEmpty) { - return const SizedBox.shrink(); - } - - return Column( - children: [ - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: SearchRowTitle(onViewAllPressed: onViewAllPressed, title: title), - ), - child, - ], - ); - } -} diff --git a/mobile/lib/widgets/settings/advanced_settings.dart b/mobile/lib/widgets/settings/advanced_settings.dart index d5905a246c..a38ccd3556 100644 --- a/mobile/lib/widgets/settings/advanced_settings.dart +++ b/mobile/lib/widgets/settings/advanced_settings.dart @@ -6,7 +6,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart' hide Store; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/services/log.service.dart'; -import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/providers/infrastructure/platform.provider.dart'; import 'package:immich_mobile/providers/infrastructure/readonly_mode.provider.dart'; @@ -14,9 +13,7 @@ import 'package:immich_mobile/repositories/local_files_manager.repository.dart'; import 'package:immich_mobile/services/app_settings.service.dart'; import 'package:immich_mobile/utils/bytes_units.dart'; import 'package:immich_mobile/utils/hooks/app_settings_update_hook.dart'; -import 'package:immich_mobile/widgets/settings/beta_timeline_list_tile.dart'; import 'package:immich_mobile/widgets/settings/custom_proxy_headers_settings/custom_proxy_headers_settings.dart'; -import 'package:immich_mobile/widgets/settings/local_storage_settings.dart'; import 'package:immich_mobile/widgets/settings/settings_action_tile.dart'; import 'package:immich_mobile/widgets/settings/settings_slider_list_tile.dart'; import 'package:immich_mobile/widgets/settings/settings_sub_page_scaffold.dart'; @@ -35,7 +32,6 @@ class AdvancedSettings extends HookConsumerWidget { final manageMediaAndroidPermission = useState(false); final levelId = useAppSettingsState(AppSettingsEnum.logLevel); final preferRemote = useAppSettingsState(AppSettingsEnum.preferRemoteImage); - final useAlternatePMFilter = useAppSettingsState(AppSettingsEnum.photoManagerCustomFilter); final readonlyModeEnabled = useAppSettingsState(AppSettingsEnum.readonlyModeEnabled); final logLevel = Level.LEVELS[levelId.value].name; @@ -114,35 +110,26 @@ class AdvancedSettings extends HookConsumerWidget { title: "advanced_settings_prefer_remote_title".tr(), subtitle: "advanced_settings_prefer_remote_subtitle".tr(), ), - if (!Store.isBetaTimelineEnabled) const LocalStorageSettings(), const CustomProxyHeaderSettings(), const SslClientCertSettings(), - if (!Store.isBetaTimelineEnabled) - SettingsSwitchListTile( - valueNotifier: useAlternatePMFilter, - title: "advanced_settings_enable_alternate_media_filter_title".tr(), - subtitle: "advanced_settings_enable_alternate_media_filter_subtitle".tr(), - ), - if (!Store.isBetaTimelineEnabled) const BetaTimelineListTile(), - if (Store.isBetaTimelineEnabled) - SettingsSwitchListTile( - valueNotifier: readonlyModeEnabled, - title: "advanced_settings_readonly_mode_title".tr(), - subtitle: "advanced_settings_readonly_mode_subtitle".tr(), - onChanged: (value) { - readonlyModeEnabled.value = value; - ref.read(readonlyModeProvider.notifier).setReadonlyMode(value); - context.scaffoldMessenger.showSnackBar( - SnackBar( - duration: const Duration(seconds: 2), - content: Text( - (value ? "readonly_mode_enabled" : "readonly_mode_disabled").tr(), - style: context.textTheme.bodyLarge?.copyWith(color: context.primaryColor), - ), + SettingsSwitchListTile( + valueNotifier: readonlyModeEnabled, + title: "advanced_settings_readonly_mode_title".tr(), + subtitle: "advanced_settings_readonly_mode_subtitle".tr(), + onChanged: (value) { + readonlyModeEnabled.value = value; + ref.read(readonlyModeProvider.notifier).setReadonlyMode(value); + context.scaffoldMessenger.showSnackBar( + SnackBar( + duration: const Duration(seconds: 2), + content: Text( + (value ? "readonly_mode_enabled" : "readonly_mode_disabled").tr(), + style: context.textTheme.bodyLarge?.copyWith(color: context.primaryColor), ), - ); - }, - ), + ), + ); + }, + ), ListTile( title: Text("advanced_settings_clear_image_cache".tr(), style: const TextStyle(fontWeight: FontWeight.w500)), leading: const Icon(Icons.playlist_remove_rounded), diff --git a/mobile/lib/widgets/settings/asset_list_settings/asset_list_group_settings.dart b/mobile/lib/widgets/settings/asset_list_settings/asset_list_group_settings.dart index 08e66df48d..42ea3acfc0 100644 --- a/mobile/lib/widgets/settings/asset_list_settings/asset_list_group_settings.dart +++ b/mobile/lib/widgets/settings/asset_list_settings/asset_list_group_settings.dart @@ -2,11 +2,11 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/domain/models/timeline.model.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/providers/app_settings.provider.dart'; import 'package:immich_mobile/services/app_settings.service.dart'; import 'package:immich_mobile/utils/hooks/app_settings_update_hook.dart'; -import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart'; import 'package:immich_mobile/widgets/settings/setting_group_title.dart'; import 'package:immich_mobile/widgets/settings/settings_radio_list_tile.dart'; diff --git a/mobile/lib/widgets/settings/asset_list_settings/asset_list_layout_settings.dart b/mobile/lib/widgets/settings/asset_list_settings/asset_list_layout_settings.dart index 2d5c9f06eb..55c8195947 100644 --- a/mobile/lib/widgets/settings/asset_list_settings/asset_list_layout_settings.dart +++ b/mobile/lib/widgets/settings/asset_list_settings/asset_list_layout_settings.dart @@ -1,21 +1,18 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/providers/app_settings.provider.dart'; import 'package:immich_mobile/services/app_settings.service.dart'; import 'package:immich_mobile/utils/hooks/app_settings_update_hook.dart'; import 'package:immich_mobile/widgets/settings/setting_group_title.dart'; import 'package:immich_mobile/widgets/settings/settings_slider_list_tile.dart'; -import 'package:immich_mobile/widgets/settings/settings_switch_list_tile.dart'; class LayoutSettings extends HookConsumerWidget { const LayoutSettings({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { - final useDynamicLayout = useAppSettingsState(AppSettingsEnum.dynamicLayout); final tilesPerRow = useAppSettingsState(AppSettingsEnum.tilesPerRow); return Column( @@ -25,12 +22,6 @@ class LayoutSettings extends HookConsumerWidget { title: "asset_list_layout_sub_title".t(context: context), icon: Icons.view_module_outlined, ), - if (!Store.isBetaTimelineEnabled) - SettingsSwitchListTile( - valueNotifier: useDynamicLayout, - title: "asset_list_layout_settings_dynamic_layout_title".t(context: context), - onChanged: (_) => ref.invalidate(appSettingsServiceProvider), - ), SettingsSliderListTile( valueNotifier: tilesPerRow, text: 'theme_setting_asset_list_tiles_per_row_title'.tr(namedArgs: {'count': "${tilesPerRow.value}"}), diff --git a/mobile/lib/widgets/settings/backup_settings/background_settings.dart b/mobile/lib/widgets/settings/backup_settings/background_settings.dart deleted file mode 100644 index 038a567dc2..0000000000 --- a/mobile/lib/widgets/settings/backup_settings/background_settings.dart +++ /dev/null @@ -1,204 +0,0 @@ -import 'dart:io'; - -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/providers/backup/backup.provider.dart'; -import 'package:immich_mobile/providers/backup/ios_background_settings.provider.dart'; -import 'package:immich_mobile/widgets/backup/ios_debug_info_tile.dart'; -import 'package:immich_mobile/widgets/settings/settings_button_list_tile.dart'; -import 'package:immich_mobile/widgets/settings/settings_slider_list_tile.dart'; -import 'package:immich_mobile/widgets/settings/settings_switch_list_tile.dart'; -import 'package:permission_handler/permission_handler.dart'; -import 'package:url_launcher/url_launcher.dart'; - -class BackgroundBackupSettings extends ConsumerWidget { - const BackgroundBackupSettings({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final isBackgroundEnabled = ref.watch(backupProvider.select((s) => s.backgroundBackup)); - final iosSettings = ref.watch(iOSBackgroundSettingsProvider); - - void showErrorToUser(String msg) { - final snackBar = SnackBar( - content: Text(msg.tr(), style: context.textTheme.bodyLarge?.copyWith(color: context.primaryColor)), - backgroundColor: Colors.red, - ); - context.scaffoldMessenger.showSnackBar(snackBar); - } - - void showBatteryOptimizationInfoToUser() { - showDialog( - context: context, - barrierDismissible: false, - builder: (BuildContext ctx) { - return AlertDialog( - title: const Text('backup_controller_page_background_battery_info_title').tr(), - content: SingleChildScrollView( - child: const Text('backup_controller_page_background_battery_info_message').tr(), - ), - actions: [ - ElevatedButton( - onPressed: () => - launchUrl(Uri.parse('https://dontkillmyapp.com'), mode: LaunchMode.externalApplication), - child: const Text( - "backup_controller_page_background_battery_info_link", - style: TextStyle(fontWeight: FontWeight.bold, fontSize: 12), - ).tr(), - ), - ElevatedButton( - child: const Text( - 'backup_controller_page_background_battery_info_ok', - style: TextStyle(fontWeight: FontWeight.bold, fontSize: 12), - ).tr(), - onPressed: () => ctx.pop(), - ), - ], - ); - }, - ); - } - - if (!isBackgroundEnabled) { - return SettingsButtonListTile( - icon: Icons.cloud_sync_outlined, - title: 'backup_controller_page_background_is_off'.tr(), - subtileText: 'backup_controller_page_background_description'.tr(), - buttonText: 'backup_controller_page_background_turn_on'.tr(), - onButtonTap: () => ref - .read(backupProvider.notifier) - .configureBackgroundBackup( - enabled: true, - onError: showErrorToUser, - onBatteryInfo: showBatteryOptimizationInfoToUser, - ), - ); - } - - return Column( - children: [ - if (!Platform.isIOS || iosSettings?.appRefreshEnabled == true) - _BackgroundSettingsEnabled(onError: showErrorToUser, onBatteryInfo: showBatteryOptimizationInfoToUser), - if (Platform.isIOS && iosSettings?.appRefreshEnabled != true) const _IOSBackgroundRefreshDisabled(), - if (Platform.isIOS && iosSettings != null) IosDebugInfoTile(settings: iosSettings), - ], - ); - } -} - -class _IOSBackgroundRefreshDisabled extends StatelessWidget { - const _IOSBackgroundRefreshDisabled(); - - @override - Widget build(BuildContext context) { - return SettingsButtonListTile( - icon: Icons.task_outlined, - title: 'backup_controller_page_background_app_refresh_disabled_title'.tr(), - subtileText: 'backup_controller_page_background_app_refresh_disabled_content'.tr(), - buttonText: 'backup_controller_page_background_app_refresh_enable_button_text'.tr(), - onButtonTap: () => openAppSettings(), - ); - } -} - -class _BackgroundSettingsEnabled extends HookConsumerWidget { - final void Function(String msg) onError; - final void Function() onBatteryInfo; - - const _BackgroundSettingsEnabled({required this.onError, required this.onBatteryInfo}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final isWifiRequired = ref.watch(backupProvider.select((s) => s.backupRequireWifi)); - final isWifiRequiredNotifier = useValueNotifier(isWifiRequired); - useValueChanged( - isWifiRequired, - (_, __) => WidgetsBinding.instance.addPostFrameCallback((_) => isWifiRequiredNotifier.value = isWifiRequired), - ); - - final isChargingRequired = ref.watch(backupProvider.select((s) => s.backupRequireCharging)); - final isChargingRequiredNotifier = useValueNotifier(isChargingRequired); - useValueChanged( - isChargingRequired, - (_, __) => - WidgetsBinding.instance.addPostFrameCallback((_) => isChargingRequiredNotifier.value = isChargingRequired), - ); - - int backupDelayToSliderValue(int ms) => switch (ms) { - 5000 => 0, - 30000 => 1, - 120000 => 2, - _ => 3, - }; - - int backupDelayToMilliseconds(int v) => switch (v) { - 0 => 5000, - 1 => 30000, - 2 => 120000, - _ => 600000, - }; - - String formatBackupDelaySliderValue(int v) => switch (v) { - 0 => 'setting_notifications_notify_seconds'.tr(namedArgs: {'count': '5'}), - 1 => 'setting_notifications_notify_seconds'.tr(namedArgs: {'count': '30'}), - 2 => 'setting_notifications_notify_minutes'.tr(namedArgs: {'count': '2'}), - _ => 'setting_notifications_notify_minutes'.tr(namedArgs: {'count': '10'}), - }; - - final backupTriggerDelay = ref.watch(backupProvider.select((s) => s.backupTriggerDelay)); - final triggerDelay = useState(backupDelayToSliderValue(backupTriggerDelay)); - useValueChanged( - triggerDelay.value, - (_, __) => ref - .read(backupProvider.notifier) - .configureBackgroundBackup( - triggerDelay: backupDelayToMilliseconds(triggerDelay.value), - onError: onError, - onBatteryInfo: onBatteryInfo, - ), - ); - - return SettingsButtonListTile( - icon: Icons.cloud_sync_rounded, - iconColor: context.primaryColor, - title: 'backup_controller_page_background_is_on'.tr(), - buttonText: 'backup_controller_page_background_turn_off'.tr(), - onButtonTap: () => ref - .read(backupProvider.notifier) - .configureBackgroundBackup(enabled: false, onError: onError, onBatteryInfo: onBatteryInfo), - subtitle: Column( - children: [ - SettingsSwitchListTile( - valueNotifier: isWifiRequiredNotifier, - title: 'backup_controller_page_background_wifi'.tr(), - icon: Icons.wifi, - onChanged: (enabled) => ref - .read(backupProvider.notifier) - .configureBackgroundBackup(requireWifi: enabled, onError: onError, onBatteryInfo: onBatteryInfo), - ), - SettingsSwitchListTile( - valueNotifier: isChargingRequiredNotifier, - title: 'backup_controller_page_background_charging'.tr(), - icon: Icons.charging_station, - onChanged: (enabled) => ref - .read(backupProvider.notifier) - .configureBackgroundBackup(requireCharging: enabled, onError: onError, onBatteryInfo: onBatteryInfo), - ), - if (Platform.isAndroid) - SettingsSliderListTile( - valueNotifier: triggerDelay, - text: 'backup_controller_page_background_delay'.tr( - namedArgs: {'duration': formatBackupDelaySliderValue(triggerDelay.value)}, - ), - maxValue: 3.0, - noDivisons: 3, - label: formatBackupDelaySliderValue(triggerDelay.value), - ), - ], - ), - ); - } -} diff --git a/mobile/lib/widgets/settings/backup_settings/backup_settings.dart b/mobile/lib/widgets/settings/backup_settings/backup_settings.dart deleted file mode 100644 index 50aa57da9f..0000000000 --- a/mobile/lib/widgets/settings/backup_settings/backup_settings.dart +++ /dev/null @@ -1,82 +0,0 @@ -import 'dart:io'; - -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/providers/backup/backup_verification.provider.dart'; -import 'package:immich_mobile/services/app_settings.service.dart'; -import 'package:immich_mobile/services/asset.service.dart'; -import 'package:immich_mobile/widgets/settings/backup_settings/background_settings.dart'; -import 'package:immich_mobile/widgets/settings/backup_settings/foreground_settings.dart'; -import 'package:immich_mobile/widgets/settings/settings_button_list_tile.dart'; -import 'package:immich_mobile/widgets/settings/settings_sub_page_scaffold.dart'; -import 'package:immich_mobile/widgets/settings/settings_switch_list_tile.dart'; -import 'package:immich_mobile/utils/hooks/app_settings_update_hook.dart'; - -class BackupSettings extends HookConsumerWidget { - const BackupSettings({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final ignoreIcloudAssets = useAppSettingsState(AppSettingsEnum.ignoreIcloudAssets); - final isAdvancedTroubleshooting = useAppSettingsState(AppSettingsEnum.advancedTroubleshooting); - final albumSync = useAppSettingsState(AppSettingsEnum.syncAlbums); - final isCorruptCheckInProgress = ref.watch(backupVerificationProvider); - final isAlbumSyncInProgress = useState(false); - - syncAlbums() async { - isAlbumSyncInProgress.value = true; - try { - await ref.read(assetServiceProvider).syncUploadedAssetToAlbums(); - } catch (_) { - } finally { - Future.delayed(const Duration(seconds: 1), () { - isAlbumSyncInProgress.value = false; - }); - } - } - - final backupSettings = [ - const ForegroundBackupSettings(), - const BackgroundBackupSettings(), - if (Platform.isIOS) - SettingsSwitchListTile( - valueNotifier: ignoreIcloudAssets, - title: 'ignore_icloud_photos'.tr(), - subtitle: 'ignore_icloud_photos_description'.tr(), - ), - if (Platform.isAndroid && isAdvancedTroubleshooting.value) - SettingsButtonListTile( - icon: Icons.warning_rounded, - title: 'check_corrupt_asset_backup'.tr(), - subtitle: isCorruptCheckInProgress - ? const Column( - children: [ - SizedBox(height: 20), - Center(child: CircularProgressIndicator()), - SizedBox(height: 20), - ], - ) - : null, - subtileText: !isCorruptCheckInProgress ? 'check_corrupt_asset_backup_description'.tr() : null, - buttonText: 'check_corrupt_asset_backup_button'.tr(), - onButtonTap: !isCorruptCheckInProgress - ? () => ref.read(backupVerificationProvider.notifier).performBackupCheck(context) - : null, - ), - if (albumSync.value) - SettingsButtonListTile( - icon: Icons.photo_album_outlined, - title: 'sync_albums'.tr(), - subtitle: Text("sync_albums_manual_subtitle".tr()), - buttonText: 'sync_albums'.tr(), - child: isAlbumSyncInProgress.value - ? const CircularProgressIndicator() - : ElevatedButton(onPressed: syncAlbums, child: Text('sync'.tr())), - ), - ]; - - return SettingsSubPageScaffold(settings: backupSettings, showDivider: true); - } -} diff --git a/mobile/lib/widgets/settings/backup_settings/foreground_settings.dart b/mobile/lib/widgets/settings/backup_settings/foreground_settings.dart deleted file mode 100644 index a2ff00fe45..0000000000 --- a/mobile/lib/widgets/settings/backup_settings/foreground_settings.dart +++ /dev/null @@ -1,35 +0,0 @@ -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/backup/backup.provider.dart'; -import 'package:immich_mobile/widgets/settings/settings_button_list_tile.dart'; - -class ForegroundBackupSettings extends ConsumerWidget { - const ForegroundBackupSettings({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final isAutoBackup = ref.watch(backupProvider.select((s) => s.autoBackup)); - - void onButtonTap() => ref.read(backupProvider.notifier).setAutoBackup(!isAutoBackup); - - if (isAutoBackup) { - return SettingsButtonListTile( - icon: Icons.cloud_done_rounded, - iconColor: context.primaryColor, - title: 'backup_controller_page_status_on'.tr(), - buttonText: 'backup_controller_page_turn_off'.tr(), - onButtonTap: onButtonTap, - ); - } - - return SettingsButtonListTile( - icon: Icons.cloud_off_rounded, - title: 'backup_controller_page_status_off'.tr(), - subtileText: 'backup_controller_page_desc_backup'.tr(), - buttonText: 'backup_controller_page_turn_on'.tr(), - onButtonTap: onButtonTap, - ); - } -} diff --git a/mobile/lib/widgets/settings/beta_timeline_list_tile.dart b/mobile/lib/widgets/settings/beta_timeline_list_tile.dart deleted file mode 100644 index 21e0edb34c..0000000000 --- a/mobile/lib/widgets/settings/beta_timeline_list_tile.dart +++ /dev/null @@ -1,71 +0,0 @@ -import 'dart:async'; - -import 'package:auto_route/auto_route.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/extensions/translate_extensions.dart'; -import 'package:immich_mobile/providers/app_settings.provider.dart'; -import 'package:immich_mobile/providers/auth.provider.dart'; -import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/services/app_settings.service.dart'; -import 'package:immich_mobile/widgets/settings/setting_list_tile.dart'; - -class BetaTimelineListTile extends ConsumerWidget { - const BetaTimelineListTile({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final betaTimelineValue = ref.watch(appSettingsServiceProvider).getSetting(AppSettingsEnum.betaTimeline); - final auth = ref.watch(authProvider); - - if (!auth.isAuthenticated) { - return const SizedBox.shrink(); - } - - void onSwitchChanged(bool value) { - showDialog( - context: context, - builder: (context) { - return AlertDialog( - title: value ? const Text("Enable New Timeline") : const Text("Disable New Timeline"), - content: value - ? const Text("Are you sure you want to enable the new timeline?") - : const Text("Are you sure you want to disable the new timeline?"), - actions: [ - TextButton( - onPressed: () { - context.pop(); - }, - child: Text( - "cancel".t(context: context), - style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500, color: context.colorScheme.outline), - ), - ), - ElevatedButton( - onPressed: () async { - Navigator.of(context).pop(); - unawaited(context.router.replaceAll([ChangeExperienceRoute(switchingToBeta: value)])); - }, - child: Text("ok".t(context: context)), - ), - ], - ); - }, - ); - } - - return Padding( - padding: const EdgeInsets.only(left: 4.0), - child: SettingListTile( - title: "new_timeline".t(context: context), - trailing: Switch.adaptive( - value: betaTimelineValue, - onChanged: onSwitchChanged, - activeThumbColor: context.primaryColor, - ), - onTap: () => onSwitchChanged(!betaTimelineValue), - ), - ); - } -} diff --git a/mobile/lib/widgets/settings/local_storage_settings.dart b/mobile/lib/widgets/settings/local_storage_settings.dart deleted file mode 100644 index af9e4079bb..0000000000 --- a/mobile/lib/widgets/settings/local_storage_settings.dart +++ /dev/null @@ -1,51 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart' show useEffect, useState; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/duplicated_asset.entity.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/extensions/theme_extensions.dart'; -import 'package:immich_mobile/providers/db.provider.dart'; - -class LocalStorageSettings extends HookConsumerWidget { - const LocalStorageSettings({super.key}); - @override - Widget build(BuildContext context, WidgetRef ref) { - final isarDb = ref.watch(dbProvider); - final cacheItemCount = useState(0); - - useEffect(() { - cacheItemCount.value = isarDb.duplicatedAssets.countSync(); - return null; - }, []); - - void clearCache() async { - await isarDb.writeTxn(() => isarDb.duplicatedAssets.clear()); - cacheItemCount.value = await isarDb.duplicatedAssets.count(); - } - - return ListTile( - contentPadding: const EdgeInsets.symmetric(horizontal: 20), - dense: true, - title: Text( - "cache_settings_duplicated_assets_title", - style: context.textTheme.bodyLarge?.copyWith(fontWeight: FontWeight.w500), - ).tr(namedArgs: {'count': "${cacheItemCount.value}"}), - subtitle: Text( - "cache_settings_duplicated_assets_subtitle", - style: context.textTheme.bodyMedium?.copyWith(color: context.colorScheme.onSurfaceSecondary), - ).tr(), - trailing: TextButton( - onPressed: cacheItemCount.value > 0 ? clearCache : null, - child: Text( - "cache_settings_duplicated_assets_clear_button", - style: TextStyle( - fontSize: 12, - color: cacheItemCount.value > 0 ? Colors.red : Colors.grey, - fontWeight: FontWeight.bold, - ), - ).tr(), - ), - ); - } -} diff --git a/mobile/pubspec.lock b/mobile/pubspec.lock index 9d5f431792..f2caf05be8 100644 --- a/mobile/pubspec.lock +++ b/mobile/pubspec.lock @@ -378,14 +378,6 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.0" - dartx: - dependency: transitive - description: - name: dartx - sha256: "8b25435617027257d43e6508b5fe061012880ddfdaa75a71d607c3de2a13d244" - url: "https://pub.dev" - source: hosted - version: "1.2.0" dbus: dependency: transitive description: @@ -1012,40 +1004,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.5" - isar: - dependency: "direct main" - description: - path: "packages/isar" - ref: bb1dca40fe87a001122e5d43bc6254718cb49f3a - resolved-ref: bb1dca40fe87a001122e5d43bc6254718cb49f3a - url: "https://github.com/immich-app/isar" - source: git - version: "3.1.8" - isar_community: - dependency: transitive - description: - name: isar_community - sha256: "28f59e54636c45ba0bb1b3b7f2656f1c50325f740cea6efcd101900be3fba546" - url: "https://pub.dev" - source: hosted - version: "3.3.0-dev.3" - isar_community_flutter_libs: - dependency: "direct main" - description: - name: isar_community_flutter_libs - sha256: c2934fe755bb3181cb67602fd5df0d080b3d3eb52799f98623aa4fc5acbea010 - url: "https://pub.dev" - source: hosted - version: "3.3.0-dev.3" - isar_generator: - dependency: "direct dev" - description: - path: "packages/isar_generator" - ref: bb1dca40fe87a001122e5d43bc6254718cb49f3a - resolved-ref: bb1dca40fe87a001122e5d43bc6254718cb49f3a - url: "https://github.com/immich-app/isar" - source: git - version: "3.1.8" jni: dependency: transitive description: @@ -1909,14 +1867,6 @@ packages: url: "https://pub.dev" source: hosted version: "0.1.0+1" - time: - dependency: transitive - description: - name: time - sha256: "370572cf5d1e58adcb3e354c47515da3f7469dac3a95b447117e728e7be6f461" - url: "https://pub.dev" - source: hosted - version: "2.1.5" timezone: dependency: "direct main" description: @@ -2173,14 +2123,6 @@ packages: url: "https://pub.dev" source: hosted version: "6.6.1" - xxh3: - dependency: transitive - description: - name: xxh3 - sha256: "399a0438f5d426785723c99da6b16e136f4953fb1e9db0bf270bd41dd4619916" - url: "https://pub.dev" - source: hosted - version: "1.2.0" yaml: dependency: transitive description: diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml index 1e03bd6e7f..5eb4deb924 100644 --- a/mobile/pubspec.yaml +++ b/mobile/pubspec.yaml @@ -43,16 +43,9 @@ dependencies: immich_ui: path: './packages/ui' intl: ^0.20.2 - isar: - git: - url: https://github.com/immich-app/isar - ref: 'bb1dca40fe87a001122e5d43bc6254718cb49f3a' - path: packages/isar/ - isar_community_flutter_libs: 3.3.0-dev.3 local_auth: ^2.3.0 logging: ^1.3.0 maplibre_gl: ^0.22.0 - native_video_player: git: url: https://github.com/immich-app/native_video_player @@ -115,11 +108,6 @@ dev_dependencies: path: './immich_lint' integration_test: sdk: flutter - isar_generator: - git: - url: https://github.com/immich-app/isar - ref: 'bb1dca40fe87a001122e5d43bc6254718cb49f3a' - path: packages/isar_generator/ mocktail: ^1.0.4 # Type safe platform code pigeon: ^26.0.2 diff --git a/mobile/test/api.mocks.dart b/mobile/test/api.mocks.dart index c6a3a90582..e1c32eaaee 100644 --- a/mobile/test/api.mocks.dart +++ b/mobile/test/api.mocks.dart @@ -1,8 +1,6 @@ import 'package:mocktail/mocktail.dart'; import 'package:openapi/api.dart'; -class MockAssetsApi extends Mock implements AssetsApi {} - class MockSyncApi extends Mock implements SyncApi {} class MockServerApi extends Mock implements ServerApi {} diff --git a/mobile/test/domain/service.mock.dart b/mobile/test/domain/service.mock.dart index 56b4802f88..89e85a3794 100644 --- a/mobile/test/domain/service.mock.dart +++ b/mobile/test/domain/service.mock.dart @@ -1,20 +1,13 @@ import 'package:immich_mobile/domain/services/store.service.dart'; -import 'package:immich_mobile/domain/services/user.service.dart'; import 'package:immich_mobile/domain/utils/background_sync.dart'; import 'package:immich_mobile/platform/native_sync_api.g.dart'; import 'package:immich_mobile/services/app_settings.service.dart'; -import 'package:immich_mobile/services/background_upload.service.dart'; import 'package:mocktail/mocktail.dart'; class MockStoreService extends Mock implements StoreService {} -class MockUserService extends Mock implements UserService {} - class MockBackgroundSyncManager extends Mock implements BackgroundSyncManager {} class MockNativeSyncApi extends Mock implements NativeSyncApi {} class MockAppSettingsService extends Mock implements AppSettingsService {} - -class MockBackgroundUploadService extends Mock implements BackgroundUploadService {} - diff --git a/mobile/test/domain/services/album.service_test.dart b/mobile/test/domain/services/album.service_test.dart deleted file mode 100644 index 9110a09471..0000000000 --- a/mobile/test/domain/services/album.service_test.dart +++ /dev/null @@ -1,115 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:immich_mobile/constants/enums.dart'; -import 'package:immich_mobile/domain/models/album/album.model.dart'; -import 'package:immich_mobile/domain/services/remote_album.service.dart'; -import 'package:immich_mobile/infrastructure/repositories/remote_album.repository.dart'; -import 'package:immich_mobile/providers/album/album_sort_by_options.provider.dart'; -import 'package:immich_mobile/repositories/drift_album_api_repository.dart'; -import 'package:mocktail/mocktail.dart'; - -import '../../infrastructure/repository.mock.dart'; - -void main() { - late RemoteAlbumService sut; - late DriftRemoteAlbumRepository mockRemoteAlbumRepo; - late DriftAlbumApiRepository mockAlbumApiRepo; - - final albumA = RemoteAlbum( - id: '1', - name: 'Album A', - description: "", - isActivityEnabled: false, - order: AlbumAssetOrder.asc, - assetCount: 1, - createdAt: DateTime(2023, 1, 1), - updatedAt: DateTime(2023, 1, 2), - ownerId: 'owner1', - ownerName: "Test User", - isShared: false, - ); - - final albumB = RemoteAlbum( - id: '2', - name: 'Album B', - description: "", - isActivityEnabled: false, - order: AlbumAssetOrder.desc, - assetCount: 2, - createdAt: DateTime(2023, 2, 1), - updatedAt: DateTime(2023, 2, 2), - ownerId: 'owner2', - ownerName: "Test User", - isShared: false, - ); - - setUp(() { - mockRemoteAlbumRepo = MockRemoteAlbumRepository(); - mockAlbumApiRepo = MockDriftAlbumApiRepository(); - - when( - () => mockRemoteAlbumRepo.getSortedAlbumIds(any(), aggregation: AssetDateAggregation.end), - ).thenAnswer((_) async => ['1', '2']); - - when( - () => mockRemoteAlbumRepo.getSortedAlbumIds(any(), aggregation: AssetDateAggregation.start), - ).thenAnswer((_) async => ['1', '2']); - - sut = RemoteAlbumService(mockRemoteAlbumRepo, mockAlbumApiRepo); - }); - - group('sortAlbums', () { - test('should sort correctly based on name', () async { - final albums = [albumB, albumA]; - - final result = await sut.sortAlbums(albums, AlbumSortMode.title); - expect(result, [albumA, albumB]); - }); - - test('should sort correctly based on createdAt', () async { - final albums = [albumB, albumA]; - - final result = await sut.sortAlbums(albums, AlbumSortMode.created); - expect(result, [albumB, albumA]); - }); - - test('should sort correctly based on updatedAt', () async { - final albums = [albumB, albumA]; - - final result = await sut.sortAlbums(albums, AlbumSortMode.lastModified); - expect(result, [albumB, albumA]); - }); - - test('should sort correctly based on assetCount', () async { - final albums = [albumB, albumA]; - - final result = await sut.sortAlbums(albums, AlbumSortMode.assetCount); - expect(result, [albumB, albumA]); - }); - - test('should sort correctly based on newestAssetTimestamp', () async { - final albums = [albumB, albumA]; - - final result = await sut.sortAlbums(albums, AlbumSortMode.mostRecent); - expect(result, [albumB, albumA]); - }); - - test('should sort correctly based on oldestAssetTimestamp', () async { - final albums = [albumB, albumA]; - - final result = await sut.sortAlbums(albums, AlbumSortMode.mostOldest); - expect(result, [albumA, albumB]); - }); - - test('should flip order when isReverse is true for all modes', () async { - final albums = [albumB, albumA]; - - for (final mode in AlbumSortMode.values) { - final normal = await sut.sortAlbums(albums, mode, isReverse: false); - final reversed = await sut.sortAlbums(albums, mode, isReverse: true); - - // reversed should be the exact inverse of normal - expect(reversed, normal.reversed.toList(), reason: 'Mode: $mode'); - } - }); - }); -} diff --git a/mobile/test/domain/services/asset.service_test.dart b/mobile/test/domain/services/asset.service_test.dart deleted file mode 100644 index 04e49f89f9..0000000000 --- a/mobile/test/domain/services/asset.service_test.dart +++ /dev/null @@ -1,185 +0,0 @@ -import 'package:flutter/foundation.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:immich_mobile/domain/models/exif.model.dart'; -import 'package:immich_mobile/domain/services/asset.service.dart'; -import 'package:mocktail/mocktail.dart'; - -import '../../infrastructure/repository.mock.dart'; -import '../../test_utils.dart'; - -void main() { - late AssetService sut; - late MockRemoteAssetRepository mockRemoteAssetRepository; - late MockDriftLocalAssetRepository mockLocalAssetRepository; - - setUp(() { - mockRemoteAssetRepository = MockRemoteAssetRepository(); - mockLocalAssetRepository = MockDriftLocalAssetRepository(); - sut = AssetService( - remoteAssetRepository: mockRemoteAssetRepository, - localAssetRepository: mockLocalAssetRepository, - ); - }); - - group('getAspectRatio', () { - test('flips dimensions on Android for 90° and 270° orientations', () async { - debugDefaultTargetPlatformOverride = TargetPlatform.android; - addTearDown(() => debugDefaultTargetPlatformOverride = null); - - for (final orientation in [90, 270]) { - final localAsset = TestUtils.createLocalAsset( - id: 'local-$orientation', - width: 1920, - height: 1080, - orientation: orientation, - ); - - final result = await sut.getAspectRatio(localAsset); - - expect(result, 1080 / 1920, reason: 'Orientation $orientation should flip on Android'); - } - }); - - test('does not flip dimensions on iOS regardless of orientation', () async { - debugDefaultTargetPlatformOverride = TargetPlatform.iOS; - addTearDown(() => debugDefaultTargetPlatformOverride = null); - - for (final orientation in [0, 90, 270]) { - final localAsset = TestUtils.createLocalAsset( - id: 'local-$orientation', - width: 1920, - height: 1080, - orientation: orientation, - ); - - final result = await sut.getAspectRatio(localAsset); - - expect(result, 1920 / 1080, reason: 'iOS should never flip dimensions'); - } - }); - - test('fetches dimensions from remote repository when missing from asset', () async { - final remoteAsset = TestUtils.createRemoteAsset(id: 'remote-1', width: null, height: null); - - final exif = const ExifInfo(orientation: '1'); - - final fetchedAsset = TestUtils.createRemoteAsset(id: 'remote-1', width: 1920, height: 1080); - - when(() => mockRemoteAssetRepository.getExif('remote-1')).thenAnswer((_) async => exif); - when(() => mockRemoteAssetRepository.get('remote-1')).thenAnswer((_) async => fetchedAsset); - - final result = await sut.getAspectRatio(remoteAsset); - - expect(result, 1920 / 1080); - verify(() => mockRemoteAssetRepository.get('remote-1')).called(1); - }); - - test('fetches dimensions from local repository when missing from local asset', () async { - final localAsset = TestUtils.createLocalAsset(id: 'local-1', width: null, height: null, orientation: 0); - - final fetchedAsset = TestUtils.createLocalAsset(id: 'local-1', width: 1920, height: 1080, orientation: 0); - - when(() => mockLocalAssetRepository.get('local-1')).thenAnswer((_) async => fetchedAsset); - - final result = await sut.getAspectRatio(localAsset); - - expect(result, 1920 / 1080); - verify(() => mockLocalAssetRepository.get('local-1')).called(1); - }); - - test('uses fetched asset orientation when dimensions are missing on Android', () async { - debugDefaultTargetPlatformOverride = TargetPlatform.android; - addTearDown(() => debugDefaultTargetPlatformOverride = null); - - // Original asset has default orientation 0, but dimensions are missing - final localAsset = TestUtils.createLocalAsset(id: 'local-1', width: null, height: null, orientation: 0); - - // Fetched asset has 90° orientation and proper dimensions - final fetchedAsset = TestUtils.createLocalAsset(id: 'local-1', width: 1920, height: 1080, orientation: 90); - - when(() => mockLocalAssetRepository.get('local-1')).thenAnswer((_) async => fetchedAsset); - - final result = await sut.getAspectRatio(localAsset); - - // Should flip dimensions since fetched asset has 90° orientation - expect(result, 1080 / 1920); - verify(() => mockLocalAssetRepository.get('local-1')).called(1); - }); - - test('returns 1.0 when dimensions are still unavailable after fetching', () async { - final remoteAsset = TestUtils.createRemoteAsset(id: 'remote-1', width: null, height: null); - - final exif = const ExifInfo(orientation: '1'); - - when(() => mockRemoteAssetRepository.getExif('remote-1')).thenAnswer((_) async => exif); - when(() => mockRemoteAssetRepository.get('remote-1')).thenAnswer((_) async => null); - - final result = await sut.getAspectRatio(remoteAsset); - - expect(result, 1.0); - }); - - test('returns 1.0 when height is zero', () async { - final remoteAsset = TestUtils.createRemoteAsset(id: 'remote-1', width: 1920, height: 0); - - final exif = const ExifInfo(orientation: '1'); - - when(() => mockRemoteAssetRepository.getExif('remote-1')).thenAnswer((_) async => exif); - - final result = await sut.getAspectRatio(remoteAsset); - - expect(result, 1.0); - }); - - test('handles local asset with remoteId using local orientation not remote exif', () async { - // When a LocalAsset has a remoteId (merged), we should use local orientation - // because the width/height come from the local asset (pre-corrected on iOS) - final localAsset = TestUtils.createLocalAsset( - id: 'local-1', - remoteId: 'remote-1', - width: 1920, - height: 1080, - orientation: 0, - ); - - final result = await sut.getAspectRatio(localAsset); - - expect(result, 1920 / 1080); - // Should not call remote exif for LocalAsset - verifyNever(() => mockRemoteAssetRepository.getExif(any())); - }); - - test('handles local asset with remoteId and 90 degree rotation on Android', () async { - debugDefaultTargetPlatformOverride = TargetPlatform.android; - addTearDown(() => debugDefaultTargetPlatformOverride = null); - - final localAsset = TestUtils.createLocalAsset( - id: 'local-1', - remoteId: 'remote-1', - width: 1920, - height: 1080, - orientation: 90, - ); - - final result = await sut.getAspectRatio(localAsset); - - expect(result, 1080 / 1920); - }); - - test('should not flip remote asset dimensions', () async { - final flippedOrientations = ['1', '2', '3', '4', '5', '6', '7', '8', '90', '-90']; - - for (final orientation in flippedOrientations) { - final remoteAsset = TestUtils.createRemoteAsset(id: 'remote-$orientation', width: 1920, height: 1080); - - final exif = ExifInfo(orientation: orientation); - - when(() => mockRemoteAssetRepository.getExif('remote-$orientation')).thenAnswer((_) async => exif); - - final result = await sut.getAspectRatio(remoteAsset); - - expect(result, 1920 / 1080, reason: 'Should not flipped remote asset dimensions for orientation $orientation'); - } - }); - }); -} diff --git a/mobile/test/domain/services/log_service_test.dart b/mobile/test/domain/services/log_service_test.dart index 95f677ba98..0ccef393ab 100644 --- a/mobile/test/domain/services/log_service_test.dart +++ b/mobile/test/domain/services/log_service_test.dart @@ -29,11 +29,11 @@ final _kWarnLog = LogMessage( void main() { late LogService sut; late LogRepository mockLogRepo; - late IsarStoreRepository mockStoreRepo; + late DriftStoreRepository mockStoreRepo; setUp(() async { mockLogRepo = MockLogRepository(); - mockStoreRepo = MockStoreRepository(); + mockStoreRepo = MockDriftStoreRepository(); registerFallbackValue(_kInfoLog); diff --git a/mobile/test/domain/services/store_service_test.dart b/mobile/test/domain/services/store_service_test.dart index 996170b518..8ceb1e3c9c 100644 --- a/mobile/test/domain/services/store_service_test.dart +++ b/mobile/test/domain/services/store_service_test.dart @@ -15,13 +15,11 @@ final _kBackupFailedSince = DateTime.utc(2023); void main() { late StoreService sut; - late IsarStoreRepository mockStoreRepo; late DriftStoreRepository mockDriftStoreRepo; late StreamController>> controller; setUp(() async { controller = StreamController>>.broadcast(); - mockStoreRepo = MockStoreRepository(); mockDriftStoreRepo = MockDriftStoreRepository(); // For generics, we need to provide fallback to each concrete type to avoid runtime errors registerFallbackValue(StoreKey.accessToken); @@ -29,16 +27,6 @@ void main() { registerFallbackValue(StoreKey.backgroundBackup); registerFallbackValue(StoreKey.backupFailedSince); - when(() => mockStoreRepo.getAll()).thenAnswer( - (_) async => [ - const StoreDto(StoreKey.accessToken, _kAccessToken), - const StoreDto(StoreKey.backgroundBackup, _kBackgroundBackup), - const StoreDto(StoreKey.groupAssetsBy, _kGroupAssetsBy), - StoreDto(StoreKey.backupFailedSince, _kBackupFailedSince), - ], - ); - when(() => mockStoreRepo.watchAll()).thenAnswer((_) => controller.stream); - when(() => mockDriftStoreRepo.getAll()).thenAnswer( (_) async => [ const StoreDto(StoreKey.accessToken, _kAccessToken), @@ -49,7 +37,7 @@ void main() { ); when(() => mockDriftStoreRepo.watchAll()).thenAnswer((_) => controller.stream); - sut = await StoreService.create(storeRepository: mockStoreRepo); + sut = await StoreService.create(storeRepository: mockDriftStoreRepo); }); tearDown(() async { @@ -59,7 +47,7 @@ void main() { group("Store Service Init:", () { test('Populates the internal cache on init', () { - verify(() => mockStoreRepo.getAll()).called(1); + verify(() => mockDriftStoreRepo.getAll()).called(1); expect(sut.tryGet(StoreKey.accessToken), _kAccessToken); expect(sut.tryGet(StoreKey.backgroundBackup), _kBackgroundBackup); expect(sut.tryGet(StoreKey.groupAssetsBy), _kGroupAssetsBy); @@ -74,7 +62,7 @@ void main() { await pumpEventQueue(); - verify(() => mockStoreRepo.watchAll()).called(1); + verify(() => mockDriftStoreRepo.watchAll()).called(1); expect(sut.tryGet(StoreKey.accessToken), _kAccessToken.toUpperCase()); }); }); @@ -95,19 +83,18 @@ void main() { group('Store Service put:', () { setUp(() { - when(() => mockStoreRepo.upsert(any>(), any())).thenAnswer((_) async => true); when(() => mockDriftStoreRepo.upsert(any>(), any())).thenAnswer((_) async => true); }); test('Skip insert when value is not modified', () async { await sut.put(StoreKey.accessToken, _kAccessToken); - verifyNever(() => mockStoreRepo.upsert(StoreKey.accessToken, any())); + verifyNever(() => mockDriftStoreRepo.upsert(StoreKey.accessToken, any())); }); test('Insert value when modified', () async { final newAccessToken = _kAccessToken.toUpperCase(); await sut.put(StoreKey.accessToken, newAccessToken); - verify(() => mockStoreRepo.upsert(StoreKey.accessToken, newAccessToken)).called(1); + verify(() => mockDriftStoreRepo.upsert(StoreKey.accessToken, newAccessToken)).called(1); expect(sut.tryGet(StoreKey.accessToken), newAccessToken); }); }); @@ -117,7 +104,6 @@ void main() { setUp(() { valueController = StreamController.broadcast(); - when(() => mockStoreRepo.watch(any>())).thenAnswer((_) => valueController.stream); when(() => mockDriftStoreRepo.watch(any>())).thenAnswer((_) => valueController.stream); }); @@ -136,19 +122,18 @@ void main() { } await pumpEventQueue(); - verify(() => mockStoreRepo.watch(StoreKey.accessToken)).called(1); + verify(() => mockDriftStoreRepo.watch(StoreKey.accessToken)).called(1); }); }); group('Store Service delete:', () { setUp(() { - when(() => mockStoreRepo.delete(any>())).thenAnswer((_) async => true); when(() => mockDriftStoreRepo.delete(any>())).thenAnswer((_) async => true); }); test('Removes the value from the DB', () async { await sut.delete(StoreKey.accessToken); - verify(() => mockStoreRepo.delete(StoreKey.accessToken)).called(1); + verify(() => mockDriftStoreRepo.delete(StoreKey.accessToken)).called(1); }); test('Removes the value from the cache', () async { @@ -159,13 +144,12 @@ void main() { group('Store Service clear:', () { setUp(() { - when(() => mockStoreRepo.deleteAll()).thenAnswer((_) async => true); when(() => mockDriftStoreRepo.deleteAll()).thenAnswer((_) async => true); }); test('Clears all values from the store', () async { await sut.clear(); - verify(() => mockStoreRepo.deleteAll()).called(1); + verify(() => mockDriftStoreRepo.deleteAll()).called(1); expect(sut.tryGet(StoreKey.accessToken), isNull); expect(sut.tryGet(StoreKey.backgroundBackup), isNull); expect(sut.tryGet(StoreKey.groupAssetsBy), isNull); diff --git a/mobile/test/domain/services/user_service_test.dart b/mobile/test/domain/services/user_service_test.dart index 395f38a207..80b6d80457 100644 --- a/mobile/test/domain/services/user_service_test.dart +++ b/mobile/test/domain/services/user_service_test.dart @@ -4,7 +4,6 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/domain/services/store.service.dart'; import 'package:immich_mobile/domain/services/user.service.dart'; -import 'package:immich_mobile/infrastructure/repositories/user.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/user_api.repository.dart'; import 'package:mocktail/mocktail.dart'; @@ -14,19 +13,13 @@ import '../service.mock.dart'; void main() { late UserService sut; - late IsarUserRepository mockUserRepo; late UserApiRepository mockUserApiRepo; late StoreService mockStoreService; setUp(() { - mockUserRepo = MockIsarUserRepository(); mockUserApiRepo = MockUserApiRepository(); mockStoreService = MockStoreService(); - sut = UserService( - isarUserRepository: mockUserRepo, - userApiRepository: mockUserApiRepo, - storeService: mockStoreService, - ); + sut = UserService(userApiRepository: mockUserApiRepo, storeService: mockStoreService); registerFallbackValue(UserStub.admin); when(() => mockStoreService.get(StoreKey.currentUser)).thenReturn(UserStub.admin); @@ -77,11 +70,9 @@ void main() { test('should return user from api and store it', () async { when(() => mockUserApiRepo.getMyUser()).thenAnswer((_) async => UserStub.admin); when(() => mockStoreService.put(StoreKey.currentUser, UserStub.admin)).thenAnswer((_) async => true); - when(() => mockUserRepo.update(UserStub.admin)).thenAnswer((_) async => UserStub.admin); final result = await sut.refreshMyUser(); verify(() => mockStoreService.put(StoreKey.currentUser, UserStub.admin)).called(1); - verify(() => mockUserRepo.update(UserStub.admin)).called(1); expect(result, UserStub.admin); }); @@ -90,7 +81,6 @@ void main() { final result = await sut.refreshMyUser(); verifyNever(() => mockStoreService.put(StoreKey.currentUser, UserStub.admin)); - verifyNever(() => mockUserRepo.update(UserStub.admin)); expect(result, isNull); }); }); @@ -104,12 +94,10 @@ void main() { () => mockUserApiRepo.createProfileImage(name: profileImagePath, data: Uint8List(0)), ).thenAnswer((_) async => profileImagePath); when(() => mockStoreService.put(StoreKey.currentUser, updatedUser)).thenAnswer((_) async => true); - when(() => mockUserRepo.update(updatedUser)).thenAnswer((_) async => UserStub.admin); final result = await sut.createProfileImage(profileImagePath, Uint8List(0)); verify(() => mockStoreService.put(StoreKey.currentUser, updatedUser)).called(1); - verify(() => mockUserRepo.update(updatedUser)).called(1); expect(result, profileImagePath); }); @@ -123,7 +111,6 @@ void main() { final result = await sut.createProfileImage(profileImagePath, Uint8List(0)); verifyNever(() => mockStoreService.put(StoreKey.currentUser, updatedUser)); - verifyNever(() => mockUserRepo.update(updatedUser)); expect(result, isNull); }); }); diff --git a/mobile/test/dto.mocks.dart b/mobile/test/dto.mocks.dart deleted file mode 100644 index ed53fcdc90..0000000000 --- a/mobile/test/dto.mocks.dart +++ /dev/null @@ -1,6 +0,0 @@ -import 'package:mocktail/mocktail.dart'; -import 'package:openapi/api.dart'; - -class MockSmartSearchDto extends Mock implements SmartSearchDto {} - -class MockMetadataSearchDto extends Mock implements MetadataSearchDto {} diff --git a/mobile/test/fixtures/album.stub.dart b/mobile/test/fixtures/album.stub.dart index a22a4b72ab..5141540a25 100644 --- a/mobile/test/fixtures/album.stub.dart +++ b/mobile/test/fixtures/album.stub.dart @@ -1,108 +1,4 @@ import 'package:immich_mobile/domain/models/album/local_album.model.dart'; -import 'package:immich_mobile/entities/album.entity.dart'; -import 'package:immich_mobile/infrastructure/entities/user.entity.dart'; - -import 'asset.stub.dart'; -import 'user.stub.dart'; - -final class AlbumStub { - const AlbumStub._(); - - static final emptyAlbum = Album( - name: "empty-album", - localId: "empty-album-local", - remoteId: "empty-album-remote", - createdAt: DateTime(2000), - modifiedAt: DateTime(2023), - shared: false, - activityEnabled: false, - startDate: DateTime(2020), - ); - - static final sharedWithUser = Album( - name: "empty-album-shared-with-user", - localId: "empty-album-shared-with-user-local", - remoteId: "empty-album-shared-with-user-remote", - createdAt: DateTime(2023), - modifiedAt: DateTime(2023), - shared: true, - activityEnabled: false, - endDate: DateTime(2020), - )..sharedUsers.addAll([User.fromDto(UserStub.admin)]); - - static final oneAsset = Album( - name: "album-with-single-asset", - localId: "album-with-single-asset-local", - remoteId: "album-with-single-asset-remote", - createdAt: DateTime(2022), - modifiedAt: DateTime(2023), - shared: false, - activityEnabled: false, - startDate: DateTime(2020), - endDate: DateTime(2023), - )..assets.addAll([AssetStub.image1]); - - static final twoAsset = - Album( - name: "album-with-two-assets", - localId: "album-with-two-assets-local", - remoteId: "album-with-two-assets-remote", - createdAt: DateTime(2001), - modifiedAt: DateTime(2010), - shared: false, - activityEnabled: false, - startDate: DateTime(2019), - endDate: DateTime(2020), - ) - ..assets.addAll([AssetStub.image1, AssetStub.image2]) - ..activityEnabled = true - ..owner.value = User.fromDto(UserStub.admin); - - static final create2020end2020Album = Album( - name: "create2020update2020Album", - localId: "create2020update2020Album-local", - remoteId: "create2020update2020Album-remote", - createdAt: DateTime(2020), - modifiedAt: DateTime(2020), - shared: false, - activityEnabled: false, - startDate: DateTime(2020), - endDate: DateTime(2020), - ); - static final create2020end2022Album = Album( - name: "create2020update2021Album", - localId: "create2020update2021Album-local", - remoteId: "create2020update2021Album-remote", - createdAt: DateTime(2020), - modifiedAt: DateTime(2022), - shared: false, - activityEnabled: false, - startDate: DateTime(2020), - endDate: DateTime(2022), - ); - static final create2020end2024Album = Album( - name: "create2020update2022Album", - localId: "create2020update2022Album-local", - remoteId: "create2020update2022Album-remote", - createdAt: DateTime(2020), - modifiedAt: DateTime(2024), - shared: false, - activityEnabled: false, - startDate: DateTime(2020), - endDate: DateTime(2024), - ); - static final create2020end2026Album = Album( - name: "create2020update2023Album", - localId: "create2020update2023Album-local", - remoteId: "create2020update2023Album-remote", - createdAt: DateTime(2020), - modifiedAt: DateTime(2026), - shared: false, - activityEnabled: false, - startDate: DateTime(2020), - endDate: DateTime(2026), - ); -} abstract final class LocalAlbumStub { const LocalAlbumStub._(); diff --git a/mobile/test/fixtures/asset.stub.dart b/mobile/test/fixtures/asset.stub.dart index 90a7f11737..473b900271 100644 --- a/mobile/test/fixtures/asset.stub.dart +++ b/mobile/test/fixtures/asset.stub.dart @@ -1,59 +1,4 @@ import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; -import 'package:immich_mobile/domain/models/exif.model.dart'; -import 'package:immich_mobile/entities/asset.entity.dart' as old; - -final class AssetStub { - const AssetStub._(); - - static final image1 = old.Asset( - checksum: "image1-checksum", - localId: "image1", - remoteId: 'image1-remote', - ownerId: 1, - fileCreatedAt: DateTime(2019), - fileModifiedAt: DateTime(2020), - updatedAt: DateTime.now(), - durationInSeconds: 0, - type: old.AssetType.image, - fileName: "image1.jpg", - isFavorite: true, - isArchived: false, - isTrashed: false, - exifInfo: const ExifInfo(isFlipped: false), - ); - - static final image2 = old.Asset( - checksum: "image2-checksum", - localId: "image2", - remoteId: 'image2-remote', - ownerId: 1, - fileCreatedAt: DateTime(2000), - fileModifiedAt: DateTime(2010), - updatedAt: DateTime.now(), - durationInSeconds: 60, - type: old.AssetType.video, - fileName: "image2.jpg", - isFavorite: false, - isArchived: false, - isTrashed: false, - exifInfo: const ExifInfo(isFlipped: true), - ); - - static final image3 = old.Asset( - checksum: "image3-checksum", - localId: "image3", - ownerId: 1, - fileCreatedAt: DateTime(2025), - fileModifiedAt: DateTime(2025), - updatedAt: DateTime.now(), - durationInSeconds: 60, - type: old.AssetType.image, - fileName: "image3.jpg", - isFavorite: true, - isArchived: false, - isTrashed: false, - ); -} abstract final class LocalAssetStub { const LocalAssetStub._(); diff --git a/mobile/test/fixtures/exif.stub.dart b/mobile/test/fixtures/exif.stub.dart deleted file mode 100644 index 5ad9a41761..0000000000 --- a/mobile/test/fixtures/exif.stub.dart +++ /dev/null @@ -1,18 +0,0 @@ -import 'package:immich_mobile/domain/models/exif.model.dart'; - -abstract final class ExifStub { - static final size = const ExifInfo(assetId: 1, fileSize: 1000); - - static final gps = const ExifInfo( - assetId: 2, - latitude: 20, - longitude: 20, - city: 'city', - state: 'state', - country: 'country', - ); - - static final rotated90CW = const ExifInfo(assetId: 3, orientation: "90"); - - static final rotated270CW = const ExifInfo(assetId: 4, orientation: "-90"); -} diff --git a/mobile/test/fixtures/user.stub.dart b/mobile/test/fixtures/user.stub.dart index 2ba7177f89..b92ba71e5b 100644 --- a/mobile/test/fixtures/user.stub.dart +++ b/mobile/test/fixtures/user.stub.dart @@ -12,24 +12,4 @@ abstract final class UserStub { profileChangedAt: DateTime(2021), avatarColor: AvatarColor.green, ); - - static final user1 = UserDto( - id: "user1", - email: "user1@test.com", - name: "user1", - isAdmin: false, - updatedAt: DateTime(2022), - profileChangedAt: DateTime(2022), - avatarColor: AvatarColor.red, - ); - - static final user2 = UserDto( - id: "user2", - email: "user2@test.com", - name: "user2", - isAdmin: false, - updatedAt: DateTime(2023), - profileChangedAt: DateTime(2023), - avatarColor: AvatarColor.primary, - ); } diff --git a/mobile/test/infrastructure/repositories/exif_repository_test.dart b/mobile/test/infrastructure/repositories/exif_repository_test.dart deleted file mode 100644 index 4e7ee4d79d..0000000000 --- a/mobile/test/infrastructure/repositories/exif_repository_test.dart +++ /dev/null @@ -1,49 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:immich_mobile/infrastructure/entities/exif.entity.dart'; -import 'package:immich_mobile/infrastructure/repositories/exif.repository.dart'; -import 'package:isar/isar.dart'; - -import '../../fixtures/exif.stub.dart'; -import '../../test_utils.dart'; - -Future _populateExifTable(Isar db) async { - await db.writeTxn(() async { - await db.exifInfos.putAll([ - ExifInfo.fromDto(ExifStub.size), - ExifInfo.fromDto(ExifStub.gps), - ExifInfo.fromDto(ExifStub.rotated90CW), - ExifInfo.fromDto(ExifStub.rotated270CW), - ]); - }); -} - -void main() { - late Isar db; - late IsarExifRepository sut; - - setUp(() async { - db = await TestUtils.initIsar(); - sut = IsarExifRepository(db); - }); - - group("Return with proper orientation", () { - setUp(() async { - await _populateExifTable(db); - }); - - test("isFlipped true for 90CW", () async { - final exif = await sut.get(ExifStub.rotated90CW.assetId!); - expect(exif!.isFlipped, true); - }); - - test("isFlipped true for 270CW", () async { - final exif = await sut.get(ExifStub.rotated270CW.assetId!); - expect(exif!.isFlipped, true); - }); - - test("isFlipped false for the original non-rotated image", () async { - final exif = await sut.get(ExifStub.size.assetId!); - expect(exif!.isFlipped, false); - }); - }); -} diff --git a/mobile/test/infrastructure/repositories/store_repository_test.dart b/mobile/test/infrastructure/repositories/store_repository_test.dart index 18d41e32e0..4cf1adc6b1 100644 --- a/mobile/test/infrastructure/repositories/store_repository_test.dart +++ b/mobile/test/infrastructure/repositories/store_repository_test.dart @@ -1,14 +1,15 @@ import 'dart:async'; +import 'package:drift/drift.dart' hide isNull; +import 'package:drift/native.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/domain/models/user.model.dart'; -import 'package:immich_mobile/infrastructure/entities/store.entity.dart'; +import 'package:immich_mobile/infrastructure/entities/store.entity.drift.dart'; +import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/store.repository.dart'; -import 'package:isar/isar.dart'; import '../../fixtures/user.stub.dart'; -import '../../test_utils.dart'; const _kTestAccessToken = "#TestToken"; final _kTestBackupFailed = DateTime(2025, 2, 20, 11, 45); @@ -16,30 +17,54 @@ const _kTestVersion = 10; const _kTestColorfulInterface = false; final _kTestUser = UserStub.admin; -Future _addIntStoreValue(Isar db, StoreKey key, int? value) async { - await db.storeValues.put(StoreValue(key.id, intValue: value, strValue: null)); -} - -Future _addStrStoreValue(Isar db, StoreKey key, String? value) async { - await db.storeValues.put(StoreValue(key.id, intValue: null, strValue: value)); -} - -Future _populateStore(Isar db) async { - await db.writeTxn(() async { - await _addIntStoreValue(db, StoreKey.colorfulInterface, _kTestColorfulInterface ? 1 : 0); - await _addIntStoreValue(db, StoreKey.backupFailedSince, _kTestBackupFailed.millisecondsSinceEpoch); - await _addStrStoreValue(db, StoreKey.accessToken, _kTestAccessToken); - await _addIntStoreValue(db, StoreKey.version, _kTestVersion); +Future _populateStore(Drift db) async { + await db.batch((batch) async { + batch.insert( + db.storeEntity, + StoreEntityCompanion( + id: Value(StoreKey.colorfulInterface.id), + intValue: const Value(_kTestColorfulInterface ? 1 : 0), + stringValue: const Value(null), + ), + ); + batch.insert( + db.storeEntity, + StoreEntityCompanion( + id: Value(StoreKey.backupFailedSince.id), + intValue: Value(_kTestBackupFailed.millisecondsSinceEpoch), + stringValue: const Value(null), + ), + ); + batch.insert( + db.storeEntity, + StoreEntityCompanion( + id: Value(StoreKey.accessToken.id), + intValue: const Value(null), + stringValue: const Value(_kTestAccessToken), + ), + ); + batch.insert( + db.storeEntity, + StoreEntityCompanion( + id: Value(StoreKey.version.id), + intValue: const Value(_kTestVersion), + stringValue: const Value(null), + ), + ); }); } void main() { - late Isar db; - late IsarStoreRepository sut; + late Drift db; + late DriftStoreRepository sut; setUp(() async { - db = await TestUtils.initIsar(); - sut = IsarStoreRepository(db); + db = Drift(DatabaseConnection(NativeDatabase.memory(), closeStreamsSynchronously: true)); + sut = DriftStoreRepository(db); + }); + + tearDown(() async { + await db.close(); }); group('Store Repository converters:', () { @@ -98,10 +123,10 @@ void main() { }); test('deleteAll()', () async { - final count = await db.storeValues.count(); + final count = await db.storeEntity.count().getSingle(); expect(count, isNot(isZero)); await sut.deleteAll(); - unawaited(expectLater(await db.storeValues.count(), isZero)); + unawaited(expectLater(await db.storeEntity.count().getSingle(), isZero)); }); }); diff --git a/mobile/test/infrastructure/repositories/sync_api_repository_test.dart b/mobile/test/infrastructure/repositories/sync_api_repository_test.dart index 85eebacb14..d538b567bd 100644 --- a/mobile/test/infrastructure/repositories/sync_api_repository_test.dart +++ b/mobile/test/infrastructure/repositories/sync_api_repository_test.dart @@ -1,10 +1,13 @@ import 'dart:async'; import 'dart:convert'; +import 'package:drift/drift.dart'; +import 'package:drift/native.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:http/http.dart' as http; import 'package:immich_mobile/domain/models/sync_event.model.dart'; import 'package:immich_mobile/domain/services/store.service.dart'; +import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/store.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/sync_api.repository.dart'; import 'package:immich_mobile/utils/semver.dart'; @@ -13,7 +16,6 @@ import 'package:openapi/api.dart'; import '../../api.mocks.dart'; import '../../service.mocks.dart'; -import '../../test_utils.dart'; class MockHttpClient extends Mock implements http.Client {} @@ -38,7 +40,8 @@ void main() { late int testBatchSize = 3; setUpAll(() async { - await StoreService.init(storeRepository: IsarStoreRepository(await TestUtils.initIsar())); + final db = Drift(DatabaseConnection(NativeDatabase.memory(), closeStreamsSynchronously: true)); + await StoreService.init(storeRepository: DriftStoreRepository(db)); }); setUp(() { @@ -137,7 +140,7 @@ void main() { bool abortWasCalledInCallback = false; final Completer firstBatchReceived = Completer(); - Future onDataCallback(List events, Function() abort, Function() _) async { + Future onDataCallback(List _, Function() abort, Function() _) async { onDataCallCount++; if (onDataCallCount == 1) { abort(); @@ -241,7 +244,7 @@ void main() { final streamError = Exception("Network Error"); int onDataCallCount = 0; - Future onDataCallback(List events, Function() _, Function() __) async { + Future onDataCallback(List _, Function() _, Function() __) async { onDataCallCount++; } @@ -267,7 +270,7 @@ void main() { when(() => mockStreamedResponse.stream).thenAnswer((_) => http.ByteStream(errorBodyController.stream)); int onDataCallCount = 0; - Future onDataCallback(List events, Function() _, Function() __) async { + Future onDataCallback(List _, Function() _, Function() __) async { onDataCallCount++; } diff --git a/mobile/test/infrastructure/repository.mock.dart b/mobile/test/infrastructure/repository.mock.dart index 2d4af5b308..b7992c1822 100644 --- a/mobile/test/infrastructure/repository.mock.dart +++ b/mobile/test/infrastructure/repository.mock.dart @@ -1,5 +1,4 @@ import 'package:immich_mobile/infrastructure/repositories/backup.repository.dart'; -import 'package:immich_mobile/infrastructure/repositories/device_asset.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/local_album.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/local_asset.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/log.repository.dart'; @@ -11,22 +10,15 @@ import 'package:immich_mobile/infrastructure/repositories/sync_api.repository.da import 'package:immich_mobile/infrastructure/repositories/sync_migration.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/sync_stream.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/trashed_local_asset.repository.dart'; -import 'package:immich_mobile/infrastructure/repositories/user.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/user_api.repository.dart'; import 'package:immich_mobile/repositories/drift_album_api_repository.dart'; import 'package:immich_mobile/repositories/upload.repository.dart'; import 'package:mocktail/mocktail.dart'; -class MockStoreRepository extends Mock implements IsarStoreRepository {} - class MockDriftStoreRepository extends Mock implements DriftStoreRepository {} class MockLogRepository extends Mock implements LogRepository {} -class MockIsarUserRepository extends Mock implements IsarUserRepository {} - -class MockDeviceAssetRepository extends Mock implements IsarDeviceAssetRepository {} - class MockSyncStreamRepository extends Mock implements SyncStreamRepository {} class MockLocalAlbumRepository extends Mock implements DriftLocalAlbumRepository {} diff --git a/mobile/test/modules/activity/activities_page_test.dart b/mobile/test/modules/activity/activities_page_test.dart deleted file mode 100644 index 39350530ea..0000000000 --- a/mobile/test/modules/activity/activities_page_test.dart +++ /dev/null @@ -1,175 +0,0 @@ -@Skip('currently failing due to mock HTTP client to download ISAR binaries') -@Tags(['widget']) -library; - -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:immich_mobile/domain/models/store.model.dart'; -import 'package:immich_mobile/domain/services/store.service.dart'; -import 'package:immich_mobile/entities/album.entity.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/entities/store.entity.dart'; -import 'package:immich_mobile/infrastructure/entities/user.entity.dart'; -import 'package:immich_mobile/infrastructure/repositories/store.repository.dart'; -import 'package:immich_mobile/models/activities/activity.model.dart'; -import 'package:immich_mobile/pages/common/activities.page.dart'; -import 'package:immich_mobile/providers/activity.provider.dart'; -import 'package:immich_mobile/providers/album/current_album.provider.dart'; -import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart'; -import 'package:immich_mobile/providers/user.provider.dart'; -import 'package:immich_mobile/widgets/activities/activity_text_field.dart'; -import 'package:immich_mobile/widgets/activities/dismissible_activity.dart'; -import 'package:isar/isar.dart'; -import 'package:mocktail/mocktail.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; - -import '../../fixtures/album.stub.dart'; -import '../../fixtures/asset.stub.dart'; -import '../../fixtures/user.stub.dart'; -import '../../test_utils.dart'; -import '../../widget_tester_extensions.dart'; -import '../album/album_mocks.dart'; -import '../asset_viewer/asset_viewer_mocks.dart'; -import '../shared/shared_mocks.dart'; -import 'activity_mocks.dart'; - -final _activities = [ - Activity( - id: '1', - createdAt: DateTime(100), - type: ActivityType.comment, - comment: 'First Activity', - assetId: 'asset-2', - user: UserStub.admin, - ), - Activity( - id: '2', - createdAt: DateTime(200), - type: ActivityType.comment, - comment: 'Second Activity', - user: UserStub.user1, - ), - Activity(id: '3', createdAt: DateTime(300), type: ActivityType.like, assetId: 'asset-1', user: UserStub.user2), - Activity(id: '4', createdAt: DateTime(400), type: ActivityType.like, user: UserStub.user1), -]; - -void main() { - late MockAlbumActivity activityMock; - late MockCurrentAlbumProvider mockCurrentAlbumProvider; - late MockCurrentAssetProvider mockCurrentAssetProvider; - late List overrides; - late Isar db; - - setUpAll(() async { - TestUtils.init(); - db = await TestUtils.initIsar(); - await StoreService.init(storeRepository: IsarStoreRepository(db)); - await Store.put(StoreKey.currentUser, UserStub.admin); - await Store.put(StoreKey.serverEndpoint, ''); - await Store.put(StoreKey.accessToken, ''); - }); - - setUp(() async { - mockCurrentAlbumProvider = MockCurrentAlbumProvider(AlbumStub.twoAsset); - mockCurrentAssetProvider = MockCurrentAssetProvider(AssetStub.image1); - activityMock = MockAlbumActivity(_activities); - overrides = [ - albumActivityProvider(AlbumStub.twoAsset.remoteId!, AssetStub.image1.remoteId!).overrideWith(() => activityMock), - currentAlbumProvider.overrideWith(() => mockCurrentAlbumProvider), - currentAssetProvider.overrideWith(() => mockCurrentAssetProvider), - ]; - - await db.writeTxn(() async { - await db.clear(); - // Save all assets - await db.users.put(User.fromDto(UserStub.admin)); - await db.assets.putAll([AssetStub.image1, AssetStub.image2]); - await db.albums.put(AlbumStub.twoAsset); - await AlbumStub.twoAsset.owner.save(); - await AlbumStub.twoAsset.assets.save(); - }); - expect(db.albums.countSync(), 1); - expect(db.assets.countSync(), 2); - expect(db.users.countSync(), 1); - }); - - group("App bar", () { - testWidgets("No title when currentAsset != null", (tester) async { - await tester.pumpConsumerWidget(const ActivitiesPage(), overrides: overrides); - - final listTile = tester.widget(find.byType(AppBar)); - expect(listTile.title, isNull); - }); - - testWidgets("Album name as title when currentAsset == null", (tester) async { - await tester.pumpConsumerWidget(const ActivitiesPage(), overrides: overrides); - await tester.pumpAndSettle(); - - mockCurrentAssetProvider.state = null; - await tester.pumpAndSettle(); - - expect(find.text(AlbumStub.twoAsset.name), findsOneWidget); - final listTile = tester.widget(find.byType(AppBar)); - expect(listTile.title, isNotNull); - }); - }); - - group("Body", () { - testWidgets("Contains a stack with Activity List and Activity Input", (tester) async { - await tester.pumpConsumerWidget(const ActivitiesPage(), overrides: overrides); - await tester.pumpAndSettle(); - - expect(find.descendant(of: find.byType(Stack), matching: find.byType(ActivityTextField)), findsOneWidget); - - expect(find.descendant(of: find.byType(Stack), matching: find.byType(ListView)), findsOneWidget); - }); - - testWidgets("List Contains all dismissible activities", (tester) async { - await tester.pumpConsumerWidget(const ActivitiesPage(), overrides: overrides); - await tester.pumpAndSettle(); - - final listFinder = find.descendant(of: find.byType(Stack), matching: find.byType(ListView)); - final listChildren = find.descendant(of: listFinder, matching: find.byType(DismissibleActivity)); - expect(listChildren, findsNWidgets(_activities.length)); - }); - - testWidgets("Submitting text input adds a comment with the text", (tester) async { - await tester.pumpConsumerWidget(const ActivitiesPage(), overrides: overrides); - await tester.pumpAndSettle(); - - when(() => activityMock.addComment(any())).thenAnswer((_) => Future.value()); - - final textField = find.byType(TextField); - await tester.enterText(textField, 'Test comment'); - await tester.testTextInput.receiveAction(TextInputAction.done); - - verify(() => activityMock.addComment('Test comment')); - }); - - testWidgets("Owner can remove all activities", (tester) async { - await tester.pumpConsumerWidget(const ActivitiesPage(), overrides: overrides); - await tester.pumpAndSettle(); - - final deletableActivityFinder = find.byWidgetPredicate( - (widget) => widget is DismissibleActivity && widget.onDismiss != null, - ); - expect(deletableActivityFinder, findsNWidgets(_activities.length)); - }); - - testWidgets("Non-Owner can remove only their activities", (tester) async { - final mockCurrentUser = MockCurrentUserProvider(); - - await tester.pumpConsumerWidget( - const ActivitiesPage(), - overrides: [...overrides, currentUserProvider.overrideWith((ref) => mockCurrentUser)], - ); - mockCurrentUser.state = UserStub.user1; - await tester.pumpAndSettle(); - - final deletableActivityFinder = find.byWidgetPredicate( - (widget) => widget is DismissibleActivity && widget.onDismiss != null, - ); - expect(deletableActivityFinder, findsNWidgets(_activities.where((a) => a.user == UserStub.user1).length)); - }); - }); -} diff --git a/mobile/test/modules/activity/activity_mocks.dart b/mobile/test/modules/activity/activity_mocks.dart deleted file mode 100644 index c50810795e..0000000000 --- a/mobile/test/modules/activity/activity_mocks.dart +++ /dev/null @@ -1,19 +0,0 @@ -import 'package:immich_mobile/models/activities/activity.model.dart'; -import 'package:immich_mobile/providers/activity.provider.dart'; -import 'package:immich_mobile/providers/activity_statistics.provider.dart'; -import 'package:immich_mobile/services/activity.service.dart'; -import 'package:mocktail/mocktail.dart'; - -class ActivityServiceMock extends Mock implements ActivityService {} - -class MockAlbumActivity extends AlbumActivityInternal with Mock implements AlbumActivity { - List? initActivities; - MockAlbumActivity([this.initActivities]); - - @override - Future> build(String albumId, [String? assetId]) async { - return initActivities ?? []; - } -} - -class ActivityStatisticsMock extends ActivityStatisticsInternal with Mock implements ActivityStatistics {} diff --git a/mobile/test/modules/activity/activity_provider_test.dart b/mobile/test/modules/activity/activity_provider_test.dart deleted file mode 100644 index 84eba62b70..0000000000 --- a/mobile/test/modules/activity/activity_provider_test.dart +++ /dev/null @@ -1,331 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/models/activities/activity.model.dart'; -import 'package:immich_mobile/providers/activity.provider.dart'; -import 'package:immich_mobile/providers/activity_service.provider.dart'; -import 'package:immich_mobile/providers/activity_statistics.provider.dart'; -import 'package:mocktail/mocktail.dart'; - -import '../../fixtures/user.stub.dart'; -import '../../test_utils.dart'; -import 'activity_mocks.dart'; - -final _activities = [ - Activity( - id: '1', - createdAt: DateTime(100), - type: ActivityType.comment, - comment: 'First Activity', - assetId: 'asset-2', - user: UserStub.admin, - ), - Activity( - id: '2', - createdAt: DateTime(200), - type: ActivityType.comment, - comment: 'Second Activity', - user: UserStub.user1, - ), - Activity(id: '3', createdAt: DateTime(300), type: ActivityType.like, assetId: 'asset-1', user: UserStub.admin), - Activity(id: '4', createdAt: DateTime(400), type: ActivityType.like, user: UserStub.user1), -]; - -void main() { - late ActivityServiceMock activityMock; - late ActivityStatisticsMock activityStatisticsMock; - late ActivityStatisticsMock albumActivityStatisticsMock; - late ProviderContainer container; - late AlbumActivityProvider provider; - late ListenerMock>> listener; - - setUpAll(() { - registerFallbackValue(AsyncData>([..._activities])); - }); - - setUp(() async { - activityMock = ActivityServiceMock(); - activityStatisticsMock = ActivityStatisticsMock(); - albumActivityStatisticsMock = ActivityStatisticsMock(); - - container = TestUtils.createContainer( - overrides: [ - activityServiceProvider.overrideWith((ref) => activityMock), - activityStatisticsProvider('test-album', 'test-asset').overrideWith(() => activityStatisticsMock), - activityStatisticsProvider('test-album').overrideWith(() => albumActivityStatisticsMock), - ], - ); - - // Mock values - when(() => activityStatisticsMock.build(any(), any())).thenReturn(0); - when(() => albumActivityStatisticsMock.build(any())).thenReturn(0); - when( - () => activityMock.getAllActivities('test-album', assetId: 'test-asset'), - ).thenAnswer((_) async => [..._activities]); - when(() => activityMock.getAllActivities('test-album')).thenAnswer((_) async => [..._activities]); - - // Init and wait for providers future to complete - provider = albumActivityProvider('test-album', 'test-asset'); - listener = ListenerMock(); - container.listen(provider, listener.call, fireImmediately: true); - - await container.read(provider.future); - }); - - test('Returns a list of activity', () async { - verifyInOrder([ - () => listener.call(null, const AsyncLoading()), - () => listener.call( - const AsyncLoading(), - any( - that: allOf([ - isA>>(), - predicate((AsyncData> ad) => ad.requireValue.every((e) => _activities.contains(e))), - ]), - ), - ), - ]); - - verifyNoMoreInteractions(listener); - }); - - group('addLike()', () { - test('Like successfully added', () async { - final like = Activity(id: '5', createdAt: DateTime(2023), type: ActivityType.like, user: UserStub.admin); - - when( - () => activityMock.addActivity('test-album', ActivityType.like, assetId: 'test-asset'), - ).thenAnswer((_) async => AsyncData(like)); - - final albumProvider = albumActivityProvider('test-album'); - container.read(albumProvider.notifier); - await container.read(albumProvider.future); - - await container.read(provider.notifier).addLike(); - - verify(() => activityMock.addActivity('test-album', ActivityType.like, assetId: 'test-asset')); - - final activities = await container.read(provider.future); - expect(activities, hasLength(5)); - expect(activities, contains(like)); - - // Never bump activity count for new likes - verifyNever(() => activityStatisticsMock.addActivity()); - verifyNever(() => albumActivityStatisticsMock.addActivity()); - - final albumActivities = container.read(albumProvider).requireValue; - expect(albumActivities, hasLength(5)); - expect(albumActivities, contains(like)); - }); - - test('Like failed', () async { - final like = Activity(id: '5', createdAt: DateTime(2023), type: ActivityType.like, user: UserStub.admin); - when( - () => activityMock.addActivity('test-album', ActivityType.like, assetId: 'test-asset'), - ).thenAnswer((_) async => AsyncError(Exception('Mock'), StackTrace.current)); - - final albumProvider = albumActivityProvider('test-album'); - container.read(albumProvider.notifier); - await container.read(albumProvider.future); - - await container.read(provider.notifier).addLike(); - - verify(() => activityMock.addActivity('test-album', ActivityType.like, assetId: 'test-asset')); - - final activities = await container.read(provider.future); - expect(activities, hasLength(4)); - expect(activities, isNot(contains(like))); - - verifyNever(() => albumActivityStatisticsMock.addActivity()); - - final albumActivities = container.read(albumProvider).requireValue; - expect(albumActivities, hasLength(4)); - expect(albumActivities, isNot(contains(like))); - }); - }); - - group('removeActivity()', () { - test('Like successfully removed', () async { - when(() => activityMock.removeActivity('3')).thenAnswer((_) async => true); - - await container.read(provider.notifier).removeActivity('3'); - - verify(() => activityMock.removeActivity('3')); - - final activities = await container.read(provider.future); - expect(activities, hasLength(3)); - expect(activities, isNot(anyElement(predicate((Activity a) => a.id == '3')))); - - verifyNever(() => activityStatisticsMock.removeActivity()); - verifyNever(() => albumActivityStatisticsMock.removeActivity()); - }); - - test('Remove Like failed', () async { - when(() => activityMock.removeActivity('3')).thenAnswer((_) async => false); - - await container.read(provider.notifier).removeActivity('3'); - - final activities = await container.read(provider.future); - expect(activities, hasLength(4)); - expect(activities, anyElement(predicate((Activity a) => a.id == '3'))); - - verifyNever(() => activityStatisticsMock.removeActivity()); - verifyNever(() => albumActivityStatisticsMock.removeActivity()); - }); - - test('Comment successfully removed', () async { - when(() => activityMock.removeActivity('1')).thenAnswer((_) async => true); - - await container.read(provider.notifier).removeActivity('1'); - - final activities = await container.read(provider.future); - expect(activities, isNot(anyElement(predicate((Activity a) => a.id == '1')))); - - verify(() => activityStatisticsMock.removeActivity()); - verify(() => albumActivityStatisticsMock.removeActivity()); - }); - - test('Removes activity from album state when asset scoped', () async { - when(() => activityMock.removeActivity('3')).thenAnswer((_) async => true); - when(() => activityMock.getAllActivities('test-album')).thenAnswer((_) async => [..._activities]); - - final albumProvider = albumActivityProvider('test-album'); - container.read(albumProvider.notifier); - await container.read(albumProvider.future); - - await container.read(provider.notifier).removeActivity('3'); - - final assetActivities = container.read(provider).requireValue; - final albumActivities = container.read(albumProvider).requireValue; - - expect(assetActivities, hasLength(3)); - expect(assetActivities, isNot(anyElement(predicate((Activity a) => a.id == '3')))); - - expect(albumActivities, hasLength(3)); - expect(albumActivities, isNot(anyElement(predicate((Activity a) => a.id == '3')))); - - verify(() => activityMock.removeActivity('3')); - verifyNever(() => activityStatisticsMock.removeActivity()); - verifyNever(() => albumActivityStatisticsMock.removeActivity()); - }); - }); - - group('addComment()', () { - test('Comment successfully added', () async { - final comment = Activity( - id: '5', - createdAt: DateTime(2023), - type: ActivityType.comment, - user: UserStub.admin, - comment: 'Test-Comment', - assetId: 'test-asset', - ); - - final albumProvider = albumActivityProvider('test-album'); - container.read(albumProvider.notifier); - await container.read(albumProvider.future); - - when( - () => activityMock.addActivity( - 'test-album', - ActivityType.comment, - assetId: 'test-asset', - comment: 'Test-Comment', - ), - ).thenAnswer((_) async => AsyncData(comment)); - when(() => activityStatisticsMock.build('test-album', 'test-asset')).thenReturn(4); - when(() => albumActivityStatisticsMock.build('test-album')).thenReturn(2); - - await container.read(provider.notifier).addComment('Test-Comment'); - - verify( - () => activityMock.addActivity( - 'test-album', - ActivityType.comment, - assetId: 'test-asset', - comment: 'Test-Comment', - ), - ); - - final activities = await container.read(provider.future); - expect(activities, hasLength(5)); - expect(activities, contains(comment)); - - verify(() => activityStatisticsMock.addActivity()); - verify(() => albumActivityStatisticsMock.addActivity()); - - final albumActivities = container.read(albumProvider).requireValue; - expect(albumActivities, hasLength(5)); - expect(albumActivities, contains(comment)); - }); - - test('Comment successfully added without assetId', () async { - final comment = Activity( - id: '5', - createdAt: DateTime(2023), - type: ActivityType.comment, - user: UserStub.admin, - assetId: 'test-asset', - comment: 'Test-Comment', - ); - - when( - () => activityMock.addActivity('test-album', ActivityType.comment, comment: 'Test-Comment'), - ).thenAnswer((_) async => AsyncData(comment)); - when(() => albumActivityStatisticsMock.build('test-album')).thenReturn(2); - when(() => activityMock.getAllActivities('test-album')).thenAnswer((_) async => [..._activities]); - - final albumProvider = albumActivityProvider('test-album'); - container.read(albumProvider.notifier); - await container.read(albumProvider.future); - await container.read(albumProvider.notifier).addComment('Test-Comment'); - - verify( - () => activityMock.addActivity('test-album', ActivityType.comment, assetId: null, comment: 'Test-Comment'), - ); - - final activities = await container.read(albumProvider.future); - expect(activities, hasLength(5)); - expect(activities, contains(comment)); - - verifyNever(() => activityStatisticsMock.addActivity()); - verify(() => albumActivityStatisticsMock.addActivity()); - }); - - test('Comment failed', () async { - final comment = Activity( - id: '5', - createdAt: DateTime(2023), - type: ActivityType.comment, - user: UserStub.admin, - comment: 'Test-Comment', - assetId: 'test-asset', - ); - - when( - () => activityMock.addActivity( - 'test-album', - ActivityType.comment, - assetId: 'test-asset', - comment: 'Test-Comment', - ), - ).thenAnswer((_) async => AsyncError(Exception('Error'), StackTrace.current)); - - final albumProvider = albumActivityProvider('test-album'); - container.read(albumProvider.notifier); - await container.read(albumProvider.future); - - await container.read(provider.notifier).addComment('Test-Comment'); - - final activities = await container.read(provider.future); - expect(activities, hasLength(4)); - expect(activities, isNot(contains(comment))); - - verifyNever(() => activityStatisticsMock.addActivity()); - verifyNever(() => albumActivityStatisticsMock.addActivity()); - - final albumActivities = container.read(albumProvider).requireValue; - expect(albumActivities, hasLength(4)); - expect(albumActivities, isNot(contains(comment))); - }); - }); -} diff --git a/mobile/test/modules/activity/activity_statistics_provider_test.dart b/mobile/test/modules/activity/activity_statistics_provider_test.dart deleted file mode 100644 index 7fe73868f5..0000000000 --- a/mobile/test/modules/activity/activity_statistics_provider_test.dart +++ /dev/null @@ -1,71 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/models/activities/activity.model.dart'; -import 'package:immich_mobile/providers/activity_service.provider.dart'; -import 'package:immich_mobile/providers/activity_statistics.provider.dart'; -import 'package:mocktail/mocktail.dart'; - -import '../../test_utils.dart'; -import 'activity_mocks.dart'; - -void main() { - late ActivityServiceMock activityMock; - late ProviderContainer container; - late ListenerMock listener; - - setUp(() async { - activityMock = ActivityServiceMock(); - container = TestUtils.createContainer(overrides: [activityServiceProvider.overrideWith((ref) => activityMock)]); - listener = ListenerMock(); - }); - - test('Returns the proper count family', () async { - when( - () => activityMock.getStatistics('test-album', assetId: 'test-asset'), - ).thenAnswer((_) async => const ActivityStats(comments: 5)); - - // Read here to make the getStatistics call - container.read(activityStatisticsProvider('test-album', 'test-asset')); - - container.listen(activityStatisticsProvider('test-album', 'test-asset'), listener.call, fireImmediately: true); - - // Sleep for the getStatistics future to resolve - await Future.delayed(const Duration(milliseconds: 1)); - - verifyInOrder([() => listener.call(null, 0), () => listener.call(0, 5)]); - - verifyNoMoreInteractions(listener); - }); - - test('Adds activity', () async { - when(() => activityMock.getStatistics('test-album')).thenAnswer((_) async => const ActivityStats(comments: 10)); - - final provider = activityStatisticsProvider('test-album'); - container.listen(provider, listener.call, fireImmediately: true); - - // Sleep for the getStatistics future to resolve - await Future.delayed(const Duration(milliseconds: 1)); - - container.read(provider.notifier).addActivity(); - container.read(provider.notifier).addActivity(); - - expect(container.read(provider), 12); - }); - - test('Removes activity', () async { - when( - () => activityMock.getStatistics('new-album', assetId: 'test-asset'), - ).thenAnswer((_) async => const ActivityStats(comments: 10)); - - final provider = activityStatisticsProvider('new-album', 'test-asset'); - container.listen(provider, listener.call, fireImmediately: true); - - // Sleep for the getStatistics future to resolve - await Future.delayed(const Duration(milliseconds: 1)); - - container.read(provider.notifier).removeActivity(); - container.read(provider.notifier).removeActivity(); - - expect(container.read(provider), 8); - }); -} diff --git a/mobile/test/modules/activity/activity_text_field_test.dart b/mobile/test/modules/activity/activity_text_field_test.dart deleted file mode 100644 index 4f4a2c7068..0000000000 --- a/mobile/test/modules/activity/activity_text_field_test.dart +++ /dev/null @@ -1,149 +0,0 @@ -@Skip('currently failing due to mock HTTP client to download ISAR binaries') -@Tags(['widget']) -library; - -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:immich_mobile/domain/models/store.model.dart'; -import 'package:immich_mobile/domain/services/store.service.dart'; -import 'package:immich_mobile/entities/store.entity.dart'; -import 'package:immich_mobile/infrastructure/repositories/store.repository.dart'; -import 'package:immich_mobile/providers/activity.provider.dart'; -import 'package:immich_mobile/providers/album/current_album.provider.dart'; -import 'package:immich_mobile/providers/user.provider.dart'; -import 'package:immich_mobile/widgets/activities/activity_text_field.dart'; -import 'package:immich_mobile/widgets/common/user_circle_avatar.dart'; -import 'package:isar/isar.dart'; -import 'package:mocktail/mocktail.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; - -import '../../fixtures/album.stub.dart'; -import '../../fixtures/user.stub.dart'; -import '../../test_utils.dart'; -import '../../widget_tester_extensions.dart'; -import '../album/album_mocks.dart'; -import '../shared/shared_mocks.dart'; -import 'activity_mocks.dart'; - -void main() { - late Isar db; - late MockCurrentAlbumProvider mockCurrentAlbumProvider; - late MockAlbumActivity activityMock; - late List overrides; - - setUpAll(() async { - TestUtils.init(); - db = await TestUtils.initIsar(); - await StoreService.init(storeRepository: IsarStoreRepository(db)); - await Store.put(StoreKey.currentUser, UserStub.admin); - await Store.put(StoreKey.serverEndpoint, ''); - }); - - setUp(() { - mockCurrentAlbumProvider = MockCurrentAlbumProvider(AlbumStub.twoAsset); - activityMock = MockAlbumActivity(); - overrides = [ - currentAlbumProvider.overrideWith(() => mockCurrentAlbumProvider), - albumActivityProvider(AlbumStub.twoAsset.remoteId!).overrideWith(() => activityMock), - ]; - }); - - testWidgets('Returns an Input text field', (tester) async { - await tester.pumpConsumerWidget(ActivityTextField(onSubmit: (_) {}), overrides: overrides); - - expect(find.byType(TextField), findsOneWidget); - }); - - testWidgets('No UserCircleAvatar when user == null', (tester) async { - final userProvider = MockCurrentUserProvider(); - - await tester.pumpConsumerWidget( - ActivityTextField(onSubmit: (_) {}), - overrides: [currentUserProvider.overrideWith((ref) => userProvider), ...overrides], - ); - - expect(find.byType(UserCircleAvatar), findsNothing); - }); - - testWidgets('UserCircleAvatar displayed when user != null', (tester) async { - await tester.pumpConsumerWidget(ActivityTextField(onSubmit: (_) {}), overrides: overrides); - - expect(find.byType(UserCircleAvatar), findsOneWidget); - }); - - testWidgets('Filled icon if likedId != null', (tester) async { - await tester.pumpConsumerWidget( - ActivityTextField(onSubmit: (_) {}, likeId: '1'), - overrides: overrides, - ); - - expect(find.widgetWithIcon(IconButton, Icons.thumb_up), findsOneWidget); - expect(find.widgetWithIcon(IconButton, Icons.thumb_up_off_alt), findsNothing); - }); - - testWidgets('Bordered icon if likedId == null', (tester) async { - await tester.pumpConsumerWidget(ActivityTextField(onSubmit: (_) {}), overrides: overrides); - - expect(find.widgetWithIcon(IconButton, Icons.thumb_up_off_alt), findsOneWidget); - expect(find.widgetWithIcon(IconButton, Icons.thumb_up), findsNothing); - }); - - testWidgets('Adds new like', (tester) async { - await tester.pumpConsumerWidget(ActivityTextField(onSubmit: (_) {}), overrides: overrides); - - when(() => activityMock.addLike()).thenAnswer((_) => Future.value()); - - final suffixIcon = find.byType(IconButton); - await tester.tap(suffixIcon); - - verify(() => activityMock.addLike()); - }); - - testWidgets('Removes like if already liked', (tester) async { - await tester.pumpConsumerWidget( - ActivityTextField(onSubmit: (_) {}, likeId: 'test-suffix'), - overrides: overrides, - ); - - when(() => activityMock.removeActivity(any())).thenAnswer((_) => Future.value()); - - final suffixIcon = find.byType(IconButton); - await tester.tap(suffixIcon); - - verify(() => activityMock.removeActivity('test-suffix')); - }); - - testWidgets('Passes text entered to onSubmit on submit', (tester) async { - String? receivedText; - - await tester.pumpConsumerWidget( - ActivityTextField(onSubmit: (text) => receivedText = text, likeId: 'test-suffix'), - overrides: overrides, - ); - - final textField = find.byType(TextField); - await tester.enterText(textField, 'This is a test comment'); - await tester.testTextInput.receiveAction(TextInputAction.done); - expect(receivedText, 'This is a test comment'); - }); - - testWidgets('Input disabled when isEnabled false', (tester) async { - String? receviedText; - - await tester.pumpConsumerWidget( - ActivityTextField(onSubmit: (text) => receviedText = text, isEnabled: false, likeId: 'test-suffix'), - overrides: overrides, - ); - - final suffixIcon = find.byType(IconButton); - await tester.tap(suffixIcon, warnIfMissed: false); - - final textField = find.byType(TextField); - await tester.enterText(textField, 'This is a test comment'); - await tester.testTextInput.receiveAction(TextInputAction.done); - - expect(receviedText, isNull); - verifyNever(() => activityMock.addLike()); - verifyNever(() => activityMock.removeActivity(any())); - }); -} diff --git a/mobile/test/modules/activity/activity_tile_test.dart b/mobile/test/modules/activity/activity_tile_test.dart deleted file mode 100644 index 538e3c0911..0000000000 --- a/mobile/test/modules/activity/activity_tile_test.dart +++ /dev/null @@ -1,165 +0,0 @@ -@Skip('currently failing due to mock HTTP client to download ISAR binaries') -@Tags(['widget']) -library; - -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/domain/models/store.model.dart'; -import 'package:immich_mobile/domain/services/store.service.dart'; -import 'package:immich_mobile/entities/store.entity.dart'; -import 'package:immich_mobile/infrastructure/repositories/store.repository.dart'; -import 'package:immich_mobile/models/activities/activity.model.dart'; -import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart'; -import 'package:immich_mobile/widgets/activities/activity_tile.dart'; -import 'package:immich_mobile/widgets/common/user_circle_avatar.dart'; -import 'package:isar/isar.dart'; - -import '../../fixtures/asset.stub.dart'; -import '../../fixtures/user.stub.dart'; -import '../../test_utils.dart'; -import '../../widget_tester_extensions.dart'; -import '../asset_viewer/asset_viewer_mocks.dart'; - -void main() { - late MockCurrentAssetProvider assetProvider; - late List overrides; - late Isar db; - - setUpAll(() async { - TestUtils.init(); - db = await TestUtils.initIsar(); - // For UserCircleAvatar - await StoreService.init(storeRepository: IsarStoreRepository(db)); - await Store.put(StoreKey.currentUser, UserStub.admin); - await Store.put(StoreKey.serverEndpoint, ''); - await Store.put(StoreKey.accessToken, ''); - }); - - setUp(() { - assetProvider = MockCurrentAssetProvider(); - overrides = [currentAssetProvider.overrideWith(() => assetProvider)]; - }); - - testWidgets('Returns a ListTile', (tester) async { - await tester.pumpConsumerWidget( - ActivityTile(Activity(id: '1', createdAt: DateTime(100), type: ActivityType.like, user: UserStub.admin)), - overrides: overrides, - ); - - expect(find.byType(ListTile), findsOneWidget); - }); - - testWidgets('No trailing widget when activity assetId == null', (tester) async { - await tester.pumpConsumerWidget( - ActivityTile(Activity(id: '1', createdAt: DateTime(100), type: ActivityType.like, user: UserStub.admin)), - overrides: overrides, - ); - - final listTile = tester.widget(find.byType(ListTile)); - expect(listTile.trailing, isNull); - }); - - testWidgets('Asset Thumbanil as trailing widget when activity assetId != null', (tester) async { - await tester.pumpConsumerWidget( - ActivityTile( - Activity(id: '1', createdAt: DateTime(100), type: ActivityType.like, user: UserStub.admin, assetId: '1'), - ), - overrides: overrides, - ); - - final listTile = tester.widget(find.byType(ListTile)); - expect(listTile.trailing, isNotNull); - // TODO: Validate this to be the common class after migrating ActivityTile#_ActivityAssetThumbnail to a common class - }); - - testWidgets('No trailing widget when current asset != null', (tester) async { - await tester.pumpConsumerWidget( - ActivityTile( - Activity(id: '1', createdAt: DateTime(100), type: ActivityType.like, user: UserStub.admin, assetId: '1'), - ), - overrides: overrides, - ); - - assetProvider.state = AssetStub.image1; - await tester.pumpAndSettle(); - - final listTile = tester.widget(find.byType(ListTile)); - expect(listTile.trailing, isNull); - }); - - group('Like Activity', () { - final activity = Activity(id: '1', createdAt: DateTime(100), type: ActivityType.like, user: UserStub.admin); - - testWidgets('Like contains filled thumbs-up as leading', (tester) async { - await tester.pumpConsumerWidget(ActivityTile(activity), overrides: overrides); - - // Leading widget should not be null - final listTile = tester.widget(find.byType(ListTile)); - expect(listTile.leading, isNotNull); - - // And should have a thumb_up icon - final thumbUpIconFinder = find.widgetWithIcon(listTile.leading!.runtimeType, Icons.thumb_up); - - expect(thumbUpIconFinder, findsOneWidget); - }); - - testWidgets('Like title is center aligned', (tester) async { - await tester.pumpConsumerWidget(ActivityTile(activity), overrides: overrides); - - final listTile = tester.widget(find.byType(ListTile)); - - expect(listTile.titleAlignment, ListTileTitleAlignment.center); - }); - - testWidgets('No subtitle for likes', (tester) async { - await tester.pumpConsumerWidget(ActivityTile(activity), overrides: overrides); - - final listTile = tester.widget(find.byType(ListTile)); - - expect(listTile.subtitle, isNull); - }); - }); - - group('Comment Activity', () { - final activity = Activity( - id: '1', - createdAt: DateTime(100), - type: ActivityType.comment, - comment: 'This is a test comment', - user: UserStub.admin, - ); - - testWidgets('Comment contains User Circle Avatar as leading', (tester) async { - await tester.pumpConsumerWidget(ActivityTile(activity), overrides: overrides); - - final userAvatarFinder = find.byType(UserCircleAvatar); - expect(userAvatarFinder, findsOneWidget); - - // Leading widget should not be null - final listTile = tester.widget(find.byType(ListTile)); - expect(listTile.leading, isNotNull); - - // Make sure that the leading widget is the UserCircleAvatar - final userAvatar = tester.widget(userAvatarFinder); - expect(listTile.leading, userAvatar); - }); - - testWidgets('Comment title is top aligned', (tester) async { - await tester.pumpConsumerWidget(ActivityTile(activity), overrides: overrides); - - final listTile = tester.widget(find.byType(ListTile)); - - expect(listTile.titleAlignment, ListTileTitleAlignment.top); - }); - - testWidgets('Contains comment text as subtitle', (tester) async { - await tester.pumpConsumerWidget(ActivityTile(activity), overrides: overrides); - - final listTile = tester.widget(find.byType(ListTile)); - - expect(listTile.subtitle, isNotNull); - expect(find.descendant(of: find.byType(ListTile), matching: find.text(activity.comment!)), findsOneWidget); - }); - }); -} diff --git a/mobile/test/modules/activity/dismissible_activity_test.dart b/mobile/test/modules/activity/dismissible_activity_test.dart deleted file mode 100644 index 32516e73ea..0000000000 --- a/mobile/test/modules/activity/dismissible_activity_test.dart +++ /dev/null @@ -1,99 +0,0 @@ -@Tags(['widget']) -library; - -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:immich_mobile/models/activities/activity.model.dart'; -import 'package:immich_mobile/widgets/activities/activity_tile.dart'; -import 'package:immich_mobile/widgets/activities/dismissible_activity.dart'; -import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart'; -import 'package:immich_mobile/widgets/common/confirm_dialog.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; - -import '../../fixtures/user.stub.dart'; -import '../../test_utils.dart'; -import '../../widget_tester_extensions.dart'; -import '../asset_viewer/asset_viewer_mocks.dart'; - -final activity = Activity(id: '1', createdAt: DateTime(100), type: ActivityType.like, user: UserStub.admin); - -void main() { - late MockCurrentAssetProvider assetProvider; - late List overrides; - - setUpAll(() => TestUtils.init()); - - setUp(() { - assetProvider = MockCurrentAssetProvider(); - overrides = [currentAssetProvider.overrideWith(() => assetProvider)]; - }); - - testWidgets('Returns a Dismissible', (tester) async { - await tester.pumpConsumerWidget( - DismissibleActivity('1', ActivityTile(activity), onDismiss: (_) {}), - overrides: overrides, - ); - - expect(find.byType(Dismissible), findsOneWidget); - }); - - testWidgets('Dialog displayed when onDismiss is set', (tester) async { - await tester.pumpConsumerWidget( - DismissibleActivity('1', ActivityTile(activity), onDismiss: (_) {}), - overrides: overrides, - ); - - final dismissible = find.byType(Dismissible); - await tester.drag(dismissible, const Offset(500, 0)); - await tester.pumpAndSettle(); - - expect(find.byType(ConfirmDialog), findsOneWidget); - }); - - testWidgets('Ok action in ConfirmDialog should call onDismiss with activityId', (tester) async { - String? receivedActivityId; - await tester.pumpConsumerWidget( - DismissibleActivity('1', ActivityTile(activity), onDismiss: (id) => receivedActivityId = id), - overrides: overrides, - ); - - final dismissible = find.byType(Dismissible); - await tester.drag(dismissible, const Offset(-500, 0)); - await tester.pumpAndSettle(); - - final okButton = find.text('delete'); - await tester.tap(okButton); - await tester.pumpAndSettle(); - - expect(receivedActivityId, '1'); - }); - - testWidgets('Delete icon for background if onDismiss is set', (tester) async { - await tester.pumpConsumerWidget( - DismissibleActivity('1', ActivityTile(activity), onDismiss: (_) {}), - overrides: overrides, - ); - - final dismissible = find.byType(Dismissible); - await tester.drag(dismissible, const Offset(500, 0)); - await tester.pumpAndSettle(); - - expect(find.byIcon(Icons.delete_sweep_rounded), findsOneWidget); - }); - - testWidgets('No delete dialog if onDismiss is not set', (tester) async { - await tester.pumpConsumerWidget(DismissibleActivity('1', ActivityTile(activity)), overrides: overrides); - - // When onDismiss is not set, the widget should not be wrapped by a Dismissible - expect(find.byType(Dismissible), findsNothing); - expect(find.byType(ConfirmDialog), findsNothing); - }); - - testWidgets('No icon for background if onDismiss is not set', (tester) async { - await tester.pumpConsumerWidget(DismissibleActivity('1', ActivityTile(activity)), overrides: overrides); - - // No Dismissible should exist when onDismiss is not provided, so no delete icon either - expect(find.byType(Dismissible), findsNothing); - expect(find.byIcon(Icons.delete_sweep_rounded), findsNothing); - }); -} diff --git a/mobile/test/modules/album/album_mocks.dart b/mobile/test/modules/album/album_mocks.dart deleted file mode 100644 index 7a1b76e0c7..0000000000 --- a/mobile/test/modules/album/album_mocks.dart +++ /dev/null @@ -1,13 +0,0 @@ -import 'package:immich_mobile/providers/album/current_album.provider.dart'; -import 'package:immich_mobile/entities/album.entity.dart'; -import 'package:mocktail/mocktail.dart'; - -class MockCurrentAlbumProvider extends CurrentAlbum with Mock implements CurrentAlbumInternal { - Album? initAlbum; - MockCurrentAlbumProvider([this.initAlbum]); - - @override - Album? build() { - return initAlbum; - } -} diff --git a/mobile/test/modules/album/album_sort_by_options_provider_test.dart b/mobile/test/modules/album/album_sort_by_options_provider_test.dart deleted file mode 100644 index a35255bc21..0000000000 --- a/mobile/test/modules/album/album_sort_by_options_provider_test.dart +++ /dev/null @@ -1,270 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/album.entity.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/providers/album/album_sort_by_options.provider.dart'; -import 'package:immich_mobile/providers/app_settings.provider.dart'; -import 'package:immich_mobile/services/app_settings.service.dart'; -import 'package:isar/isar.dart'; -import 'package:mocktail/mocktail.dart'; - -import '../../fixtures/album.stub.dart'; -import '../../fixtures/asset.stub.dart'; -import '../../test_utils.dart'; -import '../settings/settings_mocks.dart'; - -void main() { - /// Verify the sort modes - group("AlbumSortMode", () { - late final Isar db; - - setUpAll(() async { - db = await TestUtils.initIsar(); - }); - - final albums = [AlbumStub.emptyAlbum, AlbumStub.sharedWithUser, AlbumStub.oneAsset, AlbumStub.twoAsset]; - - setUp(() { - db.writeTxnSync(() { - db.clearSync(); - // Save all assets - db.assets.putAllSync([AssetStub.image1, AssetStub.image2]); - db.albums.putAllSync(albums); - for (final album in albums) { - album.sharedUsers.saveSync(); - album.assets.saveSync(); - } - }); - expect(db.albums.countSync(), 4); - expect(db.assets.countSync(), 2); - }); - - group("Album sort - Created Time", () { - const created = AlbumSortMode.created; - test("Created time - ASC", () { - final sorted = created.sortFn(albums, false); - final sortedList = [AlbumStub.emptyAlbum, AlbumStub.twoAsset, AlbumStub.oneAsset, AlbumStub.sharedWithUser]; - expect(sorted, orderedEquals(sortedList)); - }); - - test("Created time - DESC", () { - final sorted = created.sortFn(albums, true); - final sortedList = [AlbumStub.sharedWithUser, AlbumStub.oneAsset, AlbumStub.twoAsset, AlbumStub.emptyAlbum]; - expect(sorted, orderedEquals(sortedList)); - }); - }); - - group("Album sort - Asset count", () { - const assetCount = AlbumSortMode.assetCount; - test("Asset Count - ASC", () { - final sorted = assetCount.sortFn(albums, false); - final sortedList = [AlbumStub.emptyAlbum, AlbumStub.sharedWithUser, AlbumStub.oneAsset, AlbumStub.twoAsset]; - expect(sorted, orderedEquals(sortedList)); - }); - - test("Asset Count - DESC", () { - final sorted = assetCount.sortFn(albums, true); - final sortedList = [AlbumStub.twoAsset, AlbumStub.oneAsset, AlbumStub.sharedWithUser, AlbumStub.emptyAlbum]; - expect(sorted, orderedEquals(sortedList)); - }); - }); - - group("Album sort - Last modified", () { - const lastModified = AlbumSortMode.lastModified; - test("Last modified - ASC", () { - final sorted = lastModified.sortFn(albums, false); - final sortedList = [AlbumStub.twoAsset, AlbumStub.emptyAlbum, AlbumStub.sharedWithUser, AlbumStub.oneAsset]; - expect(sorted, orderedEquals(sortedList)); - }); - - test("Last modified - DESC", () { - final sorted = lastModified.sortFn(albums, true); - final sortedList = [AlbumStub.oneAsset, AlbumStub.sharedWithUser, AlbumStub.emptyAlbum, AlbumStub.twoAsset]; - expect(sorted, orderedEquals(sortedList)); - }); - }); - - group("Album sort - Created", () { - const created = AlbumSortMode.created; - test("Created - ASC", () { - final sorted = created.sortFn(albums, false); - final sortedList = [AlbumStub.emptyAlbum, AlbumStub.twoAsset, AlbumStub.oneAsset, AlbumStub.sharedWithUser]; - expect(sorted, orderedEquals(sortedList)); - }); - - test("Created - DESC", () { - final sorted = created.sortFn(albums, true); - final sortedList = [AlbumStub.sharedWithUser, AlbumStub.oneAsset, AlbumStub.twoAsset, AlbumStub.emptyAlbum]; - expect(sorted, orderedEquals(sortedList)); - }); - }); - - group("Album sort - Most Recent", () { - const mostRecent = AlbumSortMode.mostRecent; - - test("Most Recent - DESC", () { - final sorted = mostRecent.sortFn([ - AlbumStub.create2020end2020Album, - AlbumStub.create2020end2022Album, - AlbumStub.create2020end2024Album, - AlbumStub.create2020end2026Album, - ], false); - final sortedList = [ - AlbumStub.create2020end2026Album, - AlbumStub.create2020end2024Album, - AlbumStub.create2020end2022Album, - AlbumStub.create2020end2020Album, - ]; - expect(sorted, orderedEquals(sortedList)); - }); - - test("Most Recent - ASC", () { - final sorted = mostRecent.sortFn([ - AlbumStub.create2020end2020Album, - AlbumStub.create2020end2022Album, - AlbumStub.create2020end2024Album, - AlbumStub.create2020end2026Album, - ], true); - final sortedList = [ - AlbumStub.create2020end2020Album, - AlbumStub.create2020end2022Album, - AlbumStub.create2020end2024Album, - AlbumStub.create2020end2026Album, - ]; - expect(sorted, orderedEquals(sortedList)); - }); - }); - - group("Album sort - Most Oldest", () { - const mostOldest = AlbumSortMode.mostOldest; - - test("Most Oldest - ASC", () { - final sorted = mostOldest.sortFn(albums, false); - final sortedList = [AlbumStub.twoAsset, AlbumStub.emptyAlbum, AlbumStub.oneAsset, AlbumStub.sharedWithUser]; - expect(sorted, orderedEquals(sortedList)); - }); - - test("Most Oldest - DESC", () { - final sorted = mostOldest.sortFn(albums, true); - final sortedList = [AlbumStub.sharedWithUser, AlbumStub.oneAsset, AlbumStub.emptyAlbum, AlbumStub.twoAsset]; - expect(sorted, orderedEquals(sortedList)); - }); - }); - }); - - /// Verify the sort mode provider - group('AlbumSortByOptions', () { - late AppSettingsService settingsMock; - late ProviderContainer container; - - setUp(() async { - settingsMock = MockAppSettingsService(); - container = TestUtils.createContainer( - overrides: [appSettingsServiceProvider.overrideWith((ref) => settingsMock)], - ); - when( - () => settingsMock.setSetting(AppSettingsEnum.selectedAlbumSortReverse, any()), - ).thenAnswer((_) async => {}); - when( - () => settingsMock.setSetting(AppSettingsEnum.selectedAlbumSortOrder, any()), - ).thenAnswer((_) async => {}); - }); - - test('Returns the default sort mode when none set', () { - // Returns the default value when nothing is set - when(() => settingsMock.getSetting(AppSettingsEnum.selectedAlbumSortOrder)).thenReturn(0); - - expect(container.read(albumSortByOptionsProvider), AlbumSortMode.created); - }); - - test('Returns the correct sort mode with index from Store', () { - // Returns the default value when nothing is set - when(() => settingsMock.getSetting(AppSettingsEnum.selectedAlbumSortOrder)).thenReturn(3); - - expect(container.read(albumSortByOptionsProvider), AlbumSortMode.lastModified); - }); - - test('Properly saves the correct store index of sort mode', () { - container.read(albumSortByOptionsProvider.notifier).changeSortMode(AlbumSortMode.mostOldest); - - verify( - () => settingsMock.setSetting(AppSettingsEnum.selectedAlbumSortOrder, AlbumSortMode.mostOldest.storeIndex), - ); - }); - - test('Notifies listeners on state change', () { - when(() => settingsMock.getSetting(AppSettingsEnum.selectedAlbumSortOrder)).thenReturn(0); - - final listener = ListenerMock(); - container.listen(albumSortByOptionsProvider, listener.call, fireImmediately: true); - - // Created -> Most Oldest - container.read(albumSortByOptionsProvider.notifier).changeSortMode(AlbumSortMode.mostOldest); - - // Most Oldest -> Title - container.read(albumSortByOptionsProvider.notifier).changeSortMode(AlbumSortMode.title); - - verifyInOrder([ - () => listener.call(null, AlbumSortMode.created), - () => listener.call(AlbumSortMode.created, AlbumSortMode.mostOldest), - () => listener.call(AlbumSortMode.mostOldest, AlbumSortMode.title), - ]); - - verifyNoMoreInteractions(listener); - }); - }); - - /// Verify the sort order provider - group('AlbumSortOrder', () { - late AppSettingsService settingsMock; - late ProviderContainer container; - - registerFallbackValue(AppSettingsEnum.selectedAlbumSortReverse); - - setUp(() async { - settingsMock = MockAppSettingsService(); - container = TestUtils.createContainer( - overrides: [appSettingsServiceProvider.overrideWith((ref) => settingsMock)], - ); - when( - () => settingsMock.setSetting(AppSettingsEnum.selectedAlbumSortReverse, any()), - ).thenAnswer((_) async => {}); - when( - () => settingsMock.setSetting(AppSettingsEnum.selectedAlbumSortOrder, any()), - ).thenAnswer((_) async => {}); - }); - - test('Returns the default sort order when none set - false', () { - when(() => settingsMock.getSetting(AppSettingsEnum.selectedAlbumSortReverse)).thenReturn(false); - - expect(container.read(albumSortOrderProvider), isFalse); - }); - - test('Properly saves the correct order', () { - container.read(albumSortOrderProvider.notifier).changeSortDirection(true); - - verify(() => settingsMock.setSetting(AppSettingsEnum.selectedAlbumSortReverse, true)); - }); - - test('Notifies listeners on state change', () { - when(() => settingsMock.getSetting(AppSettingsEnum.selectedAlbumSortReverse)).thenReturn(false); - - final listener = ListenerMock(); - container.listen(albumSortOrderProvider, listener.call, fireImmediately: true); - - // false -> true - container.read(albumSortOrderProvider.notifier).changeSortDirection(true); - - // true -> false - container.read(albumSortOrderProvider.notifier).changeSortDirection(false); - - verifyInOrder([ - () => listener.call(null, false), - () => listener.call(false, true), - () => listener.call(true, false), - ]); - - verifyNoMoreInteractions(listener); - }); - }); -} diff --git a/mobile/test/modules/asset_viewer/asset_viewer_mocks.dart b/mobile/test/modules/asset_viewer/asset_viewer_mocks.dart deleted file mode 100644 index 89b06d3c09..0000000000 --- a/mobile/test/modules/asset_viewer/asset_viewer_mocks.dart +++ /dev/null @@ -1,13 +0,0 @@ -import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:mocktail/mocktail.dart'; - -class MockCurrentAssetProvider extends CurrentAssetInternal with Mock implements CurrentAsset { - Asset? initAsset; - MockCurrentAssetProvider([this.initAsset]); - - @override - Asset? build() { - return initAsset; - } -} diff --git a/mobile/test/modules/extensions/asset_extensions_test.dart b/mobile/test/modules/extensions/asset_extensions_test.dart deleted file mode 100644 index 2b9b740ca7..0000000000 --- a/mobile/test/modules/extensions/asset_extensions_test.dart +++ /dev/null @@ -1,113 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:immich_mobile/domain/models/exif.model.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/extensions/asset_extensions.dart'; -import 'package:timezone/data/latest.dart'; -import 'package:timezone/timezone.dart'; - -ExifInfo makeExif({DateTime? dateTimeOriginal, String? timeZone}) { - return ExifInfo(dateTimeOriginal: dateTimeOriginal, timeZone: timeZone); -} - -Asset makeAsset({required String id, required DateTime createdAt, ExifInfo? exifInfo}) { - return Asset( - checksum: '', - localId: id, - remoteId: id, - ownerId: 1, - fileCreatedAt: createdAt, - fileModifiedAt: DateTime.now(), - updatedAt: DateTime.now(), - durationInSeconds: 0, - type: AssetType.image, - fileName: id, - isFavorite: false, - isArchived: false, - isTrashed: false, - exifInfo: exifInfo, - ); -} - -void main() { - // Init Timezone DB - initializeTimeZones(); - - group("Returns local time and offset if no exifInfo", () { - test('returns createdAt directly if in local', () { - final createdAt = DateTime(2023, 12, 12, 12, 12, 12); - final a = makeAsset(id: '1', createdAt: createdAt); - final (dt, tz) = a.getTZAdjustedTimeAndOffset(); - - expect(createdAt, dt); - expect(createdAt.timeZoneOffset, tz); - }); - - test('returns createdAt in local if in utc', () { - final createdAt = DateTime.utc(2023, 12, 12, 12, 12, 12); - final a = makeAsset(id: '1', createdAt: createdAt); - final (dt, tz) = a.getTZAdjustedTimeAndOffset(); - - final localCreatedAt = createdAt.toLocal(); - expect(localCreatedAt, dt); - expect(localCreatedAt.timeZoneOffset, tz); - }); - }); - - group("Returns dateTimeOriginal", () { - test('Returns dateTimeOriginal in UTC from exifInfo without timezone', () { - final createdAt = DateTime.parse("2023-01-27T14:00:00-0500"); - final dateTimeOriginal = DateTime.parse("2022-01-27T14:00:00+0530"); - final e = makeExif(dateTimeOriginal: dateTimeOriginal); - final a = makeAsset(id: '1', createdAt: createdAt, exifInfo: e); - final (dt, tz) = a.getTZAdjustedTimeAndOffset(); - - final dateTimeInUTC = dateTimeOriginal.toUtc(); - expect(dateTimeInUTC, dt); - expect(dateTimeInUTC.timeZoneOffset, tz); - }); - - test('Returns dateTimeOriginal in UTC from exifInfo with invalid timezone', () { - final createdAt = DateTime.parse("2023-01-27T14:00:00-0500"); - final dateTimeOriginal = DateTime.parse("2022-01-27T14:00:00+0530"); - final e = makeExif(dateTimeOriginal: dateTimeOriginal, timeZone: "#_#"); // Invalid timezone - final a = makeAsset(id: '1', createdAt: createdAt, exifInfo: e); - final (dt, tz) = a.getTZAdjustedTimeAndOffset(); - - final dateTimeInUTC = dateTimeOriginal.toUtc(); - expect(dateTimeInUTC, dt); - expect(dateTimeInUTC.timeZoneOffset, tz); - }); - }); - - group("Returns adjusted time if timezone available", () { - test('With timezone as location', () { - final createdAt = DateTime.parse("2023-01-27T14:00:00-0500"); - final dateTimeOriginal = DateTime.parse("2022-01-27T14:00:00+0530"); - const location = "Asia/Hong_Kong"; - final e = makeExif(dateTimeOriginal: dateTimeOriginal, timeZone: location); - final a = makeAsset(id: '1', createdAt: createdAt, exifInfo: e); - final (dt, tz) = a.getTZAdjustedTimeAndOffset(); - - final adjustedTime = TZDateTime.from(dateTimeOriginal.toUtc(), getLocation(location)); - expect(adjustedTime, dt); - expect(adjustedTime.timeZoneOffset, tz); - }); - - test('With timezone as offset', () { - final createdAt = DateTime.parse("2023-01-27T14:00:00-0500"); - final dateTimeOriginal = DateTime.parse("2022-01-27T14:00:00+0530"); - const offset = "utc+08:00"; - final e = makeExif(dateTimeOriginal: dateTimeOriginal, timeZone: offset); - final a = makeAsset(id: '1', createdAt: createdAt, exifInfo: e); - final (dt, tz) = a.getTZAdjustedTimeAndOffset(); - - final location = getLocation("Asia/Hong_Kong"); - final offsetFromLocation = Duration(milliseconds: location.currentTimeZone.offset); - final adjustedTime = dateTimeOriginal.toUtc().add(offsetFromLocation); - - // Adds the offset to the actual time and returns the offset separately - expect(adjustedTime, dt); - expect(offsetFromLocation, tz); - }); - }); -} diff --git a/mobile/test/modules/home/asset_grid_data_structure_test.dart b/mobile/test/modules/home/asset_grid_data_structure_test.dart deleted file mode 100644 index 3e1fe06c68..0000000000 --- a/mobile/test/modules/home/asset_grid_data_structure_test.dart +++ /dev/null @@ -1,113 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; - -void main() { - final List testAssets = []; - - for (int i = 0; i < 150; i++) { - int month = i ~/ 31; - int day = (i % 31).toInt(); - - DateTime date = DateTime(2022, month, day); - - testAssets.add( - Asset( - checksum: "", - localId: '$i', - ownerId: 1, - fileCreatedAt: date, - fileModifiedAt: date, - updatedAt: date, - durationInSeconds: 0, - type: AssetType.image, - fileName: '', - isFavorite: false, - isArchived: false, - isTrashed: false, - ), - ); - } - - final List assets = []; - - assets.addAll( - testAssets.sublist(0, 5).map((e) { - e.fileCreatedAt = DateTime(2022, 1, 5); - return e; - }).toList(), - ); - assets.addAll( - testAssets.sublist(5, 10).map((e) { - e.fileCreatedAt = DateTime(2022, 1, 10); - return e; - }).toList(), - ); - assets.addAll( - testAssets.sublist(10, 15).map((e) { - e.fileCreatedAt = DateTime(2022, 2, 17); - return e; - }).toList(), - ); - assets.addAll( - testAssets.sublist(15, 30).map((e) { - e.fileCreatedAt = DateTime(2022, 10, 15); - return e; - }).toList(), - ); - - group('Test grouped', () { - test('test grouped check months', () async { - final renderList = await RenderList.fromAssets(assets, GroupAssetsBy.day); - - // Oct - // Day 1 - // 15 Assets => 5 Rows - // Feb - // Day 1 - // 5 Assets => 2 Rows - // Jan - // Day 2 - // 5 Assets => 2 Rows - // Day 1 - // 5 Assets => 2 Rows - expect(renderList.elements, hasLength(4)); - expect(renderList.elements[0].type, RenderAssetGridElementType.monthTitle); - expect(renderList.elements[0].date.month, 1); - expect(renderList.elements[1].type, RenderAssetGridElementType.groupDividerTitle); - expect(renderList.elements[1].date.month, 1); - expect(renderList.elements[2].type, RenderAssetGridElementType.monthTitle); - expect(renderList.elements[2].date.month, 2); - expect(renderList.elements[3].type, RenderAssetGridElementType.monthTitle); - expect(renderList.elements[3].date.month, 10); - }); - - test('test grouped check types', () async { - final renderList = await RenderList.fromAssets(assets, GroupAssetsBy.day); - - // Oct - // Day 1 - // 15 Assets => 3 Rows - // Feb - // Day 1 - // 5 Assets => 1 Row - // Jan - // Day 2 - // 5 Assets => 1 Row - // Day 1 - // 5 Assets => 1 Row - final types = [ - RenderAssetGridElementType.monthTitle, - RenderAssetGridElementType.groupDividerTitle, - RenderAssetGridElementType.monthTitle, - RenderAssetGridElementType.monthTitle, - ]; - - expect(renderList.elements, hasLength(types.length)); - - for (int i = 0; i < renderList.elements.length; i++) { - expect(renderList.elements[i].type, types[i]); - } - }); - }); -} diff --git a/mobile/test/modules/map/map_theme_override_test.dart b/mobile/test/modules/map/map_theme_override_test.dart index de16b7f24f..56efde98dd 100644 --- a/mobile/test/modules/map/map_theme_override_test.dart +++ b/mobile/test/modules/map/map_theme_override_test.dart @@ -2,16 +2,18 @@ @Tags(['widget']) library; +import 'package:drift/drift.dart'; +import 'package:drift/native.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/services/store.service.dart'; +import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/store.repository.dart'; import 'package:immich_mobile/models/map/map_state.model.dart'; import 'package:immich_mobile/providers/locale_provider.dart'; import 'package:immich_mobile/providers/map/map_state.provider.dart'; import 'package:immich_mobile/widgets/map/map_theme_override.dart'; -import 'package:isar/isar.dart'; import '../../test_utils.dart'; import '../../widget_tester_extensions.dart'; @@ -21,17 +23,17 @@ void main() { late MockMapStateNotifier mapStateNotifier; late List overrides; late MapState mapState; - late Isar db; + late Drift db; setUpAll(() async { - db = await TestUtils.initIsar(); + db = Drift(DatabaseConnection(NativeDatabase.memory(), closeStreamsSynchronously: true)); TestUtils.init(); }); setUp(() async { mapState = const MapState(themeMode: ThemeMode.dark); mapStateNotifier = MockMapStateNotifier(mapState); - await StoreService.init(storeRepository: IsarStoreRepository(db)); + await StoreService.init(storeRepository: DriftStoreRepository(db)); overrides = [ mapStateNotifierProvider.overrideWith(() => mapStateNotifier), localeProvider.overrideWithValue(const Locale("en")), diff --git a/mobile/test/modules/settings/settings_mocks.dart b/mobile/test/modules/settings/settings_mocks.dart deleted file mode 100644 index 63fd9312b7..0000000000 --- a/mobile/test/modules/settings/settings_mocks.dart +++ /dev/null @@ -1,4 +0,0 @@ -import 'package:immich_mobile/services/app_settings.service.dart'; -import 'package:mocktail/mocktail.dart'; - -class MockAppSettingsService extends Mock implements AppSettingsService {} diff --git a/mobile/test/modules/shared/shared_mocks.dart b/mobile/test/modules/shared/shared_mocks.dart deleted file mode 100644 index 790bbbd815..0000000000 --- a/mobile/test/modules/shared/shared_mocks.dart +++ /dev/null @@ -1,11 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/domain/models/user.model.dart'; -import 'package:immich_mobile/providers/user.provider.dart'; -import 'package:mocktail/mocktail.dart'; - -class MockCurrentUserProvider extends StateNotifier with Mock implements CurrentUserProvider { - MockCurrentUserProvider() : super(null); - - @override - set state(UserDto? user) => super.state = user; -} diff --git a/mobile/test/modules/shared/sync_service_test.dart b/mobile/test/modules/shared/sync_service_test.dart deleted file mode 100644 index 767a52b8d8..0000000000 --- a/mobile/test/modules/shared/sync_service_test.dart +++ /dev/null @@ -1,285 +0,0 @@ -import 'package:drift/drift.dart'; -import 'package:drift/native.dart'; -import 'package:flutter/widgets.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:immich_mobile/constants/enums.dart'; -import 'package:immich_mobile/domain/models/store.model.dart'; -import 'package:immich_mobile/domain/models/user.model.dart'; -import 'package:immich_mobile/domain/services/log.service.dart'; -import 'package:immich_mobile/domain/services/store.service.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/entities/etag.entity.dart'; -import 'package:immich_mobile/entities/store.entity.dart'; -import 'package:immich_mobile/infrastructure/repositories/log.repository.dart'; -import 'package:immich_mobile/infrastructure/repositories/logger_db.repository.dart'; -import 'package:immich_mobile/infrastructure/repositories/store.repository.dart'; -import 'package:immich_mobile/repositories/asset.repository.dart'; -import 'package:immich_mobile/repositories/partner_api.repository.dart'; -import 'package:immich_mobile/services/sync.service.dart'; -import 'package:mocktail/mocktail.dart'; - -import '../../domain/service.mock.dart'; -import '../../fixtures/asset.stub.dart'; -import '../../infrastructure/repository.mock.dart'; -import '../../repository.mocks.dart'; -import '../../service.mocks.dart'; -import '../../test_utils.dart'; - -void main() { - int assetIdCounter = 0; - Asset makeAsset({ - required String checksum, - String? localId, - String? remoteId, - int ownerId = 590700560494856554, // hash of "1" - }) { - final DateTime date = DateTime(2000); - return Asset( - id: assetIdCounter++, - checksum: checksum, - localId: localId, - remoteId: remoteId, - ownerId: ownerId, - fileCreatedAt: date, - fileModifiedAt: date, - updatedAt: date, - durationInSeconds: 0, - type: AssetType.image, - fileName: localId ?? remoteId ?? "", - isFavorite: false, - isArchived: false, - isTrashed: false, - ); - } - - final owner = UserDto( - id: "1", - updatedAt: DateTime.now(), - email: "a@b.c", - name: "first last", - isAdmin: false, - profileChangedAt: DateTime.now(), - ); - - setUpAll(() async { - final loggerDb = DriftLogger(DatabaseConnection(NativeDatabase.memory(), closeStreamsSynchronously: true)); - final LogRepository logRepository = LogRepository(loggerDb); - - WidgetsFlutterBinding.ensureInitialized(); - final db = await TestUtils.initIsar(); - - db.writeTxnSync(() => db.clearSync()); - await StoreService.init(storeRepository: IsarStoreRepository(db)); - await Store.put(StoreKey.currentUser, owner); - await LogService.init(logRepository: logRepository, storeRepository: IsarStoreRepository(db)); - }); - - group('Test SyncService grouped', () { - final MockHashService hs = MockHashService(); - final MockEntityService entityService = MockEntityService(); - final MockAlbumRepository albumRepository = MockAlbumRepository(); - final MockAssetRepository assetRepository = MockAssetRepository(); - final MockExifInfoRepository exifInfoRepository = MockExifInfoRepository(); - final MockIsarUserRepository userRepository = MockIsarUserRepository(); - final MockETagRepository eTagRepository = MockETagRepository(); - final MockAlbumMediaRepository albumMediaRepository = MockAlbumMediaRepository(); - final MockAlbumApiRepository albumApiRepository = MockAlbumApiRepository(); - final MockAppSettingService appSettingService = MockAppSettingService(); - final MockLocalFilesManagerRepository localFilesManagerRepository = MockLocalFilesManagerRepository(); - final MockPartnerApiRepository partnerApiRepository = MockPartnerApiRepository(); - final MockUserApiRepository userApiRepository = MockUserApiRepository(); - final MockPartnerRepository partnerRepository = MockPartnerRepository(); - final MockUserService userService = MockUserService(); - - final owner = UserDto( - id: "1", - updatedAt: DateTime.now(), - email: "a@b.c", - name: "first last", - isAdmin: false, - profileChangedAt: DateTime(2021), - ); - - late SyncService s; - - final List initialAssets = [ - makeAsset(checksum: "a", remoteId: "0-1"), - makeAsset(checksum: "b", remoteId: "2-1"), - makeAsset(checksum: "c", localId: "1", remoteId: "1-1"), - makeAsset(checksum: "d", localId: "2"), - makeAsset(checksum: "e", localId: "3"), - ]; - setUp(() { - s = SyncService( - hs, - entityService, - albumMediaRepository, - albumApiRepository, - albumRepository, - assetRepository, - exifInfoRepository, - partnerRepository, - userRepository, - userService, - eTagRepository, - appSettingService, - localFilesManagerRepository, - partnerApiRepository, - userApiRepository, - ); - when(() => userService.getMyUser()).thenReturn(owner); - when(() => eTagRepository.get(owner.id)).thenAnswer((_) async => ETag(id: owner.id, time: DateTime.now())); - when(() => eTagRepository.deleteByIds(["1"])).thenAnswer((_) async {}); - when(() => eTagRepository.upsertAll(any())).thenAnswer((_) async {}); - when(() => partnerRepository.getSharedWith()).thenAnswer((_) async => []); - when(() => userRepository.getAll(sortBy: SortUserBy.id)).thenAnswer((_) async => [owner]); - when(() => userRepository.getAll()).thenAnswer((_) async => [owner]); - when( - () => assetRepository.getAll(ownerId: owner.id, sortBy: AssetSort.checksum), - ).thenAnswer((_) async => initialAssets); - when( - () => assetRepository.getAllByOwnerIdChecksum(any(), any()), - ).thenAnswer((_) async => [initialAssets[3], null, null]); - when(() => assetRepository.updateAll(any())).thenAnswer((_) async => []); - when(() => assetRepository.deleteByIds(any())).thenAnswer((_) async {}); - when(() => exifInfoRepository.updateAll(any())).thenAnswer((_) async => []); - when( - () => assetRepository.transaction(any()), - ).thenAnswer((call) => (call.positionalArguments.first as Function).call()); - when( - () => assetRepository.transaction(any()), - ).thenAnswer((call) => (call.positionalArguments.first as Function).call()); - when(() => userApiRepository.getAll()).thenAnswer((_) async => [owner]); - registerFallbackValue(Direction.sharedByMe); - when(() => partnerApiRepository.getAll(any())).thenAnswer((_) async => []); - }); - test('test inserting existing assets', () async { - final List remoteAssets = [ - makeAsset(checksum: "a", remoteId: "0-1"), - makeAsset(checksum: "b", remoteId: "2-1"), - makeAsset(checksum: "c", remoteId: "1-1"), - ]; - final bool c1 = await s.syncRemoteAssetsToDb( - users: [owner], - getChangedAssets: _failDiff, - loadAssets: (u, d) => remoteAssets, - ); - expect(c1, isFalse); - verifyNever(() => assetRepository.updateAll(any())); - }); - - test('test inserting new assets', () async { - final List remoteAssets = [ - makeAsset(checksum: "a", remoteId: "0-1"), - makeAsset(checksum: "b", remoteId: "2-1"), - makeAsset(checksum: "c", remoteId: "1-1"), - makeAsset(checksum: "d", remoteId: "1-2"), - makeAsset(checksum: "f", remoteId: "1-4"), - makeAsset(checksum: "g", remoteId: "3-1"), - ]; - final bool c1 = await s.syncRemoteAssetsToDb( - users: [owner], - getChangedAssets: _failDiff, - loadAssets: (u, d) => remoteAssets, - ); - expect(c1, isTrue); - final updatedAsset = initialAssets[3].updatedCopy(remoteAssets[3]); - verify(() => assetRepository.updateAll([remoteAssets[4], remoteAssets[5], updatedAsset])); - }); - - test('test syncing duplicate assets', () async { - final List remoteAssets = [ - makeAsset(checksum: "a", remoteId: "0-1"), - makeAsset(checksum: "b", remoteId: "1-1"), - makeAsset(checksum: "c", remoteId: "2-1"), - makeAsset(checksum: "h", remoteId: "2-1b"), - makeAsset(checksum: "i", remoteId: "2-1c"), - makeAsset(checksum: "j", remoteId: "2-1d"), - ]; - final bool c1 = await s.syncRemoteAssetsToDb( - users: [owner], - getChangedAssets: _failDiff, - loadAssets: (u, d) => remoteAssets, - ); - expect(c1, isTrue); - when( - () => assetRepository.getAll(ownerId: owner.id, sortBy: AssetSort.checksum), - ).thenAnswer((_) async => remoteAssets); - final bool c2 = await s.syncRemoteAssetsToDb( - users: [owner], - getChangedAssets: _failDiff, - loadAssets: (u, d) => remoteAssets, - ); - expect(c2, isFalse); - final currentState = [...remoteAssets]; - when( - () => assetRepository.getAll(ownerId: owner.id, sortBy: AssetSort.checksum), - ).thenAnswer((_) async => currentState); - remoteAssets.removeAt(4); - final bool c3 = await s.syncRemoteAssetsToDb( - users: [owner], - getChangedAssets: _failDiff, - loadAssets: (u, d) => remoteAssets, - ); - expect(c3, isTrue); - remoteAssets.add(makeAsset(checksum: "k", remoteId: "2-1e")); - remoteAssets.add(makeAsset(checksum: "l", remoteId: "2-2")); - final bool c4 = await s.syncRemoteAssetsToDb( - users: [owner], - getChangedAssets: _failDiff, - loadAssets: (u, d) => remoteAssets, - ); - expect(c4, isTrue); - }); - - test('test efficient sync', () async { - when( - () => assetRepository.deleteAllByRemoteId([ - initialAssets[1].remoteId!, - initialAssets[2].remoteId!, - ], state: AssetState.remote), - ).thenAnswer((_) async { - return; - }); - when( - () => assetRepository.getAllByRemoteId(["2-1", "1-1"], state: AssetState.merged), - ).thenAnswer((_) async => [initialAssets[2]]); - when( - () => assetRepository.getAllByOwnerIdChecksum(any(), any()), - ).thenAnswer((_) async => [initialAssets[0], null, null]); //afg - final List toUpsert = [ - makeAsset(checksum: "a", remoteId: "0-1"), // changed - makeAsset(checksum: "f", remoteId: "0-2"), // new - makeAsset(checksum: "g", remoteId: "0-3"), // new - ]; - toUpsert[0].isFavorite = true; - final List toDelete = ["2-1", "1-1"]; - final expected = [...toUpsert]; - expected[0].id = initialAssets[0].id; - final bool c = await s.syncRemoteAssetsToDb( - users: [owner], - getChangedAssets: (user, since) async => (toUpsert, toDelete), - loadAssets: (user, date) => throw Exception(), - ); - expect(c, isTrue); - verify(() => assetRepository.updateAll(expected)); - }); - - group("upsertAssetsWithExif", () { - test('test upsert with EXIF data', () async { - final assets = [AssetStub.image1, AssetStub.image2]; - - expect(assets.map((a) => a.exifInfo?.assetId), List.filled(assets.length, null)); - await s.upsertAssetsWithExif(assets); - verify( - () => exifInfoRepository.updateAll( - any(that: containsAll(assets.map((a) => a.exifInfo!.copyWith(assetId: a.id)))), - ), - ); - expect(assets.map((a) => a.exifInfo?.assetId), assets.map((a) => a.id)); - }); - }); - }); -} - -Future<(List?, List?)> _failDiff(List user, DateTime time) => Future.value((null, null)); diff --git a/mobile/test/modules/utils/migration_test.dart b/mobile/test/modules/utils/migration_test.dart deleted file mode 100644 index 08ab1204a6..0000000000 --- a/mobile/test/modules/utils/migration_test.dart +++ /dev/null @@ -1,131 +0,0 @@ -import 'package:drift/drift.dart' hide isNull; -import 'package:drift/native.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:immich_mobile/domain/models/store.model.dart'; -import 'package:immich_mobile/domain/services/store.service.dart'; -import 'package:immich_mobile/entities/store.entity.dart'; -import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; -import 'package:immich_mobile/infrastructure/repositories/store.repository.dart'; -import 'package:immich_mobile/infrastructure/repositories/sync_stream.repository.dart'; -import 'package:immich_mobile/utils/migration.dart'; -import 'package:mocktail/mocktail.dart'; - -import '../../infrastructure/repository.mock.dart'; - -void main() { - late Drift db; - late SyncStreamRepository mockSyncStreamRepository; - - setUpAll(() async { - db = Drift(DatabaseConnection(NativeDatabase.memory(), closeStreamsSynchronously: true)); - await StoreService.init(storeRepository: DriftStoreRepository(db)); - mockSyncStreamRepository = MockSyncStreamRepository(); - when(() => mockSyncStreamRepository.reset()).thenAnswer((_) async => {}); - }); - - tearDown(() async { - await Store.clear(); - }); - - group('handleBetaMigration Tests', () { - group("version < 15", () { - test('already on new timeline', () async { - await Store.put(StoreKey.betaTimeline, true); - - await handleBetaMigration(14, false, mockSyncStreamRepository); - - expect(Store.tryGet(StoreKey.betaTimeline), true); - expect(Store.tryGet(StoreKey.needBetaMigration), false); - }); - - test('already on old timeline', () async { - await Store.put(StoreKey.betaTimeline, false); - - await handleBetaMigration(14, false, mockSyncStreamRepository); - - expect(Store.tryGet(StoreKey.needBetaMigration), true); - }); - - test('fresh install', () async { - await Store.delete(StoreKey.betaTimeline); - await handleBetaMigration(14, true, mockSyncStreamRepository); - - expect(Store.tryGet(StoreKey.betaTimeline), true); - expect(Store.tryGet(StoreKey.needBetaMigration), false); - }); - }); - - group("version == 15", () { - test('already on new timeline', () async { - await Store.put(StoreKey.betaTimeline, true); - - await handleBetaMigration(15, false, mockSyncStreamRepository); - - expect(Store.tryGet(StoreKey.betaTimeline), true); - expect(Store.tryGet(StoreKey.needBetaMigration), false); - }); - - test('already on old timeline', () async { - await Store.put(StoreKey.betaTimeline, false); - - await handleBetaMigration(15, false, mockSyncStreamRepository); - - expect(Store.tryGet(StoreKey.needBetaMigration), true); - }); - - test('fresh install', () async { - await Store.delete(StoreKey.betaTimeline); - await handleBetaMigration(15, true, mockSyncStreamRepository); - - expect(Store.tryGet(StoreKey.betaTimeline), true); - expect(Store.tryGet(StoreKey.needBetaMigration), false); - }); - }); - - group("version > 15", () { - test('already on new timeline', () async { - await Store.put(StoreKey.betaTimeline, true); - - await handleBetaMigration(16, false, mockSyncStreamRepository); - - expect(Store.tryGet(StoreKey.betaTimeline), true); - expect(Store.tryGet(StoreKey.needBetaMigration), false); - }); - - test('already on old timeline', () async { - await Store.put(StoreKey.betaTimeline, false); - - await handleBetaMigration(16, false, mockSyncStreamRepository); - - expect(Store.tryGet(StoreKey.betaTimeline), false); - expect(Store.tryGet(StoreKey.needBetaMigration), false); - }); - - test('fresh install', () async { - await Store.delete(StoreKey.betaTimeline); - await handleBetaMigration(16, true, mockSyncStreamRepository); - - expect(Store.tryGet(StoreKey.betaTimeline), true); - expect(Store.tryGet(StoreKey.needBetaMigration), false); - }); - }); - }); - - group('sync reset tests', () { - test('version < 16', () async { - await Store.put(StoreKey.shouldResetSync, false); - - await handleBetaMigration(15, false, mockSyncStreamRepository); - - expect(Store.tryGet(StoreKey.shouldResetSync), true); - }); - - test('version >= 16', () async { - await Store.put(StoreKey.shouldResetSync, false); - - await handleBetaMigration(16, false, mockSyncStreamRepository); - - expect(Store.tryGet(StoreKey.shouldResetSync), false); - }); - }); -} diff --git a/mobile/test/modules/utils/throttler_test.dart b/mobile/test/modules/utils/throttler_test.dart deleted file mode 100644 index 1757826daf..0000000000 --- a/mobile/test/modules/utils/throttler_test.dart +++ /dev/null @@ -1,46 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:immich_mobile/utils/throttle.dart'; -import 'package:immich_mobile/utils/debug_print.dart'; - -class _Counter { - int _count = 0; - _Counter(); - - int get count => _count; - void increment() { - dPrint(() => "Counter inside increment: $count"); - _count = _count + 1; - } -} - -void main() { - test('Executes the method immediately if no calls received previously', () async { - var counter = _Counter(); - final throttler = Throttler(interval: const Duration(milliseconds: 300)); - throttler.run(() => counter.increment()); - expect(counter.count, 1); - }); - - test('Does not execute calls before throttle interval', () async { - var counter = _Counter(); - final throttler = Throttler(interval: const Duration(milliseconds: 100)); - throttler.run(() => counter.increment()); - throttler.run(() => counter.increment()); - throttler.run(() => counter.increment()); - throttler.run(() => counter.increment()); - throttler.run(() => counter.increment()); - await Future.delayed(const Duration(seconds: 1)); - expect(counter.count, 1); - }); - - test('Executes the method if received in intervals', () async { - var counter = _Counter(); - final throttler = Throttler(interval: const Duration(milliseconds: 100)); - for (final _ in Iterable.generate(10)) { - throttler.run(() => counter.increment()); - await Future.delayed(const Duration(milliseconds: 50)); - } - await Future.delayed(const Duration(seconds: 1)); - expect(counter.count, 5); - }); -} diff --git a/mobile/test/modules/utils/thumbnail_utils_test.dart b/mobile/test/modules/utils/thumbnail_utils_test.dart deleted file mode 100644 index dd4588fc80..0000000000 --- a/mobile/test/modules/utils/thumbnail_utils_test.dart +++ /dev/null @@ -1,63 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:immich_mobile/domain/models/exif.model.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/utils/thumbnail_utils.dart'; - -void main() { - final dateTime = DateTime(2025, 04, 25, 12, 13, 14); - final dateTimeString = DateFormat.yMMMMd().format(dateTime); - - test('returns description if it has one', () { - final result = getAltText(const ExifInfo(description: 'description'), dateTime, AssetType.image, []); - expect(result, 'description'); - }); - - test('returns image alt text with date if no location', () { - final (template, args) = getAltTextTemplate(const ExifInfo(), dateTime, AssetType.image, []); - expect(template, "image_alt_text_date"); - expect(args["isVideo"], "false"); - expect(args["date"], dateTimeString); - }); - - test('returns image alt text with date and place', () { - final (template, args) = getAltTextTemplate( - const ExifInfo(city: 'city', country: 'country'), - dateTime, - AssetType.video, - [], - ); - expect(template, "image_alt_text_date_place"); - expect(args["isVideo"], "true"); - expect(args["date"], dateTimeString); - expect(args["city"], "city"); - expect(args["country"], "country"); - }); - - test('returns image alt text with date and some people', () { - final (template, args) = getAltTextTemplate(const ExifInfo(), dateTime, AssetType.image, ["Alice", "Bob"]); - expect(template, "image_alt_text_date_2_people"); - expect(args["isVideo"], "false"); - expect(args["date"], dateTimeString); - expect(args["person1"], "Alice"); - expect(args["person2"], "Bob"); - }); - - test('returns image alt text with date and location and many people', () { - final (template, args) = getAltTextTemplate( - const ExifInfo(city: "city", country: 'country'), - dateTime, - AssetType.video, - ["Alice", "Bob", "Carol", "David", "Eve"], - ); - expect(template, "image_alt_text_date_place_4_or_more_people"); - expect(args["isVideo"], "true"); - expect(args["date"], dateTimeString); - expect(args["city"], "city"); - expect(args["country"], "country"); - expect(args["person1"], "Alice"); - expect(args["person2"], "Bob"); - expect(args["person3"], "Carol"); - expect(args["additionalCount"], "2"); - }); -} diff --git a/mobile/test/pages/search/search.page_test.dart b/mobile/test/pages/search/search.page_test.dart deleted file mode 100644 index 9592623a28..0000000000 --- a/mobile/test/pages/search/search.page_test.dart +++ /dev/null @@ -1,98 +0,0 @@ -@Skip('currently failing due to mock HTTP client to download ISAR binaries') -@Tags(['pages']) -library; - -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/domain/services/store.service.dart'; -import 'package:immich_mobile/infrastructure/repositories/store.repository.dart'; -import 'package:immich_mobile/pages/search/search.page.dart'; -import 'package:immich_mobile/providers/api.provider.dart'; -import 'package:immich_mobile/providers/db.provider.dart'; -import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; -import 'package:isar/isar.dart'; -import 'package:mocktail/mocktail.dart'; -import 'package:openapi/api.dart'; - -import '../../dto.mocks.dart'; -import '../../service.mocks.dart'; -import '../../test_utils.dart'; -import '../../widget_tester_extensions.dart'; - -void main() { - late List overrides; - late Isar db; - late MockApiService mockApiService; - late MockSearchApi mockSearchApi; - - setUpAll(() async { - TestUtils.init(); - db = await TestUtils.initIsar(); - await StoreService.init(storeRepository: IsarStoreRepository(db)); - mockApiService = MockApiService(); - mockSearchApi = MockSearchApi(); - when(() => mockApiService.searchApi).thenReturn(mockSearchApi); - registerFallbackValue(MockSmartSearchDto()); - registerFallbackValue(MockMetadataSearchDto()); - overrides = [ - dbProvider.overrideWithValue(db), - isarProvider.overrideWithValue(db), - apiServiceProvider.overrideWithValue(mockApiService), - ]; - }); - - final emptyTextSearch = isA().having((s) => s.originalFileName, 'originalFileName', null); - - testWidgets('contextual search with/without text', (tester) async { - await tester.pumpConsumerWidget(const SearchPage(), overrides: overrides); - - await tester.pumpAndSettle(); - - expect(find.byIcon(Icons.abc_rounded), findsOneWidget, reason: 'Should have contextual search icon'); - - final searchField = find.byKey(const Key('search_text_field')); - expect(searchField, findsOneWidget); - - await tester.enterText(searchField, 'test'); - await tester.testTextInput.receiveAction(TextInputAction.search); - - var captured = verify(() => mockSearchApi.searchSmart(captureAny())).captured; - - expect(captured.first, isA().having((s) => s.query, 'query', 'test')); - - await tester.enterText(searchField, ''); - await tester.testTextInput.receiveAction(TextInputAction.search); - - captured = verify(() => mockSearchApi.searchAssets(captureAny())).captured; - expect(captured.first, emptyTextSearch); - }); - - testWidgets('not contextual search with/without text', (tester) async { - await tester.pumpConsumerWidget(const SearchPage(), overrides: overrides); - - await tester.pumpAndSettle(); - - await tester.tap(find.byKey(const Key('contextual_search_button'))); - - await tester.pumpAndSettle(); - - expect(find.byIcon(Icons.image_search_rounded), findsOneWidget, reason: 'Should not have contextual search icon'); - - final searchField = find.byKey(const Key('search_text_field')); - expect(searchField, findsOneWidget); - - await tester.enterText(searchField, 'test'); - await tester.testTextInput.receiveAction(TextInputAction.search); - - var captured = verify(() => mockSearchApi.searchAssets(captureAny())).captured; - - expect(captured.first, isA().having((s) => s.originalFileName, 'originalFileName', 'test')); - - await tester.enterText(searchField, ''); - await tester.testTextInput.receiveAction(TextInputAction.search); - - captured = verify(() => mockSearchApi.searchAssets(captureAny())).captured; - expect(captured.first, emptyTextSearch); - }); -} diff --git a/mobile/test/repository.mocks.dart b/mobile/test/repository.mocks.dart index 4b54ec4055..d049626f1d 100644 --- a/mobile/test/repository.mocks.dart +++ b/mobile/test/repository.mocks.dart @@ -1,48 +1,16 @@ -import 'package:immich_mobile/infrastructure/repositories/exif.repository.dart'; -import 'package:immich_mobile/repositories/partner_api.repository.dart'; -import 'package:immich_mobile/repositories/album_media.repository.dart'; -import 'package:immich_mobile/repositories/album_api.repository.dart'; -import 'package:immich_mobile/repositories/partner.repository.dart'; -import 'package:immich_mobile/repositories/etag.repository.dart'; -import 'package:immich_mobile/repositories/local_files_manager.repository.dart'; -import 'package:immich_mobile/repositories/file_media.repository.dart'; -import 'package:immich_mobile/repositories/backup.repository.dart'; +import 'package:immich_mobile/repositories/asset_api.repository.dart'; +import 'package:immich_mobile/repositories/asset_media.repository.dart'; import 'package:immich_mobile/repositories/auth.repository.dart'; import 'package:immich_mobile/repositories/auth_api.repository.dart'; -import 'package:immich_mobile/repositories/asset.repository.dart'; -import 'package:immich_mobile/repositories/asset_media.repository.dart'; -import 'package:immich_mobile/repositories/album.repository.dart'; -import 'package:immich_mobile/repositories/asset_api.repository.dart'; +import 'package:immich_mobile/repositories/local_files_manager.repository.dart'; import 'package:mocktail/mocktail.dart'; -class MockAlbumRepository extends Mock implements AlbumRepository {} - -class MockAssetRepository extends Mock implements AssetRepository {} - -class MockBackupRepository extends Mock implements BackupAlbumRepository {} - -class MockExifInfoRepository extends Mock implements IsarExifRepository {} - -class MockETagRepository extends Mock implements ETagRepository {} - -class MockAlbumMediaRepository extends Mock implements AlbumMediaRepository {} - -class MockBackupAlbumRepository extends Mock implements BackupAlbumRepository {} - class MockAssetApiRepository extends Mock implements AssetApiRepository {} class MockAssetMediaRepository extends Mock implements AssetMediaRepository {} -class MockFileMediaRepository extends Mock implements FileMediaRepository {} - -class MockAlbumApiRepository extends Mock implements AlbumApiRepository {} - class MockAuthApiRepository extends Mock implements AuthApiRepository {} class MockAuthRepository extends Mock implements AuthRepository {} -class MockPartnerRepository extends Mock implements PartnerRepository {} - -class MockPartnerApiRepository extends Mock implements PartnerApiRepository {} - class MockLocalFilesManagerRepository extends Mock implements LocalFilesManagerRepository {} diff --git a/mobile/test/service.mocks.dart b/mobile/test/service.mocks.dart index 87a8c01cf0..4591dd845d 100644 --- a/mobile/test/service.mocks.dart +++ b/mobile/test/service.mocks.dart @@ -1,31 +1,10 @@ -import 'package:immich_mobile/services/album.service.dart'; import 'package:immich_mobile/services/api.service.dart'; import 'package:immich_mobile/services/app_settings.service.dart'; -import 'package:immich_mobile/services/background.service.dart'; -import 'package:immich_mobile/services/backup.service.dart'; -import 'package:immich_mobile/services/entity.service.dart'; -import 'package:immich_mobile/services/hash.service.dart'; import 'package:immich_mobile/services/network.service.dart'; -import 'package:immich_mobile/services/sync.service.dart'; import 'package:mocktail/mocktail.dart'; -import 'package:openapi/api.dart'; class MockApiService extends Mock implements ApiService {} -class MockAlbumService extends Mock implements AlbumService {} - -class MockBackupService extends Mock implements BackupService {} - -class MockSyncService extends Mock implements SyncService {} - -class MockHashService extends Mock implements HashService {} - -class MockEntityService extends Mock implements EntityService {} - class MockNetworkService extends Mock implements NetworkService {} -class MockSearchApi extends Mock implements SearchApi {} - class MockAppSettingService extends Mock implements AppSettingsService {} - -class MockBackgroundService extends Mock implements BackgroundService {} diff --git a/mobile/test/services/album.service_test.dart b/mobile/test/services/album.service_test.dart deleted file mode 100644 index 97683cdab1..0000000000 --- a/mobile/test/services/album.service_test.dart +++ /dev/null @@ -1,177 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:immich_mobile/entities/backup_album.entity.dart'; -import 'package:immich_mobile/services/album.service.dart'; -import 'package:mocktail/mocktail.dart'; - -import '../domain/service.mock.dart'; -import '../fixtures/album.stub.dart'; -import '../fixtures/asset.stub.dart'; -import '../fixtures/user.stub.dart'; -import '../repository.mocks.dart'; -import '../service.mocks.dart'; - -void main() { - late AlbumService sut; - late MockUserService userService; - late MockSyncService syncService; - late MockEntityService entityService; - late MockAlbumRepository albumRepository; - late MockAssetRepository assetRepository; - late MockBackupRepository backupRepository; - late MockAlbumMediaRepository albumMediaRepository; - late MockAlbumApiRepository albumApiRepository; - - setUp(() { - userService = MockUserService(); - syncService = MockSyncService(); - entityService = MockEntityService(); - albumRepository = MockAlbumRepository(); - assetRepository = MockAssetRepository(); - backupRepository = MockBackupRepository(); - albumMediaRepository = MockAlbumMediaRepository(); - albumApiRepository = MockAlbumApiRepository(); - - when(() => userService.getMyUser()).thenReturn(UserStub.user1); - - when( - () => albumRepository.transaction(any()), - ).thenAnswer((call) => (call.positionalArguments.first as Function).call()); - when( - () => assetRepository.transaction(any()), - ).thenAnswer((call) => (call.positionalArguments.first as Function).call()); - - sut = AlbumService( - syncService, - userService, - entityService, - albumRepository, - assetRepository, - backupRepository, - albumMediaRepository, - albumApiRepository, - ); - }); - - group('refreshDeviceAlbums', () { - test('empty selection with one album in db', () async { - when(() => backupRepository.getIdsBySelection(BackupSelection.exclude)).thenAnswer((_) async => []); - when(() => backupRepository.getIdsBySelection(BackupSelection.select)).thenAnswer((_) async => []); - when(() => albumMediaRepository.getAll()).thenAnswer((_) async => []); - when(() => albumRepository.count(local: true)).thenAnswer((_) async => 1); - when(() => syncService.removeAllLocalAlbumsAndAssets()).thenAnswer((_) async => true); - final result = await sut.refreshDeviceAlbums(); - expect(result, false); - verify(() => syncService.removeAllLocalAlbumsAndAssets()); - }); - - test('one selected albums, two on device', () async { - when(() => backupRepository.getIdsBySelection(BackupSelection.exclude)).thenAnswer((_) async => []); - when( - () => backupRepository.getIdsBySelection(BackupSelection.select), - ).thenAnswer((_) async => [AlbumStub.oneAsset.localId!]); - when(() => albumMediaRepository.getAll()).thenAnswer((_) async => [AlbumStub.oneAsset, AlbumStub.twoAsset]); - when(() => syncService.syncLocalAlbumAssetsToDb(any(), any())).thenAnswer((_) async => true); - final result = await sut.refreshDeviceAlbums(); - expect(result, true); - verify(() => syncService.syncLocalAlbumAssetsToDb([AlbumStub.oneAsset], null)).called(1); - verifyNoMoreInteractions(syncService); - }); - }); - - group('refreshRemoteAlbums', () { - test('is working', () async { - when(() => syncService.getUsersFromServer()).thenAnswer((_) async => []); - when(() => syncService.syncUsersFromServer(any())).thenAnswer((_) async => true); - when(() => albumApiRepository.getAll(shared: true)).thenAnswer((_) async => [AlbumStub.sharedWithUser]); - - when( - () => albumApiRepository.getAll(shared: null), - ).thenAnswer((_) async => [AlbumStub.oneAsset, AlbumStub.twoAsset]); - - when( - () => syncService.syncRemoteAlbumsToDb([AlbumStub.twoAsset, AlbumStub.oneAsset, AlbumStub.sharedWithUser]), - ).thenAnswer((_) async => true); - final result = await sut.refreshRemoteAlbums(); - expect(result, true); - verify(() => syncService.getUsersFromServer()).called(1); - verify(() => syncService.syncUsersFromServer([])).called(1); - verify(() => albumApiRepository.getAll(shared: true)).called(1); - verify(() => albumApiRepository.getAll(shared: null)).called(1); - verify( - () => syncService.syncRemoteAlbumsToDb([AlbumStub.twoAsset, AlbumStub.oneAsset, AlbumStub.sharedWithUser]), - ).called(1); - verifyNoMoreInteractions(userService); - verifyNoMoreInteractions(albumApiRepository); - verifyNoMoreInteractions(syncService); - }); - }); - - group('createAlbum', () { - test('shared with assets', () async { - when( - () => albumApiRepository.create( - "name", - assetIds: any(named: "assetIds"), - sharedUserIds: any(named: "sharedUserIds"), - ), - ).thenAnswer((_) async => AlbumStub.oneAsset); - - when( - () => entityService.fillAlbumWithDatabaseEntities(AlbumStub.oneAsset), - ).thenAnswer((_) async => AlbumStub.oneAsset); - - when(() => albumRepository.create(AlbumStub.oneAsset)).thenAnswer((_) async => AlbumStub.twoAsset); - - final result = await sut.createAlbum("name", [AssetStub.image1], [UserStub.user1]); - expect(result, AlbumStub.twoAsset); - verify( - () => albumApiRepository.create( - "name", - assetIds: [AssetStub.image1.remoteId!], - sharedUserIds: [UserStub.user1.id], - ), - ).called(1); - verify(() => entityService.fillAlbumWithDatabaseEntities(AlbumStub.oneAsset)).called(1); - }); - }); - - group('addAdditionalAssetToAlbum', () { - test('one added, one duplicate', () async { - when( - () => albumApiRepository.addAssets(AlbumStub.oneAsset.remoteId!, any()), - ).thenAnswer((_) async => (added: [AssetStub.image2.remoteId!], duplicates: [AssetStub.image1.remoteId!])); - when(() => albumRepository.get(AlbumStub.oneAsset.id)).thenAnswer((_) async => AlbumStub.oneAsset); - when(() => albumRepository.addAssets(AlbumStub.oneAsset, [AssetStub.image2])).thenAnswer((_) async {}); - when(() => albumRepository.removeAssets(AlbumStub.oneAsset, [])).thenAnswer((_) async {}); - when(() => albumRepository.recalculateMetadata(AlbumStub.oneAsset)).thenAnswer((_) async => AlbumStub.oneAsset); - when(() => albumRepository.update(AlbumStub.oneAsset)).thenAnswer((_) async => AlbumStub.oneAsset); - - final result = await sut.addAssets(AlbumStub.oneAsset, [AssetStub.image1, AssetStub.image2]); - - expect(result != null, true); - expect(result!.alreadyInAlbum, [AssetStub.image1.remoteId!]); - expect(result.successfullyAdded, 1); - }); - }); - - group('addAdditionalUserToAlbum', () { - test('one added', () async { - when( - () => albumApiRepository.addUsers(AlbumStub.emptyAlbum.remoteId!, any()), - ).thenAnswer((_) async => AlbumStub.sharedWithUser); - - when( - () => albumRepository.addUsers( - AlbumStub.emptyAlbum, - AlbumStub.emptyAlbum.sharedUsers.map((u) => u.toDto()).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); - }); - }); -} diff --git a/mobile/test/services/asset.service_test.dart b/mobile/test/services/asset.service_test.dart deleted file mode 100644 index b741150165..0000000000 --- a/mobile/test/services/asset.service_test.dart +++ /dev/null @@ -1,103 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/services/asset.service.dart'; -import 'package:maplibre_gl/maplibre_gl.dart'; -import 'package:mocktail/mocktail.dart'; -import 'package:openapi/api.dart'; - -import '../api.mocks.dart'; -import '../domain/service.mock.dart'; -import '../fixtures/asset.stub.dart'; -import '../infrastructure/repository.mock.dart'; -import '../repository.mocks.dart'; -import '../service.mocks.dart'; - -class FakeAssetBulkUpdateDto extends Fake implements AssetBulkUpdateDto {} - -void main() { - late AssetService sut; - - late MockAssetRepository assetRepository; - late MockAssetApiRepository assetApiRepository; - late MockExifInfoRepository exifInfoRepository; - late MockETagRepository eTagRepository; - late MockBackupAlbumRepository backupAlbumRepository; - late MockIsarUserRepository userRepository; - late MockAssetMediaRepository assetMediaRepository; - late MockApiService apiService; - - late MockSyncService syncService; - late MockAlbumService albumService; - late MockBackupService backupService; - late MockUserService userService; - - setUp(() { - assetRepository = MockAssetRepository(); - assetApiRepository = MockAssetApiRepository(); - exifInfoRepository = MockExifInfoRepository(); - userRepository = MockIsarUserRepository(); - eTagRepository = MockETagRepository(); - backupAlbumRepository = MockBackupAlbumRepository(); - apiService = MockApiService(); - assetMediaRepository = MockAssetMediaRepository(); - - syncService = MockSyncService(); - userService = MockUserService(); - albumService = MockAlbumService(); - backupService = MockBackupService(); - - sut = AssetService( - assetApiRepository, - assetRepository, - exifInfoRepository, - userRepository, - eTagRepository, - backupAlbumRepository, - apiService, - syncService, - backupService, - albumService, - userService, - assetMediaRepository, - ); - - registerFallbackValue(FakeAssetBulkUpdateDto()); - }); - - group("Edit ExifInfo", () { - late AssetsApi assetsApi; - setUp(() { - assetsApi = MockAssetsApi(); - when(() => apiService.assetsApi).thenReturn(assetsApi); - when(() => assetsApi.updateAssets(any())).thenAnswer((_) async => Future.value()); - }); - - test("asset is updated with DateTime", () async { - final assets = [AssetStub.image1, AssetStub.image2]; - final dateTime = DateTime.utc(2025, 6, 4, 2, 57); - await sut.changeDateTime(assets, dateTime.toIso8601String()); - - verify(() => assetsApi.updateAssets(any())).called(1); - final upsertExifCallback = verify(() => syncService.upsertAssetsWithExif(captureAny())); - upsertExifCallback.called(1); - final receivedAssets = upsertExifCallback.captured.firstOrNull as List? ?? []; - final receivedDatetime = receivedAssets.cast().map((a) => a.exifInfo?.dateTimeOriginal ?? DateTime(0)); - expect(receivedDatetime.every((d) => d == dateTime), isTrue); - }); - - test("asset is updated with LatLng", () async { - final assets = [AssetStub.image1, AssetStub.image2]; - final latLng = const LatLng(37.7749, -122.4194); - await sut.changeLocation(assets, latLng); - - verify(() => assetsApi.updateAssets(any())).called(1); - final upsertExifCallback = verify(() => syncService.upsertAssetsWithExif(captureAny())); - upsertExifCallback.called(1); - final receivedAssets = upsertExifCallback.captured.firstOrNull as List? ?? []; - final receivedCoords = receivedAssets.cast().map( - (a) => LatLng(a.exifInfo?.latitude ?? 0, a.exifInfo?.longitude ?? 0), - ); - expect(receivedCoords.every((l) => l == latLng), isTrue); - }); - }); -} diff --git a/mobile/test/services/auth.service_test.dart b/mobile/test/services/auth.service_test.dart index 7c7de3cd0e..f9a6d5e282 100644 --- a/mobile/test/services/auth.service_test.dart +++ b/mobile/test/services/auth.service_test.dart @@ -1,18 +1,19 @@ +import 'package:drift/drift.dart' hide isNull; +import 'package:drift/native.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:immich_mobile/domain/services/store.service.dart'; +import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/store.repository.dart'; import 'package:immich_mobile/models/auth/auxilary_endpoint.model.dart'; import 'package:immich_mobile/services/app_settings.service.dart'; import 'package:immich_mobile/services/auth.service.dart'; -import 'package:isar/isar.dart'; import 'package:mocktail/mocktail.dart'; import 'package:openapi/api.dart'; import '../domain/service.mock.dart'; import '../repository.mocks.dart'; import '../service.mocks.dart'; -import '../test_utils.dart'; void main() { late AuthService sut; @@ -22,7 +23,7 @@ void main() { late MockNetworkService networkService; late MockBackgroundSyncManager backgroundSyncManager; late MockAppSettingService appSettingsService; - late Isar db; + late Drift db; setUp(() async { authApiRepository = MockAuthApiRepository(); @@ -45,19 +46,16 @@ void main() { }); setUpAll(() async { - db = await TestUtils.initIsar(); - db.writeTxnSync(() => db.clearSync()); - await StoreService.init(storeRepository: IsarStoreRepository(db)); + WidgetsFlutterBinding.ensureInitialized(); + db = Drift(DatabaseConnection(NativeDatabase.memory(), closeStreamsSynchronously: true)); + await StoreService.init(storeRepository: DriftStoreRepository(db)); + }); + + tearDownAll(() async { + await db.close(); }); group('validateServerUrl', () { - setUpAll(() async { - WidgetsFlutterBinding.ensureInitialized(); - final db = await TestUtils.initIsar(); - db.writeTxnSync(() => db.clearSync()); - await StoreService.init(storeRepository: IsarStoreRepository(db)); - }); - test('Should resolve HTTP endpoint', () async { const testUrl = 'http://ip:2283'; const resolvedUrl = 'http://ip:2283/api'; diff --git a/mobile/test/services/entity.service_test.dart b/mobile/test/services/entity.service_test.dart deleted file mode 100644 index 64b9fc604b..0000000000 --- a/mobile/test/services/entity.service_test.dart +++ /dev/null @@ -1,76 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:immich_mobile/entities/album.entity.dart'; -import 'package:immich_mobile/infrastructure/entities/user.entity.dart'; -import 'package:immich_mobile/services/entity.service.dart'; -import 'package:mocktail/mocktail.dart'; - -import '../fixtures/asset.stub.dart'; -import '../fixtures/user.stub.dart'; -import '../infrastructure/repository.mock.dart'; -import '../repository.mocks.dart'; - -void main() { - late EntityService sut; - late MockAssetRepository assetRepository; - late MockIsarUserRepository userRepository; - - setUp(() { - assetRepository = MockAssetRepository(); - userRepository = MockIsarUserRepository(); - sut = EntityService(assetRepository, userRepository); - }); - - group('fillAlbumWithDatabaseEntities', () { - test('remote album with owner, thumbnail, sharedUsers and assets', () async { - final Album album = - Album( - name: "album-with-two-assets-and-two-users", - localId: "album-with-two-assets-and-two-users-local", - remoteId: "album-with-two-assets-and-two-users-remote", - createdAt: DateTime(2001), - modifiedAt: DateTime(2010), - shared: true, - activityEnabled: true, - startDate: DateTime(2019), - endDate: DateTime(2020), - ) - ..remoteThumbnailAssetId = AssetStub.image1.remoteId - ..assets.addAll([AssetStub.image1, AssetStub.image1]) - ..owner.value = User.fromDto(UserStub.user1) - ..sharedUsers.addAll([User.fromDto(UserStub.admin), User.fromDto(UserStub.admin)]); - - when(() => userRepository.getByUserId(any())).thenAnswer((_) async => UserStub.admin); - when(() => userRepository.getByUserId(any())).thenAnswer((_) async => UserStub.admin); - - when(() => assetRepository.getByRemoteId(AssetStub.image1.remoteId!)).thenAnswer((_) async => AssetStub.image1); - - when(() => userRepository.getByUserIds(any())).thenAnswer((_) async => [UserStub.user1, UserStub.user2]); - - when(() => assetRepository.getAllByRemoteId(any())).thenAnswer((_) async => [AssetStub.image1, AssetStub.image2]); - - await sut.fillAlbumWithDatabaseEntities(album); - expect(album.owner.value?.toDto(), UserStub.admin); - expect(album.thumbnail.value, AssetStub.image1); - expect(album.remoteUsers.map((u) => u.toDto()).toSet(), {UserStub.user1, UserStub.user2}); - expect(album.remoteAssets.toSet(), {AssetStub.image1, AssetStub.image2}); - }); - - test('remote album without any info', () async { - makeEmptyAlbum() => Album( - name: "album-without-info", - localId: "album-without-info-local", - remoteId: "album-without-info-remote", - createdAt: DateTime(2001), - modifiedAt: DateTime(2010), - shared: false, - activityEnabled: false, - ); - - final album = makeEmptyAlbum(); - await sut.fillAlbumWithDatabaseEntities(album); - verifyNoMoreInteractions(assetRepository); - verifyNoMoreInteractions(userRepository); - expect(album, makeEmptyAlbum()); - }); - }); -} diff --git a/mobile/test/services/hash_service_test.dart b/mobile/test/services/hash_service_test.dart deleted file mode 100644 index 9429d434b0..0000000000 --- a/mobile/test/services/hash_service_test.dart +++ /dev/null @@ -1,349 +0,0 @@ -import 'dart:convert'; -import 'dart:io'; -import 'dart:math'; - -import 'package:collection/collection.dart'; -import 'package:file/memory.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:immich_mobile/domain/models/device_asset.model.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/infrastructure/repositories/device_asset.repository.dart'; -import 'package:immich_mobile/services/background.service.dart'; -import 'package:immich_mobile/services/hash.service.dart'; -import 'package:mocktail/mocktail.dart'; - -import '../fixtures/asset.stub.dart'; -import '../infrastructure/repository.mock.dart'; -import '../service.mocks.dart'; -import '../mocks/asset_entity.mock.dart'; - -class MockAsset extends Mock implements Asset {} - -void main() { - late HashService sut; - late BackgroundService mockBackgroundService; - late IsarDeviceAssetRepository mockDeviceAssetRepository; - - setUp(() { - mockBackgroundService = MockBackgroundService(); - mockDeviceAssetRepository = MockDeviceAssetRepository(); - - sut = HashService(deviceAssetRepository: mockDeviceAssetRepository, backgroundService: mockBackgroundService); - - when(() => mockDeviceAssetRepository.transaction(any())).thenAnswer((_) async { - final capturedCallback = verify(() => mockDeviceAssetRepository.transaction(captureAny())).captured; - // Invoke the transaction callback - await (capturedCallback.firstOrNull as Future Function()?)?.call(); - }); - when(() => mockDeviceAssetRepository.updateAll(any())).thenAnswer((_) async => true); - when(() => mockDeviceAssetRepository.deleteIds(any())).thenAnswer((_) async => true); - }); - - group("HashService: No DeviceAsset entry", () { - test("hash successfully", () async { - final (mockAsset, file, deviceAsset, hash) = await _createAssetMock(AssetStub.image1); - - when(() => mockBackgroundService.digestFiles([file.path])).thenAnswer((_) async => [hash]); - // No DB entries for this asset - when(() => mockDeviceAssetRepository.getByIds([AssetStub.image1.localId!])).thenAnswer((_) async => []); - - final result = await sut.hashAssets([mockAsset]); - - // Verify we stored the new hash in DB - when(() => mockDeviceAssetRepository.transaction(any())).thenAnswer((_) async { - final capturedCallback = verify(() => mockDeviceAssetRepository.transaction(captureAny())).captured; - // Invoke the transaction callback - await (capturedCallback.firstOrNull as Future Function()?)?.call(); - verify( - () => mockDeviceAssetRepository.updateAll([ - deviceAsset.copyWith(modifiedTime: AssetStub.image1.fileModifiedAt), - ]), - ).called(1); - verify(() => mockDeviceAssetRepository.deleteIds([])).called(1); - }); - expect(result, [AssetStub.image1.copyWith(checksum: base64.encode(hash))]); - }); - }); - - group("HashService: Has DeviceAsset entry", () { - test("when the asset is not modified", () async { - final hash = utf8.encode("image1-hash"); - - when(() => mockDeviceAssetRepository.getByIds([AssetStub.image1.localId!])).thenAnswer( - (_) async => [ - DeviceAsset(assetId: AssetStub.image1.localId!, hash: hash, modifiedTime: AssetStub.image1.fileModifiedAt), - ], - ); - final result = await sut.hashAssets([AssetStub.image1]); - - verifyNever(() => mockBackgroundService.digestFiles(any())); - verifyNever(() => mockBackgroundService.digestFile(any())); - verifyNever(() => mockDeviceAssetRepository.updateAll(any())); - verifyNever(() => mockDeviceAssetRepository.deleteIds(any())); - - expect(result, [AssetStub.image1.copyWith(checksum: base64.encode(hash))]); - }); - - test("hashed successful when asset is modified", () async { - final (mockAsset, file, deviceAsset, hash) = await _createAssetMock(AssetStub.image1); - - when(() => mockBackgroundService.digestFiles([file.path])).thenAnswer((_) async => [hash]); - when( - () => mockDeviceAssetRepository.getByIds([AssetStub.image1.localId!]), - ).thenAnswer((_) async => [deviceAsset]); - - final result = await sut.hashAssets([mockAsset]); - - when(() => mockDeviceAssetRepository.transaction(any())).thenAnswer((_) async { - final capturedCallback = verify(() => mockDeviceAssetRepository.transaction(captureAny())).captured; - // Invoke the transaction callback - await (capturedCallback.firstOrNull as Future Function()?)?.call(); - verify( - () => mockDeviceAssetRepository.updateAll([ - deviceAsset.copyWith(modifiedTime: AssetStub.image1.fileModifiedAt), - ]), - ).called(1); - verify(() => mockDeviceAssetRepository.deleteIds([])).called(1); - }); - - verify(() => mockBackgroundService.digestFiles([file.path])).called(1); - - expect(result, [AssetStub.image1.copyWith(checksum: base64.encode(hash))]); - }); - }); - - group("HashService: Cleanup", () { - late Asset mockAsset; - late Uint8List hash; - late DeviceAsset deviceAsset; - late File file; - - setUp(() async { - (mockAsset, file, deviceAsset, hash) = await _createAssetMock(AssetStub.image1); - - when(() => mockBackgroundService.digestFiles([file.path])).thenAnswer((_) async => [hash]); - when( - () => mockDeviceAssetRepository.getByIds([AssetStub.image1.localId!]), - ).thenAnswer((_) async => [deviceAsset]); - }); - - test("cleanups DeviceAsset when local file cannot be obtained", () async { - when(() => mockAsset.local).thenThrow(Exception("File not found")); - final result = await sut.hashAssets([mockAsset]); - - verifyNever(() => mockBackgroundService.digestFiles(any())); - verifyNever(() => mockBackgroundService.digestFile(any())); - verifyNever(() => mockDeviceAssetRepository.updateAll(any())); - verify(() => mockDeviceAssetRepository.deleteIds([AssetStub.image1.localId!])).called(1); - - expect(result, isEmpty); - }); - - test("cleanups DeviceAsset when hashing failed", () async { - when(() => mockDeviceAssetRepository.transaction(any())).thenAnswer((_) async { - final capturedCallback = verify(() => mockDeviceAssetRepository.transaction(captureAny())).captured; - // Invoke the transaction callback - await (capturedCallback.firstOrNull as Future Function()?)?.call(); - - // Verify the callback inside the transaction because, doing it outside results - // in a small delay before the callback is invoked, resulting in other LOCs getting executed - // resulting in an incorrect state - // - // i.e, consider the following piece of code - // await _deviceAssetRepository.transaction(() async { - // await _deviceAssetRepository.updateAll(toBeAdded); - // await _deviceAssetRepository.deleteIds(toBeDeleted); - // }); - // toBeDeleted.clear(); - // since the transaction method is mocked, the callback is not invoked until it is captured - // and executed manually in the next event loop. However, the toBeDeleted.clear() is executed - // immediately once the transaction stub is executed, resulting in the deleteIds method being - // called with an empty list. - // - // To avoid this, we capture the callback and execute it within the transaction stub itself - // and verify the results inside the transaction stub - verify(() => mockDeviceAssetRepository.updateAll([])).called(1); - verify(() => mockDeviceAssetRepository.deleteIds([AssetStub.image1.localId!])).called(1); - }); - - when(() => mockBackgroundService.digestFiles([file.path])).thenAnswer( - // Invalid hash, length != 20 - (_) async => [Uint8List.fromList(hash.slice(2).toList())], - ); - - final result = await sut.hashAssets([mockAsset]); - - verify(() => mockBackgroundService.digestFiles([file.path])).called(1); - expect(result, isEmpty); - }); - }); - - group("HashService: Batch processing", () { - test("processes assets in batches when size limit is reached", () async { - // Setup multiple assets with large file sizes - final (mock1, mock2, mock3) = await ( - _createAssetMock(AssetStub.image1), - _createAssetMock(AssetStub.image2), - _createAssetMock(AssetStub.image3), - ).wait; - - final (asset1, file1, deviceAsset1, hash1) = mock1; - final (asset2, file2, deviceAsset2, hash2) = mock2; - final (asset3, file3, deviceAsset3, hash3) = mock3; - - when(() => mockDeviceAssetRepository.getByIds(any())).thenAnswer((_) async => []); - - // Setup for multiple batch processing calls - when(() => mockBackgroundService.digestFiles([file1.path, file2.path])).thenAnswer((_) async => [hash1, hash2]); - when(() => mockBackgroundService.digestFiles([file3.path])).thenAnswer((_) async => [hash3]); - - final size = await file1.length() + await file2.length(); - - sut = HashService( - deviceAssetRepository: mockDeviceAssetRepository, - backgroundService: mockBackgroundService, - batchSizeLimit: size, - ); - final result = await sut.hashAssets([asset1, asset2, asset3]); - - // Verify multiple batch process calls - verify(() => mockBackgroundService.digestFiles([file1.path, file2.path])).called(1); - verify(() => mockBackgroundService.digestFiles([file3.path])).called(1); - - expect(result, [ - AssetStub.image1.copyWith(checksum: base64.encode(hash1)), - AssetStub.image2.copyWith(checksum: base64.encode(hash2)), - AssetStub.image3.copyWith(checksum: base64.encode(hash3)), - ]); - }); - - test("processes assets in batches when file limit is reached", () async { - // Setup multiple assets with large file sizes - final (mock1, mock2, mock3) = await ( - _createAssetMock(AssetStub.image1), - _createAssetMock(AssetStub.image2), - _createAssetMock(AssetStub.image3), - ).wait; - - final (asset1, file1, deviceAsset1, hash1) = mock1; - final (asset2, file2, deviceAsset2, hash2) = mock2; - final (asset3, file3, deviceAsset3, hash3) = mock3; - - when(() => mockDeviceAssetRepository.getByIds(any())).thenAnswer((_) async => []); - - when(() => mockBackgroundService.digestFiles([file1.path])).thenAnswer((_) async => [hash1]); - when(() => mockBackgroundService.digestFiles([file2.path])).thenAnswer((_) async => [hash2]); - when(() => mockBackgroundService.digestFiles([file3.path])).thenAnswer((_) async => [hash3]); - - sut = HashService( - deviceAssetRepository: mockDeviceAssetRepository, - backgroundService: mockBackgroundService, - batchFileLimit: 1, - ); - final result = await sut.hashAssets([asset1, asset2, asset3]); - - // Verify multiple batch process calls - verify(() => mockBackgroundService.digestFiles([file1.path])).called(1); - verify(() => mockBackgroundService.digestFiles([file2.path])).called(1); - verify(() => mockBackgroundService.digestFiles([file3.path])).called(1); - - expect(result, [ - AssetStub.image1.copyWith(checksum: base64.encode(hash1)), - AssetStub.image2.copyWith(checksum: base64.encode(hash2)), - AssetStub.image3.copyWith(checksum: base64.encode(hash3)), - ]); - }); - - test("HashService: Sort & Process different states", () async { - final (asset1, file1, deviceAsset1, hash1) = await _createAssetMock(AssetStub.image1); // Will need rehashing - final (asset2, file2, deviceAsset2, hash2) = await _createAssetMock(AssetStub.image2); // Will have matching hash - final (asset3, file3, deviceAsset3, hash3) = await _createAssetMock(AssetStub.image3); // No DB entry - final asset4 = AssetStub.image3.copyWith(localId: "image4"); // Cannot be hashed - - when(() => mockBackgroundService.digestFiles([file1.path, file3.path])).thenAnswer((_) async => [hash1, hash3]); - // DB entries are not sorted and a dummy entry added - when( - () => mockDeviceAssetRepository.getByIds([ - AssetStub.image1.localId!, - AssetStub.image2.localId!, - AssetStub.image3.localId!, - asset4.localId!, - ]), - ).thenAnswer( - (_) async => [ - // Same timestamp to reuse deviceAsset - deviceAsset2.copyWith(modifiedTime: asset2.fileModifiedAt), - deviceAsset1, - deviceAsset3.copyWith(assetId: asset4.localId!), - ], - ); - - final result = await sut.hashAssets([asset1, asset2, asset3, asset4]); - - // Verify correct processing of all assets - verify(() => mockBackgroundService.digestFiles([file1.path, file3.path])).called(1); - expect(result.length, 3); - expect(result, [ - AssetStub.image2.copyWith(checksum: base64.encode(hash2)), - AssetStub.image1.copyWith(checksum: base64.encode(hash1)), - AssetStub.image3.copyWith(checksum: base64.encode(hash3)), - ]); - }); - - group("HashService: Edge cases", () { - test("handles empty list of assets", () async { - when(() => mockDeviceAssetRepository.getByIds(any())).thenAnswer((_) async => []); - - final result = await sut.hashAssets([]); - - verifyNever(() => mockBackgroundService.digestFiles(any())); - verifyNever(() => mockDeviceAssetRepository.updateAll(any())); - verifyNever(() => mockDeviceAssetRepository.deleteIds(any())); - - expect(result, isEmpty); - }); - - test("handles all file access failures", () async { - // No DB entries - when( - () => mockDeviceAssetRepository.getByIds([AssetStub.image1.localId!, AssetStub.image2.localId!]), - ).thenAnswer((_) async => []); - - final result = await sut.hashAssets([AssetStub.image1, AssetStub.image2]); - - verifyNever(() => mockBackgroundService.digestFiles(any())); - verifyNever(() => mockDeviceAssetRepository.updateAll(any())); - expect(result, isEmpty); - }); - }); - }); -} - -Future<(Asset, File, DeviceAsset, Uint8List)> _createAssetMock(Asset asset) async { - final random = Random(); - final hash = Uint8List.fromList(List.generate(20, (i) => random.nextInt(255))); - final mockAsset = MockAsset(); - final mockAssetEntity = MockAssetEntity(); - final fs = MemoryFileSystem(); - final deviceAsset = DeviceAsset( - assetId: asset.localId!, - hash: Uint8List.fromList(hash), - modifiedTime: DateTime.now(), - ); - final tmp = await fs.systemTempDirectory.createTemp(); - final file = tmp.childFile("${asset.fileName}-path"); - await file.writeAsString("${asset.fileName}-content"); - - when(() => mockAsset.localId).thenReturn(asset.localId); - when(() => mockAsset.fileName).thenReturn(asset.fileName); - when(() => mockAsset.fileCreatedAt).thenReturn(asset.fileCreatedAt); - when(() => mockAsset.fileModifiedAt).thenReturn(asset.fileModifiedAt); - when( - () => mockAsset.copyWith(checksum: any(named: "checksum")), - ).thenReturn(asset.copyWith(checksum: base64.encode(hash))); - when(() => mockAsset.local).thenAnswer((_) => mockAssetEntity); - when(() => mockAssetEntity.originFile).thenAnswer((_) async => file); - - return (mockAsset, file, deviceAsset, hash); -} diff --git a/mobile/test/test_utils.dart b/mobile/test/test_utils.dart index 30d4e2e6d4..75a41b46fb 100644 --- a/mobile/test/test_utils.dart +++ b/mobile/test/test_utils.dart @@ -4,82 +4,13 @@ import 'dart:io'; import 'package:easy_localization/easy_localization.dart'; import 'package:fake_async/fake_async.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart' as domain; -import 'package:immich_mobile/entities/album.entity.dart'; -import 'package:immich_mobile/entities/android_device_asset.entity.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/entities/backup_album.entity.dart'; -import 'package:immich_mobile/entities/duplicated_asset.entity.dart'; -import 'package:immich_mobile/entities/etag.entity.dart'; -import 'package:immich_mobile/entities/ios_device_asset.entity.dart'; -import 'package:immich_mobile/infrastructure/entities/device_asset.entity.dart'; -import 'package:immich_mobile/infrastructure/entities/exif.entity.dart'; -import 'package:immich_mobile/infrastructure/entities/store.entity.dart'; -import 'package:immich_mobile/infrastructure/entities/user.entity.dart'; -import 'package:isar/isar.dart'; -import 'package:mocktail/mocktail.dart'; import 'mock_http_override.dart'; -// Listener Mock to test when a provider notifies its listeners -class ListenerMock extends Mock { - void call(T? previous, T next); -} - abstract final class TestUtils { const TestUtils._(); - /// Downloads Isar binaries (if required) and initializes a new Isar db - static Future initIsar() async { - await Isar.initializeIsarCore(download: true); - - final instance = Isar.getInstance(); - if (instance != null) { - return instance; - } - - final db = await Isar.open( - [ - StoreValueSchema, - ExifInfoSchema, - AssetSchema, - AlbumSchema, - UserSchema, - BackupAlbumSchema, - DuplicatedAssetSchema, - ETagSchema, - AndroidDeviceAssetSchema, - IOSDeviceAssetSchema, - DeviceAssetEntitySchema, - ], - directory: "test/", - maxSizeMiB: 1024, - inspector: false, - ); - - // Clear and close db on test end - addTearDown(() async { - await db.writeTxn(() async => await db.clear()); - await db.close(); - }); - return db; - } - - /// Creates a new ProviderContainer to test Riverpod providers - static ProviderContainer createContainer({ - ProviderContainer? parent, - List overrides = const [], - List? observers, - }) { - final container = ProviderContainer(parent: parent, overrides: overrides, observers: observers); - - // Dispose on test end - addTearDown(container.dispose); - - return container; - } - static void init() { // Turn off easy localization logging EasyLocalization.logger.enableBuildModes = []; diff --git a/mobile/test/test_utils/medium_factory.dart b/mobile/test/test_utils/medium_factory.dart index 50e73e5b5e..c8c41bbf0f 100644 --- a/mobile/test/test_utils/medium_factory.dart +++ b/mobile/test/test_utils/medium_factory.dart @@ -1,7 +1,6 @@ import 'dart:math'; import 'package:immich_mobile/domain/models/album/local_album.model.dart'; -import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/local_album.repository.dart'; @@ -10,28 +9,6 @@ class MediumFactory { const MediumFactory(Drift db) : _db = db; - LocalAsset localAsset({ - String? id, - String? name, - AssetType? type, - DateTime? createdAt, - DateTime? updatedAt, - String? checksum, - }) { - final random = Random(); - - return LocalAsset( - id: id ?? '${random.nextInt(1000000)}', - name: name ?? 'Asset ${random.nextInt(1000000)}', - checksum: checksum ?? '${random.nextInt(1000000)}', - type: type ?? AssetType.image, - createdAt: createdAt ?? DateTime.fromMillisecondsSinceEpoch(random.nextInt(1000000000)), - updatedAt: updatedAt ?? DateTime.fromMillisecondsSinceEpoch(random.nextInt(1000000000)), - playbackStyle: AssetPlaybackStyle.image, - isEdited: false, - ); - } - LocalAlbum localAlbum({ String? id, String? name,