You've already forked immich
							
							
				mirror of
				https://github.com/immich-app/immich.git
				synced 2025-10-31 00:18:28 +02:00 
			
		
		
		
	Refactor mobile to use OpenApi generated SDK (#336)
This commit is contained in:
		
							
								
								
									
										11
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										11
									
								
								README.md
									
									
									
									
									
								
							| @@ -161,7 +161,7 @@ To *update* docker-compose with newest image (if you have started the docker-com | |||||||
| docker-compose -f ./docker/docker-compose.yml pull && docker-compose -f ./docker/docker-compose.yml up | docker-compose -f ./docker/docker-compose.yml pull && docker-compose -f ./docker/docker-compose.yml up | ||||||
| ``` | ``` | ||||||
|  |  | ||||||
| The server will be running at `http://your-ip:2283/api` through `Nginx` | The server will be running at `http://your-ip:2283/api` | ||||||
|  |  | ||||||
| ## Step 3: Register User | ## Step 3: Register User | ||||||
|  |  | ||||||
| @@ -225,6 +225,15 @@ make dev # required Makefile installed on the system. | |||||||
|  |  | ||||||
| All servers and web container are hot reload for quick feedback loop. | All servers and web container are hot reload for quick feedback loop. | ||||||
|  |  | ||||||
|  | ## Note for developers | ||||||
|  | ### 1 - OpenAPI | ||||||
|  | OpenAPI is used to generate the client (Typescript, Dart) SDK. `openapi-generator-cli` can be installed [here](https://openapi-generator.tech/docs/installation/). When you add a new or modify an existing endpoint, you must run the generate command below to update the client SDK. | ||||||
|  |  | ||||||
|  | ```bash | ||||||
|  | npm run api:generate # Run from server directory | ||||||
|  | ``` | ||||||
|  | You can find the generated client SDK in the [`web/src/api`](web/src/api) for Typescript SDK and [`mobile/openapi`](mobile/openapi) for Dart SDK. | ||||||
|  |  | ||||||
| # Support | # Support | ||||||
|  |  | ||||||
| If you like the app, find it helpful, and want to support me to offset the cost of publishing to AppStores, you can sponsor the project with [**Github Sponsor**](https://github.com/sponsors/alextran1502), or a one time donation with the Buy Me a coffee link below. | If you like the app, find it helpful, and want to support me to offset the cost of publishing to AppStores, you can sponsor the project with [**Github Sponsor**](https://github.com/sponsors/alextran1502), or a one time donation with the Buy Me a coffee link below. | ||||||
|   | |||||||
| @@ -21,10 +21,18 @@ linter: | |||||||
|   # or a specific dart file by using the `// ignore: name_of_lint` and |   # or a specific dart file by using the `// ignore: name_of_lint` and | ||||||
|   # `// ignore_for_file: name_of_lint` syntax on the line or in the file |   # `// ignore_for_file: name_of_lint` syntax on the line or in the file | ||||||
|   # producing the lint. |   # producing the lint. | ||||||
|  |  | ||||||
|   rules: |   rules: | ||||||
|     # avoid_print: false  # Uncomment to disable the `avoid_print` rule |     # avoid_print: false  # Uncomment to disable the `avoid_print` rule | ||||||
|     # prefer_single_quotes: true  # Uncomment to enable the `prefer_single_quotes` rule |     # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule | ||||||
|     use_build_context_synchronously: false |     use_build_context_synchronously: false | ||||||
|  |     require_trailing_commas: true | ||||||
|  |     unrelated_type_equality_checks: true | ||||||
|  |  | ||||||
| # Additional information about this file can be found at | # Additional information about this file can be found at | ||||||
| # https://dart.dev/guides/language/analysis-options | # https://dart.dev/guides/language/analysis-options | ||||||
|  | analyzer: | ||||||
|  |   exclude: | ||||||
|  |     - openapi/ | ||||||
|  |     - openapi/test/ | ||||||
|  |     - lib/generated_plugin_registrant.dart | ||||||
|   | |||||||
| @@ -71,6 +71,7 @@ | |||||||
|   "login_form_label_password": "Password", |   "login_form_label_password": "Password", | ||||||
|   "login_form_password_hint": "password", |   "login_form_password_hint": "password", | ||||||
|   "login_form_save_login": "Stay logged in", |   "login_form_save_login": "Stay logged in", | ||||||
|  |   "login_form_failed_login": "Error logging you in, check server url, email and password", | ||||||
|   "monthly_title_text_date_format": "MMMM y", |   "monthly_title_text_date_format": "MMMM y", | ||||||
|   "profile_drawer_client_server_up_to_date": "Client and Server are up-to-date", |   "profile_drawer_client_server_up_to_date": "Client and Server are up-to-date", | ||||||
|   "profile_drawer_sign_out": "Sign Out", |   "profile_drawer_sign_out": "Sign Out", | ||||||
| @@ -81,6 +82,7 @@ | |||||||
|   "search_result_page_new_search_hint": "New Search", |   "search_result_page_new_search_hint": "New Search", | ||||||
|   "select_additional_user_for_sharing_page_suggestions": "Suggestions", |   "select_additional_user_for_sharing_page_suggestions": "Suggestions", | ||||||
|   "select_user_for_sharing_page_err_album": "Failed to create album", |   "select_user_for_sharing_page_err_album": "Failed to create album", | ||||||
|  |   "select_user_for_sharing_page_share_suggestions": "Suggestions", | ||||||
|   "share_add": "Add", |   "share_add": "Add", | ||||||
|   "share_add_photos": "Add photos", |   "share_add_photos": "Add photos", | ||||||
|   "share_add_title": "Add a title", |   "share_add_title": "Add a title", | ||||||
| @@ -100,4 +102,4 @@ | |||||||
|   "version_announcement_overlay_text_2": "please take your time to visit the ", |   "version_announcement_overlay_text_2": "please take your time to visit the ", | ||||||
|   "version_announcement_overlay_text_3": " and ensure your docker-compose and .env setup is up-to-date to prevent any misconfigurations, especially if you use WatchTower or any mechanism that handles updating your server application automatically.", |   "version_announcement_overlay_text_3": " and ensure your docker-compose and .env setup is up-to-date to prevent any misconfigurations, especially if you use WatchTower or any mechanism that handles updating your server application automatically.", | ||||||
|   "version_announcement_overlay_title": "New Server Version Available \uD83C\uDF89" |   "version_announcement_overlay_title": "New Server Version Available \uD83C\uDF89" | ||||||
| } | } | ||||||
|   | |||||||
| @@ -46,12 +46,15 @@ void main() async { | |||||||
|     Locale('de', 'DE') |     Locale('de', 'DE') | ||||||
|   ]; |   ]; | ||||||
|  |  | ||||||
|   runApp(EasyLocalization( |   runApp( | ||||||
|  |     EasyLocalization( | ||||||
|       supportedLocales: locales, |       supportedLocales: locales, | ||||||
|       path: 'assets/i18n', |       path: 'assets/i18n', | ||||||
|       useFallbackTranslations: true, |       useFallbackTranslations: true, | ||||||
|       fallbackLocale: locales.first, |       fallbackLocale: locales.first, | ||||||
|       child: const ProviderScope(child: ImmichApp()))); |       child: const ProviderScope(child: ImmichApp()), | ||||||
|  |     ), | ||||||
|  |   ); | ||||||
| } | } | ||||||
|  |  | ||||||
| class ImmichApp extends ConsumerStatefulWidget { | class ImmichApp extends ConsumerStatefulWidget { | ||||||
| @@ -111,6 +114,7 @@ class ImmichAppState extends ConsumerState<ImmichApp> | |||||||
|   @override |   @override | ||||||
|   initState() { |   initState() { | ||||||
|     super.initState(); |     super.initState(); | ||||||
|  |  | ||||||
|     initApp().then((_) => debugPrint("App Init Completed")); |     initApp().then((_) => debugPrint("App Init Completed")); | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -120,10 +124,9 @@ class ImmichAppState extends ConsumerState<ImmichApp> | |||||||
|     super.dispose(); |     super.dispose(); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   final _immichRouter = AppRouter(); |  | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   Widget build(BuildContext context) { |   Widget build(BuildContext context) { | ||||||
|  |     var router = ref.watch(appRouterProvider); | ||||||
|     ref.watch(releaseInfoProvider.notifier).checkGithubReleaseInfo(); |     ref.watch(releaseInfoProvider.notifier).checkGithubReleaseInfo(); | ||||||
|  |  | ||||||
|     return MaterialApp( |     return MaterialApp( | ||||||
| @@ -142,7 +145,8 @@ class ImmichAppState extends ConsumerState<ImmichApp> | |||||||
|               primarySwatch: Colors.indigo, |               primarySwatch: Colors.indigo, | ||||||
|               fontFamily: 'WorkSans', |               fontFamily: 'WorkSans', | ||||||
|               snackBarTheme: const SnackBarThemeData( |               snackBarTheme: const SnackBarThemeData( | ||||||
|                   contentTextStyle: TextStyle(fontFamily: 'WorkSans')), |                 contentTextStyle: TextStyle(fontFamily: 'WorkSans'), | ||||||
|  |               ), | ||||||
|               scaffoldBackgroundColor: immichBackgroundColor, |               scaffoldBackgroundColor: immichBackgroundColor, | ||||||
|               appBarTheme: const AppBarTheme( |               appBarTheme: const AppBarTheme( | ||||||
|                 backgroundColor: immichBackgroundColor, |                 backgroundColor: immichBackgroundColor, | ||||||
| @@ -152,9 +156,10 @@ class ImmichAppState extends ConsumerState<ImmichApp> | |||||||
|                 systemOverlayStyle: SystemUiOverlayStyle.dark, |                 systemOverlayStyle: SystemUiOverlayStyle.dark, | ||||||
|               ), |               ), | ||||||
|             ), |             ), | ||||||
|             routeInformationParser: _immichRouter.defaultRouteParser(), |             routeInformationParser: router.defaultRouteParser(), | ||||||
|             routerDelegate: _immichRouter.delegate( |             routerDelegate: router.delegate( | ||||||
|                 navigatorObservers: () => [TabNavigationObserver(ref: ref)]), |               navigatorObservers: () => [TabNavigationObserver(ref: ref)], | ||||||
|  |             ), | ||||||
|           ), |           ), | ||||||
|           const ImmichLoadingOverlay(), |           const ImmichLoadingOverlay(), | ||||||
|           const VersionAnnouncementOverlay(), |           const VersionAnnouncementOverlay(), | ||||||
|   | |||||||
| @@ -3,17 +3,20 @@ import 'package:fluttertoast/fluttertoast.dart'; | |||||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||||
| import 'package:immich_mobile/modules/asset_viewer/models/image_viewer_page_state.model.dart'; | import 'package:immich_mobile/modules/asset_viewer/models/image_viewer_page_state.model.dart'; | ||||||
| import 'package:immich_mobile/modules/asset_viewer/services/image_viewer.service.dart'; | import 'package:immich_mobile/modules/asset_viewer/services/image_viewer.service.dart'; | ||||||
| import 'package:immich_mobile/shared/models/immich_asset.model.dart'; |  | ||||||
| import 'package:immich_mobile/shared/ui/immich_toast.dart'; | import 'package:immich_mobile/shared/ui/immich_toast.dart'; | ||||||
|  | import 'package:openapi/api.dart'; | ||||||
|  |  | ||||||
| class ImageViewerStateNotifier extends StateNotifier<ImageViewerPageState> { | class ImageViewerStateNotifier extends StateNotifier<ImageViewerPageState> { | ||||||
|   final ImageViewerService _imageViewerService = ImageViewerService(); |   final ImageViewerService _imageViewerService; | ||||||
|  |  | ||||||
|   ImageViewerStateNotifier() |   ImageViewerStateNotifier(this._imageViewerService) | ||||||
|       : super(ImageViewerPageState( |       : super( | ||||||
|             downloadAssetStatus: DownloadAssetStatus.idle)); |           ImageViewerPageState( | ||||||
|  |             downloadAssetStatus: DownloadAssetStatus.idle, | ||||||
|  |           ), | ||||||
|  |         ); | ||||||
|  |  | ||||||
|   void downloadAsset(ImmichAsset asset, BuildContext context) async { |   void downloadAsset(AssetResponseDto asset, BuildContext context) async { | ||||||
|     state = state.copyWith(downloadAssetStatus: DownloadAssetStatus.loading); |     state = state.copyWith(downloadAssetStatus: DownloadAssetStatus.loading); | ||||||
|  |  | ||||||
|     bool isSuccess = await _imageViewerService.downloadAssetToDevice(asset); |     bool isSuccess = await _imageViewerService.downloadAssetToDevice(asset); | ||||||
| @@ -43,4 +46,5 @@ class ImageViewerStateNotifier extends StateNotifier<ImageViewerPageState> { | |||||||
|  |  | ||||||
| final imageViewerStateProvider = | final imageViewerStateProvider = | ||||||
|     StateNotifierProvider<ImageViewerStateNotifier, ImageViewerPageState>( |     StateNotifierProvider<ImageViewerStateNotifier, ImageViewerPageState>( | ||||||
|         ((ref) => ImageViewerStateNotifier())); |   ((ref) => ImageViewerStateNotifier(ref.watch(imageViewerServiceProvider))), | ||||||
|  | ); | ||||||
|   | |||||||
| @@ -1,33 +1,35 @@ | |||||||
| import 'dart:io'; | import 'dart:io'; | ||||||
|  |  | ||||||
| import 'package:flutter/foundation.dart'; | import 'package:flutter/foundation.dart'; | ||||||
| import 'package:hive_flutter/hive_flutter.dart'; | import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||||
| import 'package:immich_mobile/constants/hive_box.dart'; | import 'package:immich_mobile/shared/services/api.service.dart'; | ||||||
| import 'package:immich_mobile/shared/models/immich_asset.model.dart'; | import 'package:openapi/api.dart'; | ||||||
| import 'package:path/path.dart' as p; | import 'package:path/path.dart' as p; | ||||||
| import 'package:http/http.dart' as http; |  | ||||||
|  |  | ||||||
| import 'package:photo_manager/photo_manager.dart'; | import 'package:photo_manager/photo_manager.dart'; | ||||||
| import 'package:path_provider/path_provider.dart'; | import 'package:path_provider/path_provider.dart'; | ||||||
|  |  | ||||||
|  | final imageViewerServiceProvider = | ||||||
|  |     Provider((ref) => ImageViewerService(ref.watch(apiServiceProvider))); | ||||||
|  |  | ||||||
| class ImageViewerService { | class ImageViewerService { | ||||||
|   Future<bool> downloadAssetToDevice(ImmichAsset asset) async { |   final ApiService _apiService; | ||||||
|  |   ImageViewerService(this._apiService); | ||||||
|  |  | ||||||
|  |   Future<bool> downloadAssetToDevice(AssetResponseDto asset) async { | ||||||
|     try { |     try { | ||||||
|       String fileName = p.basename(asset.originalPath); |       String fileName = p.basename(asset.originalPath); | ||||||
|       var savedEndpoint = Hive.box(userInfoBox).get(serverEndpointKey); |  | ||||||
|       Uri filePath = Uri.parse( |  | ||||||
|           "$savedEndpoint/asset/download?aid=${asset.deviceAssetId}&did=${asset.deviceId}&isThumb=false"); |  | ||||||
|  |  | ||||||
|       var res = await http.get( |       var res = await _apiService.assetApi.downloadFileWithHttpInfo( | ||||||
|         filePath, |         asset.deviceAssetId, | ||||||
|         headers: { |         asset.deviceId, | ||||||
|           "Authorization": "Bearer ${Hive.box(userInfoBox).get(accessTokenKey)}" |         isThumb: false, | ||||||
|         }, |         isWeb: false, | ||||||
|       ); |       ); | ||||||
|  |  | ||||||
|       final AssetEntity? entity; |       final AssetEntity? entity; | ||||||
|  |  | ||||||
|       if (asset.type == 'IMAGE') { |       if (asset.type == AssetTypeEnum.IMAGE) { | ||||||
|         entity = await PhotoManager.editor.saveImage( |         entity = await PhotoManager.editor.saveImage( | ||||||
|           res.bodyBytes, |           res.bodyBytes, | ||||||
|           title: p.basename(asset.originalPath), |           title: p.basename(asset.originalPath), | ||||||
|   | |||||||
| @@ -2,13 +2,12 @@ import 'package:easy_localization/easy_localization.dart'; | |||||||
| import 'package:flutter/material.dart'; | import 'package:flutter/material.dart'; | ||||||
| import 'package:flutter_map/flutter_map.dart'; | import 'package:flutter_map/flutter_map.dart'; | ||||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||||
| import 'package:immich_mobile/shared/models/immich_asset_with_exif.model.dart'; | import 'package:openapi/api.dart'; | ||||||
| import 'package:intl/intl.dart'; |  | ||||||
| import 'package:path/path.dart' as p; | import 'package:path/path.dart' as p; | ||||||
| import 'package:latlong2/latlong.dart'; | import 'package:latlong2/latlong.dart'; | ||||||
|  |  | ||||||
| class ExifBottomSheet extends ConsumerWidget { | class ExifBottomSheet extends ConsumerWidget { | ||||||
|   final ImmichAssetWithExif assetDetail; |   final AssetResponseDto assetDetail; | ||||||
|  |  | ||||||
|   const ExifBottomSheet({Key? key, required this.assetDetail}) |   const ExifBottomSheet({Key? key, required this.assetDetail}) | ||||||
|       : super(key: key); |       : super(key: key); | ||||||
| @@ -26,8 +25,10 @@ class ExifBottomSheet extends ConsumerWidget { | |||||||
|           ), |           ), | ||||||
|           child: FlutterMap( |           child: FlutterMap( | ||||||
|             options: MapOptions( |             options: MapOptions( | ||||||
|               center: LatLng(assetDetail.exifInfo!.latitude!, |               center: LatLng( | ||||||
|                   assetDetail.exifInfo!.longitude!), |                 assetDetail.exifInfo?.latitude?.toDouble() ?? 0, | ||||||
|  |                 assetDetail.exifInfo?.longitude?.toDouble() ?? 0, | ||||||
|  |               ), | ||||||
|               zoom: 16.0, |               zoom: 16.0, | ||||||
|             ), |             ), | ||||||
|             layers: [ |             layers: [ | ||||||
| @@ -46,10 +47,13 @@ class ExifBottomSheet extends ConsumerWidget { | |||||||
|                 markers: [ |                 markers: [ | ||||||
|                   Marker( |                   Marker( | ||||||
|                     anchorPos: AnchorPos.align(AnchorAlign.top), |                     anchorPos: AnchorPos.align(AnchorAlign.top), | ||||||
|                     point: LatLng(assetDetail.exifInfo!.latitude!, |                     point: LatLng( | ||||||
|                         assetDetail.exifInfo!.longitude!), |                       assetDetail.exifInfo?.latitude?.toDouble() ?? 0, | ||||||
|  |                       assetDetail.exifInfo?.longitude?.toDouble() ?? 0, | ||||||
|  |                     ), | ||||||
|                     builder: (ctx) => const Image( |                     builder: (ctx) => const Image( | ||||||
|                         image: AssetImage('assets/location-pin.png')), |                       image: AssetImage('assets/location-pin.png'), | ||||||
|  |                     ), | ||||||
|                   ), |                   ), | ||||||
|                 ], |                 ], | ||||||
|               ), |               ), | ||||||
| @@ -63,7 +67,10 @@ class ExifBottomSheet extends ConsumerWidget { | |||||||
|       return Text( |       return Text( | ||||||
|         "${assetDetail.exifInfo!.city}, ${assetDetail.exifInfo!.state}", |         "${assetDetail.exifInfo!.city}, ${assetDetail.exifInfo!.state}", | ||||||
|         style: TextStyle( |         style: TextStyle( | ||||||
|             fontSize: 12, color: Colors.grey[200], fontWeight: FontWeight.bold), |           fontSize: 12, | ||||||
|  |           color: Colors.grey[200], | ||||||
|  |           fontWeight: FontWeight.bold, | ||||||
|  |         ), | ||||||
|       ); |       ); | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -74,7 +81,7 @@ class ExifBottomSheet extends ConsumerWidget { | |||||||
|           if (assetDetail.exifInfo?.dateTimeOriginal != null) |           if (assetDetail.exifInfo?.dateTimeOriginal != null) | ||||||
|             Text( |             Text( | ||||||
|               DateFormat('date_format'.tr()).format( |               DateFormat('date_format'.tr()).format( | ||||||
|                 DateTime.parse(assetDetail.exifInfo!.dateTimeOriginal!), |                 assetDetail.exifInfo!.dateTimeOriginal!, | ||||||
|               ), |               ), | ||||||
|               style: TextStyle( |               style: TextStyle( | ||||||
|                 color: Colors.grey[400], |                 color: Colors.grey[400], | ||||||
| @@ -151,7 +158,8 @@ class ExifBottomSheet extends ConsumerWidget { | |||||||
|                     ), |                     ), | ||||||
|                     subtitle: assetDetail.exifInfo?.exifImageHeight != null |                     subtitle: assetDetail.exifInfo?.exifImageHeight != null | ||||||
|                         ? Text( |                         ? Text( | ||||||
|                             "${assetDetail.exifInfo?.exifImageHeight} x ${assetDetail.exifInfo?.exifImageWidth}  ${assetDetail.exifInfo?.fileSizeInByte!}B ") |                             "${assetDetail.exifInfo?.exifImageHeight} x ${assetDetail.exifInfo?.exifImageWidth}  ${assetDetail.exifInfo?.fileSizeInByte!}B ", | ||||||
|  |                           ) | ||||||
|                         : null, |                         : null, | ||||||
|                   ), |                   ), | ||||||
|                   if (assetDetail.exifInfo?.make != null) |                   if (assetDetail.exifInfo?.make != null) | ||||||
| @@ -166,7 +174,8 @@ class ExifBottomSheet extends ConsumerWidget { | |||||||
|                         style: const TextStyle(fontWeight: FontWeight.bold), |                         style: const TextStyle(fontWeight: FontWeight.bold), | ||||||
|                       ), |                       ), | ||||||
|                       subtitle: Text( |                       subtitle: Text( | ||||||
|                           "ƒ/${assetDetail.exifInfo?.fNumber}   1/${(1 / (assetDetail.exifInfo?.exposureTime ?? 1)).toStringAsFixed(0)}   ${assetDetail.exifInfo?.focalLength}mm   ISO${assetDetail.exifInfo?.iso} "), |                         "ƒ/${assetDetail.exifInfo?.fNumber}   1/${(1 / (assetDetail.exifInfo?.exposureTime ?? 1)).toStringAsFixed(0)}   ${assetDetail.exifInfo?.focalLength}mm   ISO${assetDetail.exifInfo?.iso} ", | ||||||
|  |                       ), | ||||||
|                     ), |                     ), | ||||||
|                 ], |                 ], | ||||||
|               ), |               ), | ||||||
|   | |||||||
| @@ -17,16 +17,20 @@ class _RemotePhotoViewState extends State<RemotePhotoView> { | |||||||
|     bool allowMoving = _status == _RemoteImageStatus.full; |     bool allowMoving = _status == _RemoteImageStatus.full; | ||||||
|  |  | ||||||
|     return PhotoView( |     return PhotoView( | ||||||
|         imageProvider: _imageProvider, |       imageProvider: _imageProvider, | ||||||
|         minScale: PhotoViewComputedScale.contained, |       minScale: PhotoViewComputedScale.contained, | ||||||
|         maxScale: allowMoving ? 1.0 : PhotoViewComputedScale.contained, |       maxScale: allowMoving ? 1.0 : PhotoViewComputedScale.contained, | ||||||
|         enablePanAlways: true, |       enablePanAlways: true, | ||||||
|         scaleStateChangedCallback: _scaleStateChanged, |       scaleStateChangedCallback: _scaleStateChanged, | ||||||
|         onScaleEnd: _onScaleListener); |       onScaleEnd: _onScaleListener, | ||||||
|  |     ); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   void _onScaleListener(BuildContext context, ScaleEndDetails details, |   void _onScaleListener( | ||||||
|       PhotoViewControllerValue controllerValue) { |     BuildContext context, | ||||||
|  |     ScaleEndDetails details, | ||||||
|  |     PhotoViewControllerValue controllerValue, | ||||||
|  |   ) { | ||||||
|     // Disable swipe events when zoomed in |     // Disable swipe events when zoomed in | ||||||
|     if (_zoomedIn) return; |     if (_zoomedIn) return; | ||||||
|  |  | ||||||
| @@ -42,12 +46,17 @@ class _RemotePhotoViewState extends State<RemotePhotoView> { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   CachedNetworkImageProvider _authorizedImageProvider(String url) { |   CachedNetworkImageProvider _authorizedImageProvider(String url) { | ||||||
|     return CachedNetworkImageProvider(url, |     return CachedNetworkImageProvider( | ||||||
|         headers: {"Authorization": widget.authToken}, cacheKey: url); |       url, | ||||||
|  |       headers: {"Authorization": widget.authToken}, | ||||||
|  |       cacheKey: url, | ||||||
|  |     ); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   void _performStateTransition( |   void _performStateTransition( | ||||||
|       _RemoteImageStatus newStatus, CachedNetworkImageProvider provider) { |     _RemoteImageStatus newStatus, | ||||||
|  |     CachedNetworkImageProvider provider, | ||||||
|  |   ) { | ||||||
|     // Transition to same status is forbidden |     // Transition to same status is forbidden | ||||||
|     if (_status == newStatus) return; |     if (_status == newStatus) return; | ||||||
|     // Transition full -> thumbnail is forbidden |     // Transition full -> thumbnail is forbidden | ||||||
| @@ -67,19 +76,22 @@ class _RemotePhotoViewState extends State<RemotePhotoView> { | |||||||
|         _authorizedImageProvider(widget.thumbnailUrl); |         _authorizedImageProvider(widget.thumbnailUrl); | ||||||
|     _imageProvider = thumbnailProvider; |     _imageProvider = thumbnailProvider; | ||||||
|  |  | ||||||
|     thumbnailProvider |     thumbnailProvider.resolve(const ImageConfiguration()).addListener( | ||||||
|         .resolve(const ImageConfiguration()) |       ImageStreamListener((ImageInfo imageInfo, _) { | ||||||
|         .addListener(ImageStreamListener((ImageInfo imageInfo, _) { |         _performStateTransition( | ||||||
|       _performStateTransition(_RemoteImageStatus.thumbnail, thumbnailProvider); |           _RemoteImageStatus.thumbnail, | ||||||
|     })); |           thumbnailProvider, | ||||||
|  |         ); | ||||||
|  |       }), | ||||||
|  |     ); | ||||||
|  |  | ||||||
|     CachedNetworkImageProvider fullProvider = |     CachedNetworkImageProvider fullProvider = | ||||||
|         _authorizedImageProvider(widget.imageUrl); |         _authorizedImageProvider(widget.imageUrl); | ||||||
|     fullProvider |     fullProvider.resolve(const ImageConfiguration()).addListener( | ||||||
|         .resolve(const ImageConfiguration()) |       ImageStreamListener((ImageInfo imageInfo, _) { | ||||||
|         .addListener(ImageStreamListener((ImageInfo imageInfo, _) { |         _performStateTransition(_RemoteImageStatus.full, fullProvider); | ||||||
|       _performStateTransition(_RemoteImageStatus.full, fullProvider); |       }), | ||||||
|     })); |     ); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
| @@ -90,14 +102,14 @@ class _RemotePhotoViewState extends State<RemotePhotoView> { | |||||||
| } | } | ||||||
|  |  | ||||||
| class RemotePhotoView extends StatefulWidget { | class RemotePhotoView extends StatefulWidget { | ||||||
|   const RemotePhotoView( |   const RemotePhotoView({ | ||||||
|       {Key? key, |     Key? key, | ||||||
|       required this.thumbnailUrl, |     required this.thumbnailUrl, | ||||||
|       required this.imageUrl, |     required this.imageUrl, | ||||||
|       required this.authToken, |     required this.authToken, | ||||||
|       required this.onSwipeDown, |     required this.onSwipeDown, | ||||||
|       required this.onSwipeUp}) |     required this.onSwipeUp, | ||||||
|       : super(key: key); |   }) : super(key: key); | ||||||
|  |  | ||||||
|   final String thumbnailUrl; |   final String thumbnailUrl; | ||||||
|   final String imageUrl; |   final String imageUrl; | ||||||
|   | |||||||
| @@ -3,17 +3,17 @@ import 'dart:developer'; | |||||||
| import 'package:auto_route/auto_route.dart'; | import 'package:auto_route/auto_route.dart'; | ||||||
| import 'package:flutter/material.dart'; | import 'package:flutter/material.dart'; | ||||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||||
| import 'package:immich_mobile/shared/models/immich_asset.model.dart'; | import 'package:openapi/api.dart'; | ||||||
|  |  | ||||||
| class TopControlAppBar extends ConsumerWidget with PreferredSizeWidget { | class TopControlAppBar extends ConsumerWidget with PreferredSizeWidget { | ||||||
|   const TopControlAppBar( |   const TopControlAppBar({ | ||||||
|       {Key? key, |     Key? key, | ||||||
|       required this.asset, |     required this.asset, | ||||||
|       required this.onMoreInfoPressed, |     required this.onMoreInfoPressed, | ||||||
|       required this.onDownloadPressed}) |     required this.onDownloadPressed, | ||||||
|       : super(key: key); |   }) : super(key: key); | ||||||
|  |  | ||||||
|   final ImmichAsset asset; |   final AssetResponseDto asset; | ||||||
|   final Function onMoreInfoPressed; |   final Function onMoreInfoPressed; | ||||||
|   final Function onDownloadPressed; |   final Function onDownloadPressed; | ||||||
|  |  | ||||||
| @@ -54,12 +54,13 @@ class TopControlAppBar extends ConsumerWidget with PreferredSizeWidget { | |||||||
|               : const Icon(Icons.favorite_border_rounded), |               : const Icon(Icons.favorite_border_rounded), | ||||||
|         ), |         ), | ||||||
|         IconButton( |         IconButton( | ||||||
|             iconSize: iconSize, |           iconSize: iconSize, | ||||||
|             splashRadius: iconSize, |           splashRadius: iconSize, | ||||||
|             onPressed: () { |           onPressed: () { | ||||||
|               onMoreInfoPressed(); |             onMoreInfoPressed(); | ||||||
|             }, |           }, | ||||||
|             icon: const Icon(Icons.more_horiz_rounded)) |           icon: const Icon(Icons.more_horiz_rounded), | ||||||
|  |         ) | ||||||
|       ], |       ], | ||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
|   | |||||||
| @@ -11,17 +11,16 @@ import 'package:immich_mobile/modules/asset_viewer/ui/exif_bottom_sheet.dart'; | |||||||
| import 'package:immich_mobile/modules/asset_viewer/ui/remote_photo_view.dart'; | import 'package:immich_mobile/modules/asset_viewer/ui/remote_photo_view.dart'; | ||||||
| import 'package:immich_mobile/modules/asset_viewer/ui/top_control_app_bar.dart'; | import 'package:immich_mobile/modules/asset_viewer/ui/top_control_app_bar.dart'; | ||||||
| import 'package:immich_mobile/modules/home/services/asset.service.dart'; | import 'package:immich_mobile/modules/home/services/asset.service.dart'; | ||||||
| import 'package:immich_mobile/shared/models/immich_asset.model.dart'; | import 'package:openapi/api.dart'; | ||||||
| import 'package:immich_mobile/shared/models/immich_asset_with_exif.model.dart'; |  | ||||||
|  |  | ||||||
| // ignore: must_be_immutable | // ignore: must_be_immutable | ||||||
| class ImageViewerPage extends HookConsumerWidget { | class ImageViewerPage extends HookConsumerWidget { | ||||||
|   final String imageUrl; |   final String imageUrl; | ||||||
|   final String heroTag; |   final String heroTag; | ||||||
|   final String thumbnailUrl; |   final String thumbnailUrl; | ||||||
|   final ImmichAsset asset; |   final AssetResponseDto asset; | ||||||
|  |  | ||||||
|   ImmichAssetWithExif? assetDetail; |   AssetResponseDto? assetDetail; | ||||||
|  |  | ||||||
|   ImageViewerPage({ |   ImageViewerPage({ | ||||||
|     Key? key, |     Key? key, | ||||||
| @@ -54,10 +53,13 @@ class ImageViewerPage extends HookConsumerWidget { | |||||||
|       ); |       ); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     useEffect(() { |     useEffect( | ||||||
|       getAssetExif(); |       () { | ||||||
|       return null; |         getAssetExif(); | ||||||
|     }, []); |         return null; | ||||||
|  |       }, | ||||||
|  |       [], | ||||||
|  |     ); | ||||||
|  |  | ||||||
|     return Scaffold( |     return Scaffold( | ||||||
|       backgroundColor: Colors.black, |       backgroundColor: Colors.black, | ||||||
| @@ -75,14 +77,15 @@ class ImageViewerPage extends HookConsumerWidget { | |||||||
|           children: [ |           children: [ | ||||||
|             Center( |             Center( | ||||||
|               child: Hero( |               child: Hero( | ||||||
|                   tag: heroTag, |                 tag: heroTag, | ||||||
|                   child: RemotePhotoView( |                 child: RemotePhotoView( | ||||||
|                     thumbnailUrl: thumbnailUrl, |                   thumbnailUrl: thumbnailUrl, | ||||||
|                     imageUrl: imageUrl, |                   imageUrl: imageUrl, | ||||||
|                     authToken: "Bearer ${box.get(accessTokenKey)}", |                   authToken: "Bearer ${box.get(accessTokenKey)}", | ||||||
|                     onSwipeDown: () => AutoRouter.of(context).pop(), |                   onSwipeDown: () => AutoRouter.of(context).pop(), | ||||||
|                     onSwipeUp: () => showInfo(), |                   onSwipeUp: () => showInfo(), | ||||||
|                   )), |                 ), | ||||||
|  |               ), | ||||||
|             ), |             ), | ||||||
|             if (downloadAssetStatus == DownloadAssetStatus.loading) |             if (downloadAssetStatus == DownloadAssetStatus.loading) | ||||||
|               const Center( |               const Center( | ||||||
|   | |||||||
| @@ -12,15 +12,14 @@ import 'package:immich_mobile/modules/asset_viewer/ui/download_loading_indicator | |||||||
| import 'package:immich_mobile/modules/asset_viewer/ui/exif_bottom_sheet.dart'; | import 'package:immich_mobile/modules/asset_viewer/ui/exif_bottom_sheet.dart'; | ||||||
| import 'package:immich_mobile/modules/asset_viewer/ui/top_control_app_bar.dart'; | import 'package:immich_mobile/modules/asset_viewer/ui/top_control_app_bar.dart'; | ||||||
| import 'package:immich_mobile/modules/home/services/asset.service.dart'; | import 'package:immich_mobile/modules/home/services/asset.service.dart'; | ||||||
| import 'package:immich_mobile/shared/models/immich_asset.model.dart'; | import 'package:openapi/api.dart'; | ||||||
| import 'package:immich_mobile/shared/models/immich_asset_with_exif.model.dart'; |  | ||||||
| import 'package:video_player/video_player.dart'; | import 'package:video_player/video_player.dart'; | ||||||
|  |  | ||||||
| // ignore: must_be_immutable | // ignore: must_be_immutable | ||||||
| class VideoViewerPage extends HookConsumerWidget { | class VideoViewerPage extends HookConsumerWidget { | ||||||
|   final String videoUrl; |   final String videoUrl; | ||||||
|   final ImmichAsset asset; |   final AssetResponseDto asset; | ||||||
|   ImmichAssetWithExif? assetDetail; |   AssetResponseDto? assetDetail; | ||||||
|  |  | ||||||
|   VideoViewerPage({Key? key, required this.videoUrl, required this.asset}) |   VideoViewerPage({Key? key, required this.videoUrl, required this.asset}) | ||||||
|       : super(key: key); |       : super(key: key); | ||||||
| @@ -49,10 +48,13 @@ class VideoViewerPage extends HookConsumerWidget { | |||||||
|           await ref.watch(assetServiceProvider).getAssetById(asset.id); |           await ref.watch(assetServiceProvider).getAssetById(asset.id); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     useEffect(() { |     useEffect( | ||||||
|       getAssetExif(); |       () { | ||||||
|       return null; |         getAssetExif(); | ||||||
|     }, []); |         return null; | ||||||
|  |       }, | ||||||
|  |       [], | ||||||
|  |     ); | ||||||
|  |  | ||||||
|     return Scaffold( |     return Scaffold( | ||||||
|       backgroundColor: Colors.black, |       backgroundColor: Colors.black, | ||||||
| @@ -116,8 +118,10 @@ class _VideoThumbnailPlayerState extends State<VideoThumbnailPlayer> { | |||||||
|  |  | ||||||
|   Future<void> initializePlayer() async { |   Future<void> initializePlayer() async { | ||||||
|     try { |     try { | ||||||
|       videoPlayerController = VideoPlayerController.network(widget.url, |       videoPlayerController = VideoPlayerController.network( | ||||||
|           httpHeaders: {"Authorization": "Bearer ${widget.jwtToken}"}); |         widget.url, | ||||||
|  |         httpHeaders: {"Authorization": "Bearer ${widget.jwtToken}"}, | ||||||
|  |       ); | ||||||
|  |  | ||||||
|       await videoPlayerController.initialize(); |       await videoPlayerController.initialize(); | ||||||
|       _createChewieController(); |       _createChewieController(); | ||||||
|   | |||||||
| @@ -1,10 +1,10 @@ | |||||||
| import 'package:cancellation_token_http/http.dart'; | import 'package:cancellation_token_http/http.dart'; | ||||||
| import 'package:collection/collection.dart'; | import 'package:collection/collection.dart'; | ||||||
|  | import 'package:openapi/api.dart'; | ||||||
| import 'package:photo_manager/photo_manager.dart'; | import 'package:photo_manager/photo_manager.dart'; | ||||||
|  |  | ||||||
| import 'package:immich_mobile/modules/backup/models/available_album.model.dart'; | import 'package:immich_mobile/modules/backup/models/available_album.model.dart'; | ||||||
| import 'package:immich_mobile/modules/backup/models/current_upload_asset.model.dart'; | import 'package:immich_mobile/modules/backup/models/current_upload_asset.model.dart'; | ||||||
| import 'package:immich_mobile/shared/models/server_info.model.dart'; |  | ||||||
|  |  | ||||||
| enum BackUpProgressEnum { idle, inProgress, done } | enum BackUpProgressEnum { idle, inProgress, done } | ||||||
|  |  | ||||||
| @@ -14,7 +14,7 @@ class BackUpState { | |||||||
|   final List<String> allAssetsInDatabase; |   final List<String> allAssetsInDatabase; | ||||||
|   final double progressInPercentage; |   final double progressInPercentage; | ||||||
|   final CancellationToken cancelToken; |   final CancellationToken cancelToken; | ||||||
|   final ServerInfo serverInfo; |   final ServerInfoResponseDto serverInfo; | ||||||
|  |  | ||||||
|   /// All available albums on the device |   /// All available albums on the device | ||||||
|   final List<AvailableAlbum> availableAlbums; |   final List<AvailableAlbum> availableAlbums; | ||||||
| @@ -49,7 +49,7 @@ class BackUpState { | |||||||
|     List<String>? allAssetsInDatabase, |     List<String>? allAssetsInDatabase, | ||||||
|     double? progressInPercentage, |     double? progressInPercentage, | ||||||
|     CancellationToken? cancelToken, |     CancellationToken? cancelToken, | ||||||
|     ServerInfo? serverInfo, |     ServerInfoResponseDto? serverInfo, | ||||||
|     List<AvailableAlbum>? availableAlbums, |     List<AvailableAlbum>? availableAlbums, | ||||||
|     Set<AssetPathEntity>? selectedBackupAlbums, |     Set<AssetPathEntity>? selectedBackupAlbums, | ||||||
|     Set<AssetPathEntity>? excludedBackupAlbums, |     Set<AssetPathEntity>? excludedBackupAlbums, | ||||||
| @@ -93,8 +93,10 @@ class BackUpState { | |||||||
|         collectionEquals(other.selectedBackupAlbums, selectedBackupAlbums) && |         collectionEquals(other.selectedBackupAlbums, selectedBackupAlbums) && | ||||||
|         collectionEquals(other.excludedBackupAlbums, excludedBackupAlbums) && |         collectionEquals(other.excludedBackupAlbums, excludedBackupAlbums) && | ||||||
|         collectionEquals(other.allUniqueAssets, allUniqueAssets) && |         collectionEquals(other.allUniqueAssets, allUniqueAssets) && | ||||||
|         collectionEquals(other.selectedAlbumsBackupAssetsIds, |         collectionEquals( | ||||||
|             selectedAlbumsBackupAssetsIds) && |           other.selectedAlbumsBackupAssetsIds, | ||||||
|  |           selectedAlbumsBackupAssetsIds, | ||||||
|  |         ) && | ||||||
|         other.currentUploadAsset == currentUploadAsset; |         other.currentUploadAsset == currentUploadAsset; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,48 +0,0 @@ | |||||||
| import 'dart:convert'; |  | ||||||
|  |  | ||||||
| class CheckDuplicateAssetResponse { |  | ||||||
|   final bool isExist; |  | ||||||
|   CheckDuplicateAssetResponse({ |  | ||||||
|     required this.isExist, |  | ||||||
|   }); |  | ||||||
|  |  | ||||||
|   CheckDuplicateAssetResponse copyWith({ |  | ||||||
|     bool? isExist, |  | ||||||
|   }) { |  | ||||||
|     return CheckDuplicateAssetResponse( |  | ||||||
|       isExist: isExist ?? this.isExist, |  | ||||||
|     ); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   Map<String, dynamic> toMap() { |  | ||||||
|     final result = <String, dynamic>{}; |  | ||||||
|  |  | ||||||
|     result.addAll({'isExist': isExist}); |  | ||||||
|  |  | ||||||
|     return result; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   factory CheckDuplicateAssetResponse.fromMap(Map<String, dynamic> map) { |  | ||||||
|     return CheckDuplicateAssetResponse( |  | ||||||
|       isExist: map['isExist'] ?? false, |  | ||||||
|     ); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   String toJson() => json.encode(toMap()); |  | ||||||
|  |  | ||||||
|   factory CheckDuplicateAssetResponse.fromJson(String source) => |  | ||||||
|       CheckDuplicateAssetResponse.fromMap(json.decode(source)); |  | ||||||
|  |  | ||||||
|   @override |  | ||||||
|   String toString() => 'CheckDuplicateAssetResponse(isExist: $isExist)'; |  | ||||||
|  |  | ||||||
|   @override |  | ||||||
|   bool operator ==(Object other) { |  | ||||||
|     if (identical(this, other)) return true; |  | ||||||
|  |  | ||||||
|     return other is CheckDuplicateAssetResponse && other.isExist == isExist; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   @override |  | ||||||
|   int get hashCode => isExist.hashCode; |  | ||||||
| } |  | ||||||
| @@ -12,8 +12,8 @@ import 'package:immich_mobile/modules/backup/providers/error_backup_list.provide | |||||||
| import 'package:immich_mobile/modules/backup/services/backup.service.dart'; | import 'package:immich_mobile/modules/backup/services/backup.service.dart'; | ||||||
| import 'package:immich_mobile/modules/login/models/authentication_state.model.dart'; | import 'package:immich_mobile/modules/login/models/authentication_state.model.dart'; | ||||||
| import 'package:immich_mobile/modules/login/providers/authentication.provider.dart'; | import 'package:immich_mobile/modules/login/providers/authentication.provider.dart'; | ||||||
| import 'package:immich_mobile/shared/models/server_info.model.dart'; |  | ||||||
| import 'package:immich_mobile/shared/services/server_info.service.dart'; | import 'package:immich_mobile/shared/services/server_info.service.dart'; | ||||||
|  | import 'package:openapi/api.dart'; | ||||||
| import 'package:photo_manager/photo_manager.dart'; | import 'package:photo_manager/photo_manager.dart'; | ||||||
|  |  | ||||||
| class BackupNotifier extends StateNotifier<BackUpState> { | class BackupNotifier extends StateNotifier<BackUpState> { | ||||||
| @@ -28,12 +28,12 @@ class BackupNotifier extends StateNotifier<BackUpState> { | |||||||
|             allAssetsInDatabase: const [], |             allAssetsInDatabase: const [], | ||||||
|             progressInPercentage: 0, |             progressInPercentage: 0, | ||||||
|             cancelToken: CancellationToken(), |             cancelToken: CancellationToken(), | ||||||
|             serverInfo: ServerInfo( |             serverInfo: ServerInfoResponseDto( | ||||||
|               diskAvailable: "0", |               diskAvailable: "0", | ||||||
|               diskAvailableRaw: 0, |               diskAvailableRaw: 0, | ||||||
|               diskSize: "0", |               diskSize: "0", | ||||||
|               diskSizeRaw: 0, |               diskSizeRaw: 0, | ||||||
|               diskUsagePercentage: 0.0, |               diskUsagePercentage: 0, | ||||||
|               diskUse: "0", |               diskUse: "0", | ||||||
|               diskUseRaw: 0, |               diskUseRaw: 0, | ||||||
|             ), |             ), | ||||||
| @@ -113,7 +113,9 @@ class BackupNotifier extends StateNotifier<BackUpState> { | |||||||
|     // Get all albums on the device |     // Get all albums on the device | ||||||
|     List<AvailableAlbum> availableAlbums = []; |     List<AvailableAlbum> availableAlbums = []; | ||||||
|     List<AssetPathEntity> albums = await PhotoManager.getAssetPathList( |     List<AssetPathEntity> albums = await PhotoManager.getAssetPathList( | ||||||
|         hasAll: true, type: RequestType.common); |       hasAll: true, | ||||||
|  |       type: RequestType.common, | ||||||
|  |     ); | ||||||
|  |  | ||||||
|     for (AssetPathEntity album in albums) { |     for (AssetPathEntity album in albums) { | ||||||
|       AvailableAlbum availableAlbum = AvailableAlbum(albumEntity: album); |       AvailableAlbum availableAlbum = AvailableAlbum(albumEntity: album); | ||||||
| @@ -156,7 +158,10 @@ class BackupNotifier extends StateNotifier<BackUpState> { | |||||||
|  |  | ||||||
|       // Get album that contains all assets |       // Get album that contains all assets | ||||||
|       var list = await PhotoManager.getAssetPathList( |       var list = await PhotoManager.getAssetPathList( | ||||||
|           hasAll: true, onlyAll: true, type: RequestType.common); |         hasAll: true, | ||||||
|  |         onlyAll: true, | ||||||
|  |         type: RequestType.common, | ||||||
|  |       ); | ||||||
|       AssetPathEntity albumHasAllAssets = list.first; |       AssetPathEntity albumHasAllAssets = list.first; | ||||||
|  |  | ||||||
|       backupAlbumInfoBox.put( |       backupAlbumInfoBox.put( | ||||||
| @@ -175,13 +180,15 @@ class BackupNotifier extends StateNotifier<BackUpState> { | |||||||
|       for (var selectedAlbumId in backupAlbumInfo!.selectedAlbumIds) { |       for (var selectedAlbumId in backupAlbumInfo!.selectedAlbumIds) { | ||||||
|         var albumAsset = await AssetPathEntity.fromId(selectedAlbumId); |         var albumAsset = await AssetPathEntity.fromId(selectedAlbumId); | ||||||
|         state = state.copyWith( |         state = state.copyWith( | ||||||
|             selectedBackupAlbums: {...state.selectedBackupAlbums, albumAsset}); |           selectedBackupAlbums: {...state.selectedBackupAlbums, albumAsset}, | ||||||
|  |         ); | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       for (var excludedAlbumId in backupAlbumInfo.excludedAlbumsIds) { |       for (var excludedAlbumId in backupAlbumInfo.excludedAlbumsIds) { | ||||||
|         var albumAsset = await AssetPathEntity.fromId(excludedAlbumId); |         var albumAsset = await AssetPathEntity.fromId(excludedAlbumId); | ||||||
|         state = state.copyWith( |         state = state.copyWith( | ||||||
|             excludedBackupAlbums: {...state.excludedBackupAlbums, albumAsset}); |           excludedBackupAlbums: {...state.excludedBackupAlbums, albumAsset}, | ||||||
|  |         ); | ||||||
|       } |       } | ||||||
|     } catch (e) { |     } catch (e) { | ||||||
|       debugPrint("[ERROR] Failed to generate album from id $e"); |       debugPrint("[ERROR] Failed to generate album from id $e"); | ||||||
| @@ -211,8 +218,11 @@ class BackupNotifier extends StateNotifier<BackUpState> { | |||||||
|  |  | ||||||
|     Set<AssetEntity> allUniqueAssets = |     Set<AssetEntity> allUniqueAssets = | ||||||
|         assetsFromSelectedAlbums.difference(assetsFromExcludedAlbums); |         assetsFromSelectedAlbums.difference(assetsFromExcludedAlbums); | ||||||
|     List<String> allAssetsInDatabase = |     var allAssetsInDatabase = await _backupService.getDeviceBackupAsset(); | ||||||
|         await _backupService.getDeviceBackupAsset(); |  | ||||||
|  |     if (allAssetsInDatabase == null) { | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |  | ||||||
|     // Find asset that were backup from selected albums |     // Find asset that were backup from selected albums | ||||||
|     Set<String> selectedAlbumsBackupAssets = |     Set<String> selectedAlbumsBackupAssets = | ||||||
| @@ -328,23 +338,27 @@ class BackupNotifier extends StateNotifier<BackUpState> { | |||||||
|   void cancelBackup() { |   void cancelBackup() { | ||||||
|     state.cancelToken.cancel(); |     state.cancelToken.cancel(); | ||||||
|     state = state.copyWith( |     state = state.copyWith( | ||||||
|         backupProgress: BackUpProgressEnum.idle, progressInPercentage: 0.0); |       backupProgress: BackUpProgressEnum.idle, | ||||||
|  |       progressInPercentage: 0.0, | ||||||
|  |     ); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   void _onAssetUploaded(String deviceAssetId, String deviceId) { |   void _onAssetUploaded(String deviceAssetId, String deviceId) { | ||||||
|     state = state.copyWith(selectedAlbumsBackupAssetsIds: { |     state = state.copyWith( | ||||||
|       ...state.selectedAlbumsBackupAssetsIds, |       selectedAlbumsBackupAssetsIds: { | ||||||
|       deviceAssetId |         ...state.selectedAlbumsBackupAssetsIds, | ||||||
|     }, allAssetsInDatabase: [ |         deviceAssetId | ||||||
|       ...state.allAssetsInDatabase, |       }, | ||||||
|       deviceAssetId |       allAssetsInDatabase: [...state.allAssetsInDatabase, deviceAssetId], | ||||||
|     ]); |     ); | ||||||
|  |  | ||||||
|     if (state.allUniqueAssets.length - |     if (state.allUniqueAssets.length - | ||||||
|             state.selectedAlbumsBackupAssetsIds.length == |             state.selectedAlbumsBackupAssetsIds.length == | ||||||
|         0) { |         0) { | ||||||
|       state = state.copyWith( |       state = state.copyWith( | ||||||
|           backupProgress: BackUpProgressEnum.done, progressInPercentage: 0.0); |         backupProgress: BackUpProgressEnum.done, | ||||||
|  |         progressInPercentage: 0.0, | ||||||
|  |       ); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     _updateServerInfo(); |     _updateServerInfo(); | ||||||
| @@ -352,24 +366,19 @@ class BackupNotifier extends StateNotifier<BackUpState> { | |||||||
|  |  | ||||||
|   void _onUploadProgress(int sent, int total) { |   void _onUploadProgress(int sent, int total) { | ||||||
|     state = state.copyWith( |     state = state.copyWith( | ||||||
|         progressInPercentage: (sent.toDouble() / total.toDouble() * 100)); |       progressInPercentage: (sent.toDouble() / total.toDouble() * 100), | ||||||
|  |     ); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   Future<void> _updateServerInfo() async { |   Future<void> _updateServerInfo() async { | ||||||
|     var serverInfo = await _serverInfoService.getServerInfo(); |     var serverInfo = await _serverInfoService.getServerInfo(); | ||||||
|  |  | ||||||
|     // Update server info |     // Update server info | ||||||
|     state = state.copyWith( |     if (serverInfo != null) { | ||||||
|       serverInfo: ServerInfo( |       state = state.copyWith( | ||||||
|         diskSize: serverInfo.diskSize, |         serverInfo: serverInfo, | ||||||
|         diskUse: serverInfo.diskUse, |       ); | ||||||
|         diskAvailable: serverInfo.diskAvailable, |     } | ||||||
|         diskSizeRaw: serverInfo.diskSizeRaw, |  | ||||||
|         diskUseRaw: serverInfo.diskUseRaw, |  | ||||||
|         diskAvailableRaw: serverInfo.diskAvailableRaw, |  | ||||||
|         diskUsagePercentage: serverInfo.diskUsagePercentage, |  | ||||||
|       ), |  | ||||||
|     ); |  | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   void resumeBackup() { |   void resumeBackup() { | ||||||
|   | |||||||
| @@ -2,59 +2,38 @@ import 'dart:async'; | |||||||
| import 'dart:convert'; | import 'dart:convert'; | ||||||
| import 'dart:io'; | import 'dart:io'; | ||||||
|  |  | ||||||
| import 'package:dio/dio.dart'; |  | ||||||
| import 'package:flutter/material.dart'; | import 'package:flutter/material.dart'; | ||||||
| import 'package:hive/hive.dart'; | import 'package:hive/hive.dart'; | ||||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||||
| import 'package:immich_mobile/constants/hive_box.dart'; | import 'package:immich_mobile/constants/hive_box.dart'; | ||||||
| import 'package:immich_mobile/modules/backup/models/check_duplicate_asset_response.model.dart'; |  | ||||||
| import 'package:immich_mobile/modules/backup/models/current_upload_asset.model.dart'; | import 'package:immich_mobile/modules/backup/models/current_upload_asset.model.dart'; | ||||||
| import 'package:immich_mobile/modules/backup/models/error_upload_asset.model.dart'; | import 'package:immich_mobile/modules/backup/models/error_upload_asset.model.dart'; | ||||||
| import 'package:immich_mobile/shared/services/network.service.dart'; | import 'package:immich_mobile/shared/services/api.service.dart'; | ||||||
| import 'package:immich_mobile/shared/models/device_info.model.dart'; |  | ||||||
| import 'package:immich_mobile/utils/files_helper.dart'; | import 'package:immich_mobile/utils/files_helper.dart'; | ||||||
|  | import 'package:openapi/api.dart'; | ||||||
| import 'package:photo_manager/photo_manager.dart'; | import 'package:photo_manager/photo_manager.dart'; | ||||||
| import 'package:http_parser/http_parser.dart'; | import 'package:http_parser/http_parser.dart'; | ||||||
| import 'package:path/path.dart' as p; | import 'package:path/path.dart' as p; | ||||||
| import 'package:cancellation_token_http/http.dart' as http; | import 'package:cancellation_token_http/http.dart' as http; | ||||||
|  |  | ||||||
| final backupServiceProvider = | final backupServiceProvider = Provider( | ||||||
|     Provider((ref) => BackupService(ref.watch(networkServiceProvider))); |   (ref) => BackupService( | ||||||
|  |     ref.watch(apiServiceProvider), | ||||||
|  |   ), | ||||||
|  | ); | ||||||
|  |  | ||||||
| class BackupService { | class BackupService { | ||||||
|   final NetworkService _networkService; |   final ApiService _apiService; | ||||||
|  |   BackupService(this._apiService); | ||||||
|  |  | ||||||
|   BackupService(this._networkService); |   Future<List<String>?> getDeviceBackupAsset() async { | ||||||
|  |  | ||||||
|   Future<List<String>> getDeviceBackupAsset() async { |  | ||||||
|     String deviceId = Hive.box(userInfoBox).get(deviceIdKey); |  | ||||||
|  |  | ||||||
|     Response response = |  | ||||||
|         await _networkService.getRequest(url: "asset/$deviceId"); |  | ||||||
|     List<dynamic> result = jsonDecode(response.toString()); |  | ||||||
|  |  | ||||||
|     return result.cast<String>(); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   Future<bool> checkDuplicateAsset(String deviceAssetId) async { |  | ||||||
|     String deviceId = Hive.box(userInfoBox).get(deviceIdKey); |     String deviceId = Hive.box(userInfoBox).get(deviceIdKey); | ||||||
|  |  | ||||||
|     try { |     try { | ||||||
|       Response response = |       return await _apiService.assetApi.getUserAssetsByDeviceId(deviceId); | ||||||
|           await _networkService.postRequest(url: "asset/check", data: { |  | ||||||
|         "deviceId": deviceId, |  | ||||||
|         "deviceAssetId": deviceAssetId, |  | ||||||
|       }); |  | ||||||
|  |  | ||||||
|       if (response.statusCode == 200) { |  | ||||||
|         var result = CheckDuplicateAssetResponse.fromJson(response.toString()); |  | ||||||
|  |  | ||||||
|         return result.isExist; |  | ||||||
|       } else { |  | ||||||
|         return false; |  | ||||||
|       } |  | ||||||
|     } catch (e) { |     } catch (e) { | ||||||
|       return false; |       debugPrint('Error [getDeviceBackupAsset] ${e.toString()}'); | ||||||
|  |       return null; | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -99,9 +78,11 @@ class BackupService { | |||||||
|           var box = Hive.box(userInfoBox); |           var box = Hive.box(userInfoBox); | ||||||
|  |  | ||||||
|           var req = MultipartRequest( |           var req = MultipartRequest( | ||||||
|               'POST', Uri.parse('$savedEndpoint/asset/upload'), |             'POST', | ||||||
|               onProgress: ((bytes, totalBytes) => |             Uri.parse('$savedEndpoint/asset/upload'), | ||||||
|                   uploadProgressCb(bytes, totalBytes))); |             onProgress: ((bytes, totalBytes) => | ||||||
|  |                 uploadProgressCb(bytes, totalBytes)), | ||||||
|  |           ); | ||||||
|           req.headers["Authorization"] = "Bearer ${box.get(accessTokenKey)}"; |           req.headers["Authorization"] = "Bearer ${box.get(accessTokenKey)}"; | ||||||
|  |  | ||||||
|           req.fields['deviceAssetId'] = entity.id; |           req.fields['deviceAssetId'] = entity.id; | ||||||
| @@ -133,16 +114,19 @@ class BackupService { | |||||||
|             var error = jsonDecode(data); |             var error = jsonDecode(data); | ||||||
|  |  | ||||||
|             debugPrint( |             debugPrint( | ||||||
|                 "Error(${error['statusCode']}) uploading ${entity.id} | $originalFileName | Created on ${entity.createDateTime} | ${error['error']}"); |               "Error(${error['statusCode']}) uploading ${entity.id} | $originalFileName | Created on ${entity.createDateTime} | ${error['error']}", | ||||||
|  |             ); | ||||||
|  |  | ||||||
|             errorCb(ErrorUploadAsset( |             errorCb( | ||||||
|               asset: entity, |               ErrorUploadAsset( | ||||||
|               id: entity.id, |                 asset: entity, | ||||||
|               createdAt: entity.createDateTime, |                 id: entity.id, | ||||||
|               fileName: originalFileName, |                 createdAt: entity.createDateTime, | ||||||
|               fileType: _getAssetType(entity.type), |                 fileName: originalFileName, | ||||||
|               errorMessage: error['error'], |                 fileType: _getAssetType(entity.type), | ||||||
|             )); |                 errorMessage: error['error'], | ||||||
|  |               ), | ||||||
|  |             ); | ||||||
|             continue; |             continue; | ||||||
|           } |           } | ||||||
|         } |         } | ||||||
| @@ -160,8 +144,6 @@ class BackupService { | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   void sendBackupRequest(AssetEntity entity) {} |  | ||||||
|  |  | ||||||
|   String _getAssetType(AssetType assetType) { |   String _getAssetType(AssetType assetType) { | ||||||
|     switch (assetType) { |     switch (assetType) { | ||||||
|       case AssetType.audio: |       case AssetType.audio: | ||||||
| @@ -175,15 +157,29 @@ class BackupService { | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   Future<DeviceInfoRemote> setAutoBackup( |   Future<DeviceInfoResponseDto> setAutoBackup( | ||||||
|       bool status, String deviceId, String deviceType) async { |     bool status, | ||||||
|     var res = await _networkService.patchRequest(url: 'device-info', data: { |     String deviceId, | ||||||
|       "isAutoBackup": status, |     DeviceTypeEnum deviceType, | ||||||
|       "deviceId": deviceId, |   ) async { | ||||||
|       "deviceType": deviceType, |     try { | ||||||
|     }); |       var updatedDeviceInfo = await _apiService.deviceInfoApi.updateDeviceInfo( | ||||||
|  |         UpdateDeviceInfoDto( | ||||||
|  |           deviceId: deviceId, | ||||||
|  |           deviceType: deviceType, | ||||||
|  |           isAutoBackup: status, | ||||||
|  |         ), | ||||||
|  |       ); | ||||||
|  |  | ||||||
|     return DeviceInfoRemote.fromJson(res.toString()); |       if (updatedDeviceInfo == null) { | ||||||
|  |         throw Exception("Error updating device info"); | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       return updatedDeviceInfo; | ||||||
|  |     } catch (e) { | ||||||
|  |       debugPrint("Error setAutoBackup: ${e.toString()}"); | ||||||
|  |       throw Error(); | ||||||
|  |     } | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -26,7 +26,9 @@ class AlbumInfoCard extends HookConsumerWidget { | |||||||
|         ref.watch(backupProvider).excludedBackupAlbums.contains(albumInfo); |         ref.watch(backupProvider).excludedBackupAlbums.contains(albumInfo); | ||||||
|  |  | ||||||
|     ColorFilter selectedFilter = ColorFilter.mode( |     ColorFilter selectedFilter = ColorFilter.mode( | ||||||
|         Theme.of(context).primaryColor.withAlpha(100), BlendMode.darken); |       Theme.of(context).primaryColor.withAlpha(100), | ||||||
|  |       BlendMode.darken, | ||||||
|  |     ); | ||||||
|     ColorFilter excludedFilter = |     ColorFilter excludedFilter = | ||||||
|         ColorFilter.mode(Colors.red.withAlpha(75), BlendMode.darken); |         ColorFilter.mode(Colors.red.withAlpha(75), BlendMode.darken); | ||||||
|     ColorFilter unselectedFilter = |     ColorFilter unselectedFilter = | ||||||
| @@ -40,7 +42,10 @@ class AlbumInfoCard extends HookConsumerWidget { | |||||||
|           label: const Text( |           label: const Text( | ||||||
|             "album_info_card_backup_album_included", |             "album_info_card_backup_album_included", | ||||||
|             style: TextStyle( |             style: TextStyle( | ||||||
|                 fontSize: 10, color: Colors.white, fontWeight: FontWeight.bold), |               fontSize: 10, | ||||||
|  |               color: Colors.white, | ||||||
|  |               fontWeight: FontWeight.bold, | ||||||
|  |             ), | ||||||
|           ).tr(), |           ).tr(), | ||||||
|           backgroundColor: Theme.of(context).primaryColor, |           backgroundColor: Theme.of(context).primaryColor, | ||||||
|         ); |         ); | ||||||
| @@ -51,7 +56,10 @@ class AlbumInfoCard extends HookConsumerWidget { | |||||||
|           label: const Text( |           label: const Text( | ||||||
|             "album_info_card_backup_album_excluded", |             "album_info_card_backup_album_excluded", | ||||||
|             style: TextStyle( |             style: TextStyle( | ||||||
|                 fontSize: 10, color: Colors.white, fontWeight: FontWeight.bold), |               fontSize: 10, | ||||||
|  |               color: Colors.white, | ||||||
|  |               fontWeight: FontWeight.bold, | ||||||
|  |             ), | ||||||
|           ).tr(), |           ).tr(), | ||||||
|           backgroundColor: Colors.red[300], |           backgroundColor: Colors.red[300], | ||||||
|         ); |         ); | ||||||
| @@ -138,15 +146,16 @@ class AlbumInfoCard extends HookConsumerWidget { | |||||||
|                   height: 200, |                   height: 200, | ||||||
|                   decoration: BoxDecoration( |                   decoration: BoxDecoration( | ||||||
|                     borderRadius: const BorderRadius.only( |                     borderRadius: const BorderRadius.only( | ||||||
|                         topLeft: Radius.circular(12), |                       topLeft: Radius.circular(12), | ||||||
|                         topRight: Radius.circular(12)), |                       topRight: Radius.circular(12), | ||||||
|  |                     ), | ||||||
|                     image: DecorationImage( |                     image: DecorationImage( | ||||||
|                       colorFilter: _buildImageFilter(), |                       colorFilter: _buildImageFilter(), | ||||||
|                       image: imageData != null |                       image: imageData != null | ||||||
|                           ? MemoryImage(imageData!) |                           ? MemoryImage(imageData!) | ||||||
|                           : const AssetImage( |                           : const AssetImage( | ||||||
|                                   'assets/immich-logo-no-outline.png') |                               'assets/immich-logo-no-outline.png', | ||||||
|                               as ImageProvider, |                             ) as ImageProvider, | ||||||
|                       fit: BoxFit.cover, |                       fit: BoxFit.cover, | ||||||
|                     ), |                     ), | ||||||
|                   ), |                   ), | ||||||
| @@ -174,9 +183,10 @@ class AlbumInfoCard extends HookConsumerWidget { | |||||||
|                           Text( |                           Text( | ||||||
|                             albumInfo.name, |                             albumInfo.name, | ||||||
|                             style: TextStyle( |                             style: TextStyle( | ||||||
|                                 fontSize: 14, |                               fontSize: 14, | ||||||
|                                 color: Theme.of(context).primaryColor, |                               color: Theme.of(context).primaryColor, | ||||||
|                                 fontWeight: FontWeight.bold), |                               fontWeight: FontWeight.bold, | ||||||
|  |                             ), | ||||||
|                           ), |                           ), | ||||||
|                           Padding( |                           Padding( | ||||||
|                             padding: const EdgeInsets.only(top: 2.0), |                             padding: const EdgeInsets.only(top: 2.0), | ||||||
| @@ -186,7 +196,9 @@ class AlbumInfoCard extends HookConsumerWidget { | |||||||
|                                       ? " (${'backup_all'.tr()})" |                                       ? " (${'backup_all'.tr()})" | ||||||
|                                       : ""), |                                       : ""), | ||||||
|                               style: TextStyle( |                               style: TextStyle( | ||||||
|                                   fontSize: 12, color: Colors.grey[600]), |                                 fontSize: 12, | ||||||
|  |                                 color: Colors.grey[600], | ||||||
|  |                               ), | ||||||
|                             ), |                             ), | ||||||
|                           ) |                           ) | ||||||
|                         ], |                         ], | ||||||
|   | |||||||
| @@ -5,12 +5,12 @@ class BackupInfoCard extends StatelessWidget { | |||||||
|   final String title; |   final String title; | ||||||
|   final String subtitle; |   final String subtitle; | ||||||
|   final String info; |   final String info; | ||||||
|   const BackupInfoCard( |   const BackupInfoCard({ | ||||||
|       {Key? key, |     Key? key, | ||||||
|       required this.title, |     required this.title, | ||||||
|       required this.subtitle, |     required this.subtitle, | ||||||
|       required this.info}) |     required this.info, | ||||||
|       : super(key: key); |   }) : super(key: key); | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   Widget build(BuildContext context) { |   Widget build(BuildContext context) { | ||||||
|   | |||||||
| @@ -20,10 +20,13 @@ class AlbumPreviewPage extends HookConsumerWidget { | |||||||
|           await album.getAssetListRange(start: 0, end: album.assetCount); |           await album.getAssetListRange(start: 0, end: album.assetCount); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     useEffect(() { |     useEffect( | ||||||
|       _getAssetsInAlbum(); |       () { | ||||||
|       return null; |         _getAssetsInAlbum(); | ||||||
|     }, []); |         return null; | ||||||
|  |       }, | ||||||
|  |       [], | ||||||
|  |     ); | ||||||
|  |  | ||||||
|     return Scaffold( |     return Scaffold( | ||||||
|       appBar: AppBar( |       appBar: AppBar( | ||||||
| @@ -39,9 +42,10 @@ class AlbumPreviewPage extends HookConsumerWidget { | |||||||
|               child: Text( |               child: Text( | ||||||
|                 "ID ${album.id}", |                 "ID ${album.id}", | ||||||
|                 style: TextStyle( |                 style: TextStyle( | ||||||
|                     fontSize: 10, |                   fontSize: 10, | ||||||
|                     color: Colors.grey[600], |                   color: Colors.grey[600], | ||||||
|                     fontWeight: FontWeight.bold), |                   fontWeight: FontWeight.bold, | ||||||
|  |                 ), | ||||||
|               ), |               ), | ||||||
|             ), |             ), | ||||||
|           ], |           ], | ||||||
| @@ -59,9 +63,11 @@ class AlbumPreviewPage extends HookConsumerWidget { | |||||||
|         ), |         ), | ||||||
|         itemCount: assets.value.length, |         itemCount: assets.value.length, | ||||||
|         itemBuilder: (context, index) { |         itemBuilder: (context, index) { | ||||||
|           Future<Uint8List?> thumbData = assets.value[index] |           Future<Uint8List?> thumbData = | ||||||
|               .thumbnailDataWithSize(const ThumbnailSize(200, 200), |               assets.value[index].thumbnailDataWithSize( | ||||||
|                   quality: 50); |             const ThumbnailSize(200, 200), | ||||||
|  |             quality: 50, | ||||||
|  |           ); | ||||||
|  |  | ||||||
|           return FutureBuilder<Uint8List?>( |           return FutureBuilder<Uint8List?>( | ||||||
|             future: thumbData, |             future: thumbData, | ||||||
|   | |||||||
| @@ -17,10 +17,13 @@ class BackupAlbumSelectionPage extends HookConsumerWidget { | |||||||
|     final selectedBackupAlbums = ref.watch(backupProvider).selectedBackupAlbums; |     final selectedBackupAlbums = ref.watch(backupProvider).selectedBackupAlbums; | ||||||
|     final excludedBackupAlbums = ref.watch(backupProvider).excludedBackupAlbums; |     final excludedBackupAlbums = ref.watch(backupProvider).excludedBackupAlbums; | ||||||
|  |  | ||||||
|     useEffect(() { |     useEffect( | ||||||
|       ref.read(backupProvider.notifier).getBackupInfo(); |       () { | ||||||
|       return null; |         ref.read(backupProvider.notifier).getBackupInfo(); | ||||||
|     }, []); |         return null; | ||||||
|  |       }, | ||||||
|  |       [], | ||||||
|  |     ); | ||||||
|  |  | ||||||
|     _buildAlbumSelectionList() { |     _buildAlbumSelectionList() { | ||||||
|       if (availableAlbums.isEmpty) { |       if (availableAlbums.isEmpty) { | ||||||
| @@ -42,8 +45,9 @@ class BackupAlbumSelectionPage extends HookConsumerWidget { | |||||||
|                   ? const EdgeInsets.only(left: 16.00) |                   ? const EdgeInsets.only(left: 16.00) | ||||||
|                   : const EdgeInsets.all(0), |                   : const EdgeInsets.all(0), | ||||||
|               child: AlbumInfoCard( |               child: AlbumInfoCard( | ||||||
|                   imageData: thumbnailData, |                 imageData: thumbnailData, | ||||||
|                   albumInfo: availableAlbums[index].albumEntity), |                 albumInfo: availableAlbums[index].albumEntity, | ||||||
|  |               ), | ||||||
|             ); |             ); | ||||||
|           }), |           }), | ||||||
|         ), |         ), | ||||||
| @@ -73,13 +77,15 @@ class BackupAlbumSelectionPage extends HookConsumerWidget { | |||||||
|             child: Chip( |             child: Chip( | ||||||
|               visualDensity: VisualDensity.compact, |               visualDensity: VisualDensity.compact, | ||||||
|               shape: RoundedRectangleBorder( |               shape: RoundedRectangleBorder( | ||||||
|                   borderRadius: BorderRadius.circular(5)), |                 borderRadius: BorderRadius.circular(5), | ||||||
|  |               ), | ||||||
|               label: Text( |               label: Text( | ||||||
|                 album.name, |                 album.name, | ||||||
|                 style: const TextStyle( |                 style: const TextStyle( | ||||||
|                     fontSize: 10, |                   fontSize: 10, | ||||||
|                     color: Colors.white, |                   color: Colors.white, | ||||||
|                     fontWeight: FontWeight.bold), |                   fontWeight: FontWeight.bold, | ||||||
|  |                 ), | ||||||
|               ), |               ), | ||||||
|               backgroundColor: Theme.of(context).primaryColor, |               backgroundColor: Theme.of(context).primaryColor, | ||||||
|               deleteIconColor: Colors.white, |               deleteIconColor: Colors.white, | ||||||
| @@ -109,13 +115,15 @@ class BackupAlbumSelectionPage extends HookConsumerWidget { | |||||||
|             child: Chip( |             child: Chip( | ||||||
|               visualDensity: VisualDensity.compact, |               visualDensity: VisualDensity.compact, | ||||||
|               shape: RoundedRectangleBorder( |               shape: RoundedRectangleBorder( | ||||||
|                   borderRadius: BorderRadius.circular(5)), |                 borderRadius: BorderRadius.circular(5), | ||||||
|  |               ), | ||||||
|               label: Text( |               label: Text( | ||||||
|                 album.name, |                 album.name, | ||||||
|                 style: const TextStyle( |                 style: const TextStyle( | ||||||
|                     fontSize: 10, |                   fontSize: 10, | ||||||
|                     color: Colors.white, |                   color: Colors.white, | ||||||
|                     fontWeight: FontWeight.bold), |                   fontWeight: FontWeight.bold, | ||||||
|  |                 ), | ||||||
|               ), |               ), | ||||||
|               backgroundColor: Colors.red[300], |               backgroundColor: Colors.red[300], | ||||||
|               deleteIconColor: Colors.white, |               deleteIconColor: Colors.white, | ||||||
| @@ -185,9 +193,10 @@ class BackupAlbumSelectionPage extends HookConsumerWidget { | |||||||
|                     title: Text( |                     title: Text( | ||||||
|                       "backup_album_selection_page_total_assets", |                       "backup_album_selection_page_total_assets", | ||||||
|                       style: TextStyle( |                       style: TextStyle( | ||||||
|                           fontWeight: FontWeight.bold, |                         fontWeight: FontWeight.bold, | ||||||
|                           fontSize: 14, |                         fontSize: 14, | ||||||
|                           color: Colors.grey[700]), |                         color: Colors.grey[700], | ||||||
|  |                       ), | ||||||
|                     ).tr(), |                     ).tr(), | ||||||
|                     trailing: Text( |                     trailing: Text( | ||||||
|                       ref |                       ref | ||||||
| @@ -234,7 +243,8 @@ class BackupAlbumSelectionPage extends HookConsumerWidget { | |||||||
|                   builder: (BuildContext context) { |                   builder: (BuildContext context) { | ||||||
|                     return AlertDialog( |                     return AlertDialog( | ||||||
|                       shape: RoundedRectangleBorder( |                       shape: RoundedRectangleBorder( | ||||||
|                           borderRadius: BorderRadius.circular(12)), |                         borderRadius: BorderRadius.circular(12), | ||||||
|  |                       ), | ||||||
|                       elevation: 5, |                       elevation: 5, | ||||||
|                       title: Text( |                       title: Text( | ||||||
|                         'backup_album_selection_page_selection_info', |                         'backup_album_selection_page_selection_info', | ||||||
| @@ -250,7 +260,9 @@ class BackupAlbumSelectionPage extends HookConsumerWidget { | |||||||
|                             Text( |                             Text( | ||||||
|                               'backup_album_selection_page_assets_scatter', |                               'backup_album_selection_page_assets_scatter', | ||||||
|                               style: TextStyle( |                               style: TextStyle( | ||||||
|                                   fontSize: 14, color: Colors.grey[700]), |                                 fontSize: 14, | ||||||
|  |                                 color: Colors.grey[700], | ||||||
|  |                               ), | ||||||
|                             ).tr(), |                             ).tr(), | ||||||
|                           ], |                           ], | ||||||
|                         ), |                         ), | ||||||
|   | |||||||
| @@ -11,7 +11,6 @@ import 'package:immich_mobile/modules/backup/providers/backup.provider.dart'; | |||||||
| import 'package:immich_mobile/routing/router.dart'; | import 'package:immich_mobile/routing/router.dart'; | ||||||
| import 'package:immich_mobile/shared/providers/websocket.provider.dart'; | import 'package:immich_mobile/shared/providers/websocket.provider.dart'; | ||||||
| import 'package:immich_mobile/modules/backup/ui/backup_info_card.dart'; | import 'package:immich_mobile/modules/backup/ui/backup_info_card.dart'; | ||||||
| import 'package:intl/intl.dart'; |  | ||||||
| import 'package:percent_indicator/linear_percent_indicator.dart'; | import 'package:percent_indicator/linear_percent_indicator.dart'; | ||||||
|  |  | ||||||
| class BackupControllerPage extends HookConsumerWidget { | class BackupControllerPage extends HookConsumerWidget { | ||||||
| @@ -27,16 +26,19 @@ class BackupControllerPage extends HookConsumerWidget { | |||||||
|         ? false |         ? false | ||||||
|         : true; |         : true; | ||||||
|  |  | ||||||
|     useEffect(() { |     useEffect( | ||||||
|       if (backupState.backupProgress != BackUpProgressEnum.inProgress) { |       () { | ||||||
|         ref.watch(backupProvider.notifier).getBackupInfo(); |         if (backupState.backupProgress != BackUpProgressEnum.inProgress) { | ||||||
|       } |           ref.watch(backupProvider.notifier).getBackupInfo(); | ||||||
|  |         } | ||||||
|  |  | ||||||
|       ref |         ref | ||||||
|           .watch(websocketProvider.notifier) |             .watch(websocketProvider.notifier) | ||||||
|           .stopListenToEvent('on_upload_success'); |             .stopListenToEvent('on_upload_success'); | ||||||
|       return null; |         return null; | ||||||
|     }, []); |       }, | ||||||
|  |       [], | ||||||
|  |     ); | ||||||
|  |  | ||||||
|     Widget _buildStorageInformation() { |     Widget _buildStorageInformation() { | ||||||
|       return ListTile( |       return ListTile( | ||||||
| @@ -68,10 +70,11 @@ class BackupControllerPage extends HookConsumerWidget { | |||||||
|               Padding( |               Padding( | ||||||
|                 padding: const EdgeInsets.only(top: 12.0), |                 padding: const EdgeInsets.only(top: 12.0), | ||||||
|                 child: const Text('backup_controller_page_storage_format').tr( |                 child: const Text('backup_controller_page_storage_format').tr( | ||||||
|                     args: [ |                   args: [ | ||||||
|                       backupState.serverInfo.diskUse, |                     backupState.serverInfo.diskUse, | ||||||
|                       backupState.serverInfo.diskSize |                     backupState.serverInfo.diskSize | ||||||
|                     ]), |                   ], | ||||||
|  |                 ), | ||||||
|               ), |               ), | ||||||
|             ], |             ], | ||||||
|           ), |           ), | ||||||
| @@ -129,8 +132,10 @@ class BackupControllerPage extends HookConsumerWidget { | |||||||
|                           .setAutoBackup(true); |                           .setAutoBackup(true); | ||||||
|                     } |                     } | ||||||
|                   }, |                   }, | ||||||
|                   child: Text(backupBtnText, |                   child: Text( | ||||||
|                       style: const TextStyle(fontWeight: FontWeight.bold)), |                     backupBtnText, | ||||||
|  |                     style: const TextStyle(fontWeight: FontWeight.bold), | ||||||
|  |                   ), | ||||||
|                 ), |                 ), | ||||||
|               ) |               ) | ||||||
|             ], |             ], | ||||||
| @@ -157,9 +162,10 @@ class BackupControllerPage extends HookConsumerWidget { | |||||||
|           child: Text( |           child: Text( | ||||||
|             text.trim().substring(0, text.length - 2), |             text.trim().substring(0, text.length - 2), | ||||||
|             style: TextStyle( |             style: TextStyle( | ||||||
|                 color: Theme.of(context).primaryColor, |               color: Theme.of(context).primaryColor, | ||||||
|                 fontSize: 12, |               fontSize: 12, | ||||||
|                 fontWeight: FontWeight.bold), |               fontWeight: FontWeight.bold, | ||||||
|  |             ), | ||||||
|           ), |           ), | ||||||
|         ); |         ); | ||||||
|       } else { |       } else { | ||||||
| @@ -168,9 +174,10 @@ class BackupControllerPage extends HookConsumerWidget { | |||||||
|           child: Text( |           child: Text( | ||||||
|             "backup_controller_page_none_selected".tr(), |             "backup_controller_page_none_selected".tr(), | ||||||
|             style: TextStyle( |             style: TextStyle( | ||||||
|                 color: Theme.of(context).primaryColor, |               color: Theme.of(context).primaryColor, | ||||||
|                 fontSize: 12, |               fontSize: 12, | ||||||
|                 fontWeight: FontWeight.bold), |               fontWeight: FontWeight.bold, | ||||||
|  |             ), | ||||||
|           ), |           ), | ||||||
|         ); |         ); | ||||||
|       } |       } | ||||||
| @@ -190,9 +197,10 @@ class BackupControllerPage extends HookConsumerWidget { | |||||||
|           child: Text( |           child: Text( | ||||||
|             text.trim().substring(0, text.length - 2), |             text.trim().substring(0, text.length - 2), | ||||||
|             style: TextStyle( |             style: TextStyle( | ||||||
|                 color: Colors.red[300], |               color: Colors.red[300], | ||||||
|                 fontSize: 12, |               fontSize: 12, | ||||||
|                 fontWeight: FontWeight.bold), |               fontWeight: FontWeight.bold, | ||||||
|  |             ), | ||||||
|           ), |           ), | ||||||
|         ); |         ); | ||||||
|       } else { |       } else { | ||||||
| @@ -213,9 +221,10 @@ class BackupControllerPage extends HookConsumerWidget { | |||||||
|         borderOnForeground: false, |         borderOnForeground: false, | ||||||
|         child: ListTile( |         child: ListTile( | ||||||
|           minVerticalPadding: 15, |           minVerticalPadding: 15, | ||||||
|           title: const Text("backup_controller_page_albums", |           title: const Text( | ||||||
|                   style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20)) |             "backup_controller_page_albums", | ||||||
|               .tr(), |             style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20), | ||||||
|  |           ).tr(), | ||||||
|           subtitle: Padding( |           subtitle: Padding( | ||||||
|             padding: const EdgeInsets.only(top: 8.0), |             padding: const EdgeInsets.only(top: 8.0), | ||||||
|             child: Column( |             child: Column( | ||||||
| @@ -284,9 +293,9 @@ class BackupControllerPage extends HookConsumerWidget { | |||||||
|                     fontWeight: FontWeight.bold, |                     fontWeight: FontWeight.bold, | ||||||
|                     fontSize: 11, |                     fontSize: 11, | ||||||
|                   ), |                   ), | ||||||
|                 ).tr(args: [ |                 ).tr( | ||||||
|                   ref.watch(errorBackupListProvider).length.toString() |                   args: [ref.watch(errorBackupListProvider).length.toString()], | ||||||
|                 ]), |                 ), | ||||||
|                 backgroundColor: Colors.white, |                 backgroundColor: Colors.white, | ||||||
|                 onPressed: () { |                 onPressed: () { | ||||||
|                   AutoRouter.of(context).push(const FailedBackupStatusRoute()); |                   AutoRouter.of(context).push(const FailedBackupStatusRoute()); | ||||||
| @@ -331,12 +340,16 @@ class BackupControllerPage extends HookConsumerWidget { | |||||||
|                           child: const Text( |                           child: const Text( | ||||||
|                             'backup_controller_page_filename', |                             'backup_controller_page_filename', | ||||||
|                             style: TextStyle( |                             style: TextStyle( | ||||||
|                                 fontWeight: FontWeight.bold, fontSize: 10.0), |                               fontWeight: FontWeight.bold, | ||||||
|                           ).tr(args: [ |                               fontSize: 10.0, | ||||||
|                             backupState.currentUploadAsset.fileName, |                             ), | ||||||
|                             backupState.currentUploadAsset.fileType |                           ).tr( | ||||||
|                                 .toLowerCase() |                             args: [ | ||||||
|                           ]), |                               backupState.currentUploadAsset.fileName, | ||||||
|  |                               backupState.currentUploadAsset.fileType | ||||||
|  |                                   .toLowerCase() | ||||||
|  |                             ], | ||||||
|  |                           ), | ||||||
|                         ), |                         ), | ||||||
|                       ), |                       ), | ||||||
|                     ], |                     ], | ||||||
| @@ -352,16 +365,20 @@ class BackupControllerPage extends HookConsumerWidget { | |||||||
|                           padding: const EdgeInsets.all(6.0), |                           padding: const EdgeInsets.all(6.0), | ||||||
|                           child: const Text( |                           child: const Text( | ||||||
|                             "backup_controller_page_created", |                             "backup_controller_page_created", | ||||||
|                             style: const TextStyle( |                             style: TextStyle( | ||||||
|                                 fontWeight: FontWeight.bold, fontSize: 10.0), |                               fontWeight: FontWeight.bold, | ||||||
|                           ).tr(args: [ |                               fontSize: 10.0, | ||||||
|                             DateFormat.yMMMMd('en_US').format( |                             ), | ||||||
|                               DateTime.parse( |                           ).tr( | ||||||
|                                 backupState.currentUploadAsset.createdAt |                             args: [ | ||||||
|                                     .toString(), |                               DateFormat.yMMMMd('en_US').format( | ||||||
|                               ), |                                 DateTime.parse( | ||||||
|                             ) |                                   backupState.currentUploadAsset.createdAt | ||||||
|                           ]), |                                       .toString(), | ||||||
|  |                                 ), | ||||||
|  |                               ) | ||||||
|  |                             ], | ||||||
|  |                           ), | ||||||
|                         ), |                         ), | ||||||
|                       ), |                       ), | ||||||
|                     ], |                     ], | ||||||
| @@ -406,14 +423,15 @@ class BackupControllerPage extends HookConsumerWidget { | |||||||
|           style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold), |           style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold), | ||||||
|         ).tr(), |         ).tr(), | ||||||
|         leading: IconButton( |         leading: IconButton( | ||||||
|             onPressed: () { |           onPressed: () { | ||||||
|               ref.watch(websocketProvider.notifier).listenUploadEvent(); |             ref.watch(websocketProvider.notifier).listenUploadEvent(); | ||||||
|               AutoRouter.of(context).pop(true); |             AutoRouter.of(context).pop(true); | ||||||
|             }, |           }, | ||||||
|             splashRadius: 24, |           splashRadius: 24, | ||||||
|             icon: const Icon( |           icon: const Icon( | ||||||
|               Icons.arrow_back_ios_rounded, |             Icons.arrow_back_ios_rounded, | ||||||
|             )), |           ), | ||||||
|  |         ), | ||||||
|       ), |       ), | ||||||
|       body: Padding( |       body: Padding( | ||||||
|         padding: const EdgeInsets.only(left: 16.0, right: 16, bottom: 32), |         padding: const EdgeInsets.only(left: 16.0, right: 16, bottom: 32), | ||||||
|   | |||||||
| @@ -19,13 +19,14 @@ class FailedBackupStatusPage extends HookConsumerWidget { | |||||||
|           style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold), |           style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold), | ||||||
|         ), |         ), | ||||||
|         leading: IconButton( |         leading: IconButton( | ||||||
|             onPressed: () { |           onPressed: () { | ||||||
|               AutoRouter.of(context).pop(true); |             AutoRouter.of(context).pop(true); | ||||||
|             }, |           }, | ||||||
|             splashRadius: 24, |           splashRadius: 24, | ||||||
|             icon: const Icon( |           icon: const Icon( | ||||||
|               Icons.arrow_back_ios_rounded, |             Icons.arrow_back_ios_rounded, | ||||||
|             )), |           ), | ||||||
|  |         ), | ||||||
|       ), |       ), | ||||||
|       body: ListView.builder( |       body: ListView.builder( | ||||||
|         shrinkWrap: true, |         shrinkWrap: true, | ||||||
| @@ -92,9 +93,10 @@ class FailedBackupStatusPage extends HookConsumerWidget { | |||||||
|                                   ), |                                   ), | ||||||
|                                 ), |                                 ), | ||||||
|                                 style: TextStyle( |                                 style: TextStyle( | ||||||
|                                     fontSize: 12, |                                   fontSize: 12, | ||||||
|                                     fontWeight: FontWeight.w600, |                                   fontWeight: FontWeight.w600, | ||||||
|                                     color: Colors.grey[700]), |                                   color: Colors.grey[700], | ||||||
|  |                                 ), | ||||||
|                               ), |                               ), | ||||||
|                               Icon( |                               Icon( | ||||||
|                                 Icons.error, |                                 Icons.error, | ||||||
|   | |||||||
| @@ -1,55 +0,0 @@ | |||||||
| import 'dart:convert'; |  | ||||||
|  |  | ||||||
| class DeleteAssetResponse { |  | ||||||
|   final String id; |  | ||||||
|   final String status; |  | ||||||
|  |  | ||||||
|   DeleteAssetResponse({ |  | ||||||
|     required this.id, |  | ||||||
|     required this.status, |  | ||||||
|   }); |  | ||||||
|  |  | ||||||
|   DeleteAssetResponse copyWith({ |  | ||||||
|     String? id, |  | ||||||
|     String? status, |  | ||||||
|   }) { |  | ||||||
|     return DeleteAssetResponse( |  | ||||||
|       id: id ?? this.id, |  | ||||||
|       status: status ?? this.status, |  | ||||||
|     ); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   Map<String, dynamic> toMap() { |  | ||||||
|     return { |  | ||||||
|       'id': id, |  | ||||||
|       'status': status, |  | ||||||
|     }; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   factory DeleteAssetResponse.fromMap(Map<String, dynamic> map) { |  | ||||||
|     return DeleteAssetResponse( |  | ||||||
|       id: map['id'] ?? '', |  | ||||||
|       status: map['status'] ?? '', |  | ||||||
|     ); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   String toJson() => json.encode(toMap()); |  | ||||||
|  |  | ||||||
|   factory DeleteAssetResponse.fromJson(String source) => |  | ||||||
|       DeleteAssetResponse.fromMap(json.decode(source)); |  | ||||||
|  |  | ||||||
|   @override |  | ||||||
|   String toString() => 'DeleteAssetResponse(id: $id, status: $status)'; |  | ||||||
|  |  | ||||||
|   @override |  | ||||||
|   bool operator ==(Object other) { |  | ||||||
|     if (identical(this, other)) return true; |  | ||||||
|  |  | ||||||
|     return other is DeleteAssetResponse && |  | ||||||
|         other.id == id && |  | ||||||
|         other.status == status; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   @override |  | ||||||
|   int get hashCode => id.hashCode ^ status.hashCode; |  | ||||||
| } |  | ||||||
| @@ -1,11 +1,9 @@ | |||||||
| import 'dart:convert'; |  | ||||||
|  |  | ||||||
| import 'package:flutter/foundation.dart'; | import 'package:flutter/foundation.dart'; | ||||||
| import 'package:immich_mobile/shared/models/immich_asset.model.dart'; | import 'package:openapi/api.dart'; | ||||||
|  |  | ||||||
| class ImmichAssetGroupByDate { | class ImmichAssetGroupByDate { | ||||||
|   final String date; |   final String date; | ||||||
|   List<ImmichAsset> assets; |   List<AssetResponseDto> assets; | ||||||
|   ImmichAssetGroupByDate({ |   ImmichAssetGroupByDate({ | ||||||
|     required this.date, |     required this.date, | ||||||
|     required this.assets, |     required this.assets, | ||||||
| @@ -13,7 +11,7 @@ class ImmichAssetGroupByDate { | |||||||
|  |  | ||||||
|   ImmichAssetGroupByDate copyWith({ |   ImmichAssetGroupByDate copyWith({ | ||||||
|     String? date, |     String? date, | ||||||
|     List<ImmichAsset>? assets, |     List<AssetResponseDto>? assets, | ||||||
|   }) { |   }) { | ||||||
|     return ImmichAssetGroupByDate( |     return ImmichAssetGroupByDate( | ||||||
|       date: date ?? this.date, |       date: date ?? this.date, | ||||||
| @@ -21,26 +19,6 @@ class ImmichAssetGroupByDate { | |||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   Map<String, dynamic> toMap() { |  | ||||||
|     return { |  | ||||||
|       'date': date, |  | ||||||
|       'assets': assets.map((x) => x.toMap()).toList(), |  | ||||||
|     }; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   factory ImmichAssetGroupByDate.fromMap(Map<String, dynamic> map) { |  | ||||||
|     return ImmichAssetGroupByDate( |  | ||||||
|       date: map['date'] ?? '', |  | ||||||
|       assets: List<ImmichAsset>.from( |  | ||||||
|           map['assets']?.map((x) => ImmichAsset.fromMap(x))), |  | ||||||
|     ); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   String toJson() => json.encode(toMap()); |  | ||||||
|  |  | ||||||
|   factory ImmichAssetGroupByDate.fromJson(String source) => |  | ||||||
|       ImmichAssetGroupByDate.fromMap(json.decode(source)); |  | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   String toString() => 'ImmichAssetGroupByDate(date: $date, assets: $assets)'; |   String toString() => 'ImmichAssetGroupByDate(date: $date, assets: $assets)'; | ||||||
|  |  | ||||||
| @@ -79,28 +57,6 @@ class GetAllAssetResponse { | |||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   Map<String, dynamic> toMap() { |  | ||||||
|     return { |  | ||||||
|       'count': count, |  | ||||||
|       'data': data.map((x) => x.toMap()).toList(), |  | ||||||
|       'nextPageKey': nextPageKey, |  | ||||||
|     }; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   factory GetAllAssetResponse.fromMap(Map<String, dynamic> map) { |  | ||||||
|     return GetAllAssetResponse( |  | ||||||
|       count: map['count']?.toInt() ?? 0, |  | ||||||
|       data: List<ImmichAssetGroupByDate>.from( |  | ||||||
|           map['data']?.map((x) => ImmichAssetGroupByDate.fromMap(x))), |  | ||||||
|       nextPageKey: map['nextPageKey'] ?? '', |  | ||||||
|     ); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   String toJson() => json.encode(toMap()); |  | ||||||
|  |  | ||||||
|   factory GetAllAssetResponse.fromJson(String source) => |  | ||||||
|       GetAllAssetResponse.fromMap(json.decode(source)); |  | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   String toString() => |   String toString() => | ||||||
|       'GetAllAssetResponse(count: $count, data: $data, nextPageKey: $nextPageKey)'; |       'GetAllAssetResponse(count: $count, data: $data, nextPageKey: $nextPageKey)'; | ||||||
|   | |||||||
| @@ -1,12 +1,10 @@ | |||||||
| import 'dart:convert'; |  | ||||||
|  |  | ||||||
| import 'package:collection/collection.dart'; | import 'package:collection/collection.dart'; | ||||||
|  |  | ||||||
| import 'package:immich_mobile/shared/models/immich_asset.model.dart'; | import 'package:openapi/api.dart'; | ||||||
|  |  | ||||||
| class HomePageState { | class HomePageState { | ||||||
|   final bool isMultiSelectEnable; |   final bool isMultiSelectEnable; | ||||||
|   final Set<ImmichAsset> selectedItems; |   final Set<AssetResponseDto> selectedItems; | ||||||
|   final Set<String> selectedDateGroup; |   final Set<String> selectedDateGroup; | ||||||
|   HomePageState({ |   HomePageState({ | ||||||
|     required this.isMultiSelectEnable, |     required this.isMultiSelectEnable, | ||||||
| @@ -16,7 +14,7 @@ class HomePageState { | |||||||
|  |  | ||||||
|   HomePageState copyWith({ |   HomePageState copyWith({ | ||||||
|     bool? isMultiSelectEnable, |     bool? isMultiSelectEnable, | ||||||
|     Set<ImmichAsset>? selectedItems, |     Set<AssetResponseDto>? selectedItems, | ||||||
|     Set<String>? selectedDateGroup, |     Set<String>? selectedDateGroup, | ||||||
|   }) { |   }) { | ||||||
|     return HomePageState( |     return HomePageState( | ||||||
| @@ -26,28 +24,6 @@ class HomePageState { | |||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   Map<String, dynamic> toMap() { |  | ||||||
|     return { |  | ||||||
|       'isMultiSelectEnable': isMultiSelectEnable, |  | ||||||
|       'selectedItems': selectedItems.map((x) => x.toMap()).toList(), |  | ||||||
|       'selectedDateGroup': selectedDateGroup.toList(), |  | ||||||
|     }; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   factory HomePageState.fromMap(Map<String, dynamic> map) { |  | ||||||
|     return HomePageState( |  | ||||||
|       isMultiSelectEnable: map['isMultiSelectEnable'] ?? false, |  | ||||||
|       selectedItems: Set<ImmichAsset>.from( |  | ||||||
|           map['selectedItems']?.map((x) => ImmichAsset.fromMap(x))), |  | ||||||
|       selectedDateGroup: Set<String>.from(map['selectedDateGroup']), |  | ||||||
|     ); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   String toJson() => json.encode(toMap()); |  | ||||||
|  |  | ||||||
|   factory HomePageState.fromJson(String source) => |  | ||||||
|       HomePageState.fromMap(json.decode(source)); |  | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   String toString() => |   String toString() => | ||||||
|       'HomePageState(isMultiSelectEnable: $isMultiSelectEnable, selectedItems: $selectedItems, selectedDateGroup: $selectedDateGroup)'; |       'HomePageState(isMultiSelectEnable: $isMultiSelectEnable, selectedItems: $selectedItems, selectedDateGroup: $selectedDateGroup)'; | ||||||
|   | |||||||
| @@ -1,6 +1,6 @@ | |||||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||||
| import 'package:immich_mobile/modules/home/models/home_page_state.model.dart'; | import 'package:immich_mobile/modules/home/models/home_page_state.model.dart'; | ||||||
| import 'package:immich_mobile/shared/models/immich_asset.model.dart'; | import 'package:openapi/api.dart'; | ||||||
|  |  | ||||||
| class HomePageStateNotifier extends StateNotifier<HomePageState> { | class HomePageStateNotifier extends StateNotifier<HomePageState> { | ||||||
|   HomePageStateNotifier() |   HomePageStateNotifier() | ||||||
| @@ -14,7 +14,8 @@ class HomePageStateNotifier extends StateNotifier<HomePageState> { | |||||||
|  |  | ||||||
|   void addSelectedDateGroup(String dateGroupTitle) { |   void addSelectedDateGroup(String dateGroupTitle) { | ||||||
|     state = state.copyWith( |     state = state.copyWith( | ||||||
|         selectedDateGroup: {...state.selectedDateGroup, dateGroupTitle}); |       selectedDateGroup: {...state.selectedDateGroup, dateGroupTitle}, | ||||||
|  |     ); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   void removeSelectedDateGroup(String dateGroupTitle) { |   void removeSelectedDateGroup(String dateGroupTitle) { | ||||||
| @@ -25,36 +26,39 @@ class HomePageStateNotifier extends StateNotifier<HomePageState> { | |||||||
|     state = state.copyWith(selectedDateGroup: currentDateGroup); |     state = state.copyWith(selectedDateGroup: currentDateGroup); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   void enableMultiSelect(Set<ImmichAsset> selectedItems) { |   void enableMultiSelect(Set<AssetResponseDto> selectedItems) { | ||||||
|     state = |     state = | ||||||
|         state.copyWith(isMultiSelectEnable: true, selectedItems: selectedItems); |         state.copyWith(isMultiSelectEnable: true, selectedItems: selectedItems); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   void disableMultiSelect() { |   void disableMultiSelect() { | ||||||
|     state = state.copyWith( |     state = state.copyWith( | ||||||
|         isMultiSelectEnable: false, selectedItems: {}, selectedDateGroup: {}); |       isMultiSelectEnable: false, | ||||||
|  |       selectedItems: {}, | ||||||
|  |       selectedDateGroup: {}, | ||||||
|  |     ); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   void addSingleSelectedItem(ImmichAsset asset) { |   void addSingleSelectedItem(AssetResponseDto asset) { | ||||||
|     state = state.copyWith(selectedItems: {...state.selectedItems, asset}); |     state = state.copyWith(selectedItems: {...state.selectedItems, asset}); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   void addMultipleSelectedItems(List<ImmichAsset> assets) { |   void addMultipleSelectedItems(List<AssetResponseDto> assets) { | ||||||
|     state = state.copyWith(selectedItems: {...state.selectedItems, ...assets}); |     state = state.copyWith(selectedItems: {...state.selectedItems, ...assets}); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   void removeSingleSelectedItem(ImmichAsset asset) { |   void removeSingleSelectedItem(AssetResponseDto asset) { | ||||||
|     Set<ImmichAsset> currentList = state.selectedItems; |     Set<AssetResponseDto> currentList = state.selectedItems; | ||||||
|  |  | ||||||
|     currentList.removeWhere((e) => e.id == asset.id); |     currentList.removeWhere((e) => e.id == asset.id); | ||||||
|  |  | ||||||
|     state = state.copyWith(selectedItems: currentList); |     state = state.copyWith(selectedItems: currentList); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   void removeMultipleSelectedItem(List<ImmichAsset> assets) { |   void removeMultipleSelectedItem(List<AssetResponseDto> assets) { | ||||||
|     Set<ImmichAsset> currentList = state.selectedItems; |     Set<AssetResponseDto> currentList = state.selectedItems; | ||||||
|  |  | ||||||
|     for (ImmichAsset asset in assets) { |     for (AssetResponseDto asset in assets) { | ||||||
|       currentList.removeWhere((e) => e.id == asset.id); |       currentList.removeWhere((e) => e.id == asset.id); | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -64,4 +68,5 @@ class HomePageStateNotifier extends StateNotifier<HomePageState> { | |||||||
|  |  | ||||||
| final homePageStateProvider = | final homePageStateProvider = | ||||||
|     StateNotifierProvider<HomePageStateNotifier, HomePageState>( |     StateNotifierProvider<HomePageStateNotifier, HomePageState>( | ||||||
|         ((ref) => HomePageStateNotifier())); |   ((ref) => HomePageStateNotifier()), | ||||||
|  | ); | ||||||
|   | |||||||
| @@ -73,10 +73,12 @@ class UploadProfileImageState { | |||||||
| class UploadProfileImageNotifier | class UploadProfileImageNotifier | ||||||
|     extends StateNotifier<UploadProfileImageState> { |     extends StateNotifier<UploadProfileImageState> { | ||||||
|   UploadProfileImageNotifier(this._userSErvice) |   UploadProfileImageNotifier(this._userSErvice) | ||||||
|       : super(UploadProfileImageState( |       : super( | ||||||
|           profileImagePath: '', |           UploadProfileImageState( | ||||||
|           status: UploadProfileStatus.idle, |             profileImagePath: '', | ||||||
|         )); |             status: UploadProfileStatus.idle, | ||||||
|  |           ), | ||||||
|  |         ); | ||||||
|  |  | ||||||
|   final UserService _userSErvice; |   final UserService _userSErvice; | ||||||
|  |  | ||||||
| @@ -88,8 +90,9 @@ class UploadProfileImageNotifier | |||||||
|     if (res != null) { |     if (res != null) { | ||||||
|       debugPrint("Succesfully upload profile image"); |       debugPrint("Succesfully upload profile image"); | ||||||
|       state = state.copyWith( |       state = state.copyWith( | ||||||
|           status: UploadProfileStatus.success, |         status: UploadProfileStatus.success, | ||||||
|           profileImagePath: res.profileImagePath); |         profileImagePath: res.profileImagePath, | ||||||
|  |       ); | ||||||
|       return true; |       return true; | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -100,4 +103,5 @@ class UploadProfileImageNotifier | |||||||
|  |  | ||||||
| final uploadProfileImageProvider = | final uploadProfileImageProvider = | ||||||
|     StateNotifierProvider<UploadProfileImageNotifier, UploadProfileImageState>( |     StateNotifierProvider<UploadProfileImageNotifier, UploadProfileImageState>( | ||||||
|         ((ref) => UploadProfileImageNotifier(ref.watch(userServiceProvider)))); |   ((ref) => UploadProfileImageNotifier(ref.watch(userServiceProvider))), | ||||||
|  | ); | ||||||
|   | |||||||
| @@ -1,120 +1,51 @@ | |||||||
| import 'dart:convert'; | import 'dart:async'; | ||||||
|  |  | ||||||
| import 'package:flutter/material.dart'; | import 'package:flutter/material.dart'; | ||||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||||
| import 'package:immich_mobile/modules/home/models/delete_asset_response.model.dart'; | import 'package:immich_mobile/shared/services/api.service.dart'; | ||||||
| import 'package:immich_mobile/modules/home/models/get_all_asset_response.model.dart'; | import 'package:openapi/api.dart'; | ||||||
| import 'package:immich_mobile/shared/models/immich_asset.model.dart'; |  | ||||||
| import 'package:immich_mobile/shared/models/immich_asset_with_exif.model.dart'; |  | ||||||
| import 'package:immich_mobile/shared/services/network.service.dart'; |  | ||||||
|  |  | ||||||
| final assetServiceProvider = | final assetServiceProvider = Provider( | ||||||
|     Provider((ref) => AssetService(ref.watch(networkServiceProvider))); |   (ref) => AssetService( | ||||||
|  |     ref.watch(apiServiceProvider), | ||||||
|  |   ), | ||||||
|  | ); | ||||||
|  |  | ||||||
| class AssetService { | class AssetService { | ||||||
|   final NetworkService _networkService; |   final ApiService _apiService; | ||||||
|   AssetService(this._networkService); |  | ||||||
|  |  | ||||||
|   Future<List<ImmichAsset>?> getAllAsset() async { |   AssetService(this._apiService); | ||||||
|     var res = await _networkService.getRequest(url: "asset/"); |  | ||||||
|  |   Future<List<AssetResponseDto>?> getAllAsset() async { | ||||||
|     try { |     try { | ||||||
|       List<dynamic> decodedData = jsonDecode(res.toString()); |       return await _apiService.assetApi.getAllAssets(); | ||||||
|  |  | ||||||
|       List<ImmichAsset> result = |  | ||||||
|           List.from(decodedData.map((a) => ImmichAsset.fromMap(a))); |  | ||||||
|       return result; |  | ||||||
|     } catch (e) { |     } catch (e) { | ||||||
|       debugPrint("Error getAllAsset  ${e.toString()}"); |       debugPrint("Error [getAllAsset]  ${e.toString()}"); | ||||||
|     } |  | ||||||
|     return null; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   Future<GetAllAssetResponse?> getAllAssetWithPagination() async { |  | ||||||
|     var res = await _networkService.getRequest(url: "asset/all"); |  | ||||||
|     try { |  | ||||||
|       Map<String, dynamic> decodedData = jsonDecode(res.toString()); |  | ||||||
|  |  | ||||||
|       GetAllAssetResponse result = GetAllAssetResponse.fromMap(decodedData); |  | ||||||
|       return result; |  | ||||||
|     } catch (e) { |  | ||||||
|       debugPrint("Error getAllAsset  ${e.toString()}"); |  | ||||||
|     } |  | ||||||
|     return null; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   Future<GetAllAssetResponse?> getOlderAsset(String? nextPageKey) async { |  | ||||||
|     try { |  | ||||||
|       var res = await _networkService.getRequest( |  | ||||||
|         url: "asset/all?nextPageKey=$nextPageKey", |  | ||||||
|       ); |  | ||||||
|  |  | ||||||
|       Map<String, dynamic> decodedData = jsonDecode(res.toString()); |  | ||||||
|  |  | ||||||
|       GetAllAssetResponse result = GetAllAssetResponse.fromMap(decodedData); |  | ||||||
|       if (result.count != 0) { |  | ||||||
|         return result; |  | ||||||
|       } |  | ||||||
|     } catch (e) { |  | ||||||
|       debugPrint("Error getAllAsset  ${e.toString()}"); |  | ||||||
|     } |  | ||||||
|     return null; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   Future<List<ImmichAsset>> getNewAsset(String latestDate) async { |  | ||||||
|     try { |  | ||||||
|       var res = await _networkService.getRequest( |  | ||||||
|         url: "asset/new?latestDate=$latestDate", |  | ||||||
|       ); |  | ||||||
|  |  | ||||||
|       List<dynamic> decodedData = jsonDecode(res.toString()); |  | ||||||
|  |  | ||||||
|       List<ImmichAsset> result = |  | ||||||
|           List.from(decodedData.map((a) => ImmichAsset.fromMap(a))); |  | ||||||
|       if (result.isNotEmpty) { |  | ||||||
|         return result; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       return []; |  | ||||||
|     } catch (e) { |  | ||||||
|       debugPrint("Error getAllAsset  ${e.toString()}"); |  | ||||||
|       return []; |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   Future<ImmichAssetWithExif?> getAssetById(String assetId) async { |  | ||||||
|     try { |  | ||||||
|       var res = await _networkService.getRequest( |  | ||||||
|         url: "asset/assetById/$assetId", |  | ||||||
|       ); |  | ||||||
|  |  | ||||||
|       Map<String, dynamic> decodedData = jsonDecode(res.toString()); |  | ||||||
|  |  | ||||||
|       ImmichAssetWithExif result = ImmichAssetWithExif.fromMap(decodedData); |  | ||||||
|       return result; |  | ||||||
|     } catch (e) { |  | ||||||
|       debugPrint("Error getAllAsset  ${e.toString()}"); |  | ||||||
|       return null; |       return null; | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   Future<List<DeleteAssetResponse>?> deleteAssets( |   Future<AssetResponseDto?> getAssetById(String assetId) async { | ||||||
|       Set<ImmichAsset> deleteAssets) async { |  | ||||||
|     try { |     try { | ||||||
|       var payload = []; |       return await _apiService.assetApi.getAssetById(assetId); | ||||||
|  |     } catch (e) { | ||||||
|  |       debugPrint("Error [getAssetById]  ${e.toString()}"); | ||||||
|  |       return null; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   Future<List<DeleteAssetResponseDto>?> deleteAssets( | ||||||
|  |     Set<AssetResponseDto> deleteAssets, | ||||||
|  |   ) async { | ||||||
|  |     try { | ||||||
|  |       List<String> payload = []; | ||||||
|  |  | ||||||
|       for (var asset in deleteAssets) { |       for (var asset in deleteAssets) { | ||||||
|         payload.add(asset.id); |         payload.add(asset.id); | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       var res = await _networkService |       return await _apiService.assetApi | ||||||
|           .deleteRequest(url: "asset/", data: {"ids": payload}); |           .deleteAsset(DeleteAssetDto(ids: payload)); | ||||||
|  |  | ||||||
|       List<dynamic> decodedData = jsonDecode(res.toString()); |  | ||||||
|  |  | ||||||
|       List<DeleteAssetResponse> result = |  | ||||||
|           List.from(decodedData.map((a) => DeleteAssetResponse.fromMap(a))); |  | ||||||
|  |  | ||||||
|       return result; |  | ||||||
|     } catch (e) { |     } catch (e) { | ||||||
|       debugPrint("Error getAllAsset  ${e.toString()}"); |       debugPrint("Error getAllAsset  ${e.toString()}"); | ||||||
|       return null; |       return null; | ||||||
|   | |||||||
| @@ -15,7 +15,9 @@ class ControlBottomAppBar extends StatelessWidget { | |||||||
|         height: MediaQuery.of(context).size.height * 0.15, |         height: MediaQuery.of(context).size.height * 0.15, | ||||||
|         decoration: BoxDecoration( |         decoration: BoxDecoration( | ||||||
|           borderRadius: const BorderRadius.only( |           borderRadius: const BorderRadius.only( | ||||||
|               topLeft: Radius.circular(15), topRight: Radius.circular(15)), |             topLeft: Radius.circular(15), | ||||||
|  |             topRight: Radius.circular(15), | ||||||
|  |           ), | ||||||
|           color: Colors.grey[300]?.withOpacity(0.98), |           color: Colors.grey[300]?.withOpacity(0.98), | ||||||
|         ), |         ), | ||||||
|         child: Column( |         child: Column( | ||||||
| @@ -48,12 +50,12 @@ class ControlBottomAppBar extends StatelessWidget { | |||||||
| } | } | ||||||
|  |  | ||||||
| class ControlBoxButton extends StatelessWidget { | class ControlBoxButton extends StatelessWidget { | ||||||
|   const ControlBoxButton( |   const ControlBoxButton({ | ||||||
|       {Key? key, |     Key? key, | ||||||
|       required this.label, |     required this.label, | ||||||
|       required this.iconData, |     required this.iconData, | ||||||
|       required this.onPressed}) |     required this.onPressed, | ||||||
|       : super(key: key); |   }) : super(key: key); | ||||||
|  |  | ||||||
|   final String label; |   final String label; | ||||||
|   final IconData iconData; |   final IconData iconData; | ||||||
|   | |||||||
| @@ -2,8 +2,7 @@ import 'package:easy_localization/easy_localization.dart'; | |||||||
| import 'package:flutter/material.dart'; | import 'package:flutter/material.dart'; | ||||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||||
| import 'package:immich_mobile/modules/home/providers/home_page_state.provider.dart'; | import 'package:immich_mobile/modules/home/providers/home_page_state.provider.dart'; | ||||||
| import 'package:immich_mobile/shared/models/immich_asset.model.dart'; | import 'package:openapi/api.dart'; | ||||||
| import 'package:intl/intl.dart'; |  | ||||||
|  |  | ||||||
| class DailyTitleText extends ConsumerWidget { | class DailyTitleText extends ConsumerWidget { | ||||||
|   const DailyTitleText({ |   const DailyTitleText({ | ||||||
| @@ -13,14 +12,15 @@ class DailyTitleText extends ConsumerWidget { | |||||||
|   }) : super(key: key); |   }) : super(key: key); | ||||||
|  |  | ||||||
|   final String isoDate; |   final String isoDate; | ||||||
|   final List<ImmichAsset> assetGroup; |   final List<AssetResponseDto> assetGroup; | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   Widget build(BuildContext context, WidgetRef ref) { |   Widget build(BuildContext context, WidgetRef ref) { | ||||||
|     var currentYear = DateTime.now().year; |     var currentYear = DateTime.now().year; | ||||||
|     var groupYear = DateTime.parse(isoDate).year; |     var groupYear = DateTime.parse(isoDate).year; | ||||||
|     var formatDateTemplate = |     var formatDateTemplate = currentYear == groupYear | ||||||
|         currentYear == groupYear ? "daily_title_text_date".tr() : "daily_title_text_date_year".tr(); |         ? "daily_title_text_date".tr() | ||||||
|  |         : "daily_title_text_date_year".tr(); | ||||||
|     var dateText = |     var dateText = | ||||||
|         DateFormat(formatDateTemplate).format(DateTime.parse(isoDate)); |         DateFormat(formatDateTemplate).format(DateTime.parse(isoDate)); | ||||||
|     var isMultiSelectEnable = |     var isMultiSelectEnable = | ||||||
| @@ -74,7 +74,11 @@ class DailyTitleText extends ConsumerWidget { | |||||||
|     return SliverToBoxAdapter( |     return SliverToBoxAdapter( | ||||||
|       child: Padding( |       child: Padding( | ||||||
|         padding: const EdgeInsets.only( |         padding: const EdgeInsets.only( | ||||||
|             top: 29.0, bottom: 29.0, left: 12.0, right: 12.0), |           top: 29.0, | ||||||
|  |           bottom: 29.0, | ||||||
|  |           left: 12.0, | ||||||
|  |           right: 12.0, | ||||||
|  |         ), | ||||||
|         child: Row( |         child: Row( | ||||||
|           children: [ |           children: [ | ||||||
|             Text( |             Text( | ||||||
|   | |||||||
| @@ -29,15 +29,18 @@ class DisableMultiSelectButton extends ConsumerWidget { | |||||||
|             child: Padding( |             child: Padding( | ||||||
|               padding: const EdgeInsets.symmetric(horizontal: 4.0), |               padding: const EdgeInsets.symmetric(horizontal: 4.0), | ||||||
|               child: TextButton.icon( |               child: TextButton.icon( | ||||||
|                   onPressed: () { |                 onPressed: () { | ||||||
|                     onPressed(); |                   onPressed(); | ||||||
|                   }, |                 }, | ||||||
|                   icon: const Icon(Icons.close_rounded), |                 icon: const Icon(Icons.close_rounded), | ||||||
|                   label: Text( |                 label: Text( | ||||||
|                     '$selectedItemCount', |                   '$selectedItemCount', | ||||||
|                     style: const TextStyle( |                   style: const TextStyle( | ||||||
|                         fontWeight: FontWeight.w600, fontSize: 18), |                     fontWeight: FontWeight.w600, | ||||||
|                   )), |                     fontSize: 18, | ||||||
|  |                   ), | ||||||
|  |                 ), | ||||||
|  |               ), | ||||||
|             ), |             ), | ||||||
|           ), |           ), | ||||||
|         ), |         ), | ||||||
|   | |||||||
| @@ -118,20 +118,24 @@ class DraggableScrollbar extends StatefulWidget { | |||||||
|     this.labelConstraints, |     this.labelConstraints, | ||||||
|   })  : assert(child.scrollDirection == Axis.vertical), |   })  : assert(child.scrollDirection == Axis.vertical), | ||||||
|         scrollThumbBuilder = _thumbSemicircleBuilder( |         scrollThumbBuilder = _thumbSemicircleBuilder( | ||||||
|             heightScrollThumb * 0.6, scrollThumbKey, alwaysVisibleScrollThumb), |           heightScrollThumb * 0.6, | ||||||
|  |           scrollThumbKey, | ||||||
|  |           alwaysVisibleScrollThumb, | ||||||
|  |         ), | ||||||
|         super(key: key); |         super(key: key); | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   DraggableScrollbarState createState() => DraggableScrollbarState(); |   DraggableScrollbarState createState() => DraggableScrollbarState(); | ||||||
|  |  | ||||||
|   static buildScrollThumbAndLabel( |   static buildScrollThumbAndLabel({ | ||||||
|       {required Widget scrollThumb, |     required Widget scrollThumb, | ||||||
|       required Color backgroundColor, |     required Color backgroundColor, | ||||||
|       required Animation<double>? thumbAnimation, |     required Animation<double>? thumbAnimation, | ||||||
|       required Animation<double>? labelAnimation, |     required Animation<double>? labelAnimation, | ||||||
|       required Text? labelText, |     required Text? labelText, | ||||||
|       required BoxConstraints? labelConstraints, |     required BoxConstraints? labelConstraints, | ||||||
|       required bool alwaysVisibleScrollThumb}) { |     required bool alwaysVisibleScrollThumb, | ||||||
|  |   }) { | ||||||
|     var scrollThumbAndLabel = labelText == null |     var scrollThumbAndLabel = labelText == null | ||||||
|         ? scrollThumb |         ? scrollThumb | ||||||
|         : Row( |         : Row( | ||||||
| @@ -158,7 +162,10 @@ class DraggableScrollbar extends StatefulWidget { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   static ScrollThumbBuilder _thumbSemicircleBuilder( |   static ScrollThumbBuilder _thumbSemicircleBuilder( | ||||||
|       double width, Key? scrollThumbKey, bool alwaysVisibleScrollThumb) { |     double width, | ||||||
|  |     Key? scrollThumbKey, | ||||||
|  |     bool alwaysVisibleScrollThumb, | ||||||
|  |   ) { | ||||||
|     return ( |     return ( | ||||||
|       Color backgroundColor, |       Color backgroundColor, | ||||||
|       Animation<double> thumbAnimation, |       Animation<double> thumbAnimation, | ||||||
| @@ -198,7 +205,9 @@ class DraggableScrollbar extends StatefulWidget { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   static ScrollThumbBuilder _thumbArrowBuilder( |   static ScrollThumbBuilder _thumbArrowBuilder( | ||||||
|       Key? scrollThumbKey, bool alwaysVisibleScrollThumb) { |     Key? scrollThumbKey, | ||||||
|  |     bool alwaysVisibleScrollThumb, | ||||||
|  |   ) { | ||||||
|     return ( |     return ( | ||||||
|       Color backgroundColor, |       Color backgroundColor, | ||||||
|       Animation<double> thumbAnimation, |       Animation<double> thumbAnimation, | ||||||
| @@ -234,7 +243,9 @@ class DraggableScrollbar extends StatefulWidget { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   static ScrollThumbBuilder _thumbRRectBuilder( |   static ScrollThumbBuilder _thumbRRectBuilder( | ||||||
|       Key? scrollThumbKey, bool alwaysVisibleScrollThumb) { |     Key? scrollThumbKey, | ||||||
|  |     bool alwaysVisibleScrollThumb, | ||||||
|  |   ) { | ||||||
|     return ( |     return ( | ||||||
|       Color backgroundColor, |       Color backgroundColor, | ||||||
|       Animation<double> thumbAnimation, |       Animation<double> thumbAnimation, | ||||||
| @@ -372,42 +383,44 @@ class DraggableScrollbarState extends State<DraggableScrollbar> | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     return LayoutBuilder( |     return LayoutBuilder( | ||||||
|         builder: (BuildContext context, BoxConstraints constraints) { |       builder: (BuildContext context, BoxConstraints constraints) { | ||||||
|       //print("LayoutBuilder constraints=$constraints"); |         //print("LayoutBuilder constraints=$constraints"); | ||||||
|  |  | ||||||
|       return NotificationListener<ScrollNotification>( |         return NotificationListener<ScrollNotification>( | ||||||
|         onNotification: (ScrollNotification notification) { |           onNotification: (ScrollNotification notification) { | ||||||
|           changePosition(notification); |             changePosition(notification); | ||||||
|           return false; |             return false; | ||||||
|         }, |           }, | ||||||
|         child: Stack( |           child: Stack( | ||||||
|           children: <Widget>[ |             children: <Widget>[ | ||||||
|             RepaintBoundary( |               RepaintBoundary( | ||||||
|               child: widget.child, |                 child: widget.child, | ||||||
|             ), |               ), | ||||||
|             RepaintBoundary( |               RepaintBoundary( | ||||||
|                 child: GestureDetector( |                 child: GestureDetector( | ||||||
|               onVerticalDragStart: _onVerticalDragStart, |                   onVerticalDragStart: _onVerticalDragStart, | ||||||
|               onVerticalDragUpdate: _onVerticalDragUpdate, |                   onVerticalDragUpdate: _onVerticalDragUpdate, | ||||||
|               onVerticalDragEnd: _onVerticalDragEnd, |                   onVerticalDragEnd: _onVerticalDragEnd, | ||||||
|               child: Container( |                   child: Container( | ||||||
|                 alignment: Alignment.topRight, |                     alignment: Alignment.topRight, | ||||||
|                 margin: EdgeInsets.only(top: _barOffset), |                     margin: EdgeInsets.only(top: _barOffset), | ||||||
|                 padding: widget.padding, |                     padding: widget.padding, | ||||||
|                 child: widget.scrollThumbBuilder( |                     child: widget.scrollThumbBuilder( | ||||||
|                   widget.backgroundColor, |                       widget.backgroundColor, | ||||||
|                   _thumbAnimation, |                       _thumbAnimation, | ||||||
|                   _labelAnimation, |                       _labelAnimation, | ||||||
|                   widget.heightScrollThumb, |                       widget.heightScrollThumb, | ||||||
|                   labelText: labelText, |                       labelText: labelText, | ||||||
|                   labelConstraints: widget.labelConstraints, |                       labelConstraints: widget.labelConstraints, | ||||||
|  |                     ), | ||||||
|  |                   ), | ||||||
|                 ), |                 ), | ||||||
|               ), |               ), | ||||||
|             )), |             ], | ||||||
|           ], |           ), | ||||||
|         ), |         ); | ||||||
|       ); |       }, | ||||||
|     }); |     ); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   //scroll bar has received notification that it's view was scrolled |   //scroll bar has received notification that it's view was scrolled | ||||||
| @@ -498,7 +511,10 @@ class DraggableScrollbarState extends State<DraggableScrollbar> | |||||||
|         } |         } | ||||||
|  |  | ||||||
|         double viewDelta = getScrollViewDelta( |         double viewDelta = getScrollViewDelta( | ||||||
|             details.delta.dy, barMaxScrollExtent, viewMaxScrollExtent); |           details.delta.dy, | ||||||
|  |           barMaxScrollExtent, | ||||||
|  |           viewMaxScrollExtent, | ||||||
|  |         ); | ||||||
|  |  | ||||||
|         _viewOffset = widget.controller.position.pixels + viewDelta; |         _viewOffset = widget.controller.position.pixels + viewDelta; | ||||||
|         if (_viewOffset < widget.controller.position.minScrollExtent) { |         if (_viewOffset < widget.controller.position.minScrollExtent) { | ||||||
| @@ -579,7 +595,9 @@ class ArrowClipper extends CustomClipper<Path> { | |||||||
|     path.lineTo(startPointX + arrowWidth, startPointY); |     path.lineTo(startPointX + arrowWidth, startPointY); | ||||||
|     path.lineTo(startPointX + arrowWidth, startPointY + 1.0); |     path.lineTo(startPointX + arrowWidth, startPointY + 1.0); | ||||||
|     path.lineTo( |     path.lineTo( | ||||||
|         startPointX + arrowWidth / 2, startPointY - arrowWidth / 2 + 1.0); |       startPointX + arrowWidth / 2, | ||||||
|  |       startPointY - arrowWidth / 2 + 1.0, | ||||||
|  |     ); | ||||||
|     path.lineTo(startPointX, startPointY + 1.0); |     path.lineTo(startPointX, startPointY + 1.0); | ||||||
|     path.close(); |     path.close(); | ||||||
|  |  | ||||||
| @@ -589,7 +607,9 @@ class ArrowClipper extends CustomClipper<Path> { | |||||||
|     path.lineTo(startPointX, startPointY); |     path.lineTo(startPointX, startPointY); | ||||||
|     path.lineTo(startPointX, startPointY - 1.0); |     path.lineTo(startPointX, startPointY - 1.0); | ||||||
|     path.lineTo( |     path.lineTo( | ||||||
|         startPointX + arrowWidth / 2, startPointY + arrowWidth / 2 - 1.0); |       startPointX + arrowWidth / 2, | ||||||
|  |       startPointY + arrowWidth / 2 - 1.0, | ||||||
|  |     ); | ||||||
|     path.lineTo(startPointX + arrowWidth, startPointY - 1.0); |     path.lineTo(startPointX + arrowWidth, startPointY - 1.0); | ||||||
|     path.close(); |     path.close(); | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,10 +1,10 @@ | |||||||
| import 'package:flutter/material.dart'; | import 'package:flutter/material.dart'; | ||||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||||
| import 'package:immich_mobile/modules/home/ui/thumbnail_image.dart'; | import 'package:immich_mobile/modules/home/ui/thumbnail_image.dart'; | ||||||
| import 'package:immich_mobile/shared/models/immich_asset.model.dart'; | import 'package:openapi/api.dart'; | ||||||
|  |  | ||||||
| class ImageGrid extends ConsumerWidget { | class ImageGrid extends ConsumerWidget { | ||||||
|   final List<ImmichAsset> assetGroup; |   final List<AssetResponseDto> assetGroup; | ||||||
|  |  | ||||||
|   const ImageGrid({Key? key, required this.assetGroup}) : super(key: key); |   const ImageGrid({Key? key, required this.assetGroup}) : super(key: key); | ||||||
|  |  | ||||||
| @@ -25,7 +25,7 @@ class ImageGrid extends ConsumerWidget { | |||||||
|             child: Stack( |             child: Stack( | ||||||
|               children: [ |               children: [ | ||||||
|                 ThumbnailImage(asset: assetGroup[index]), |                 ThumbnailImage(asset: assetGroup[index]), | ||||||
|                 if (assetType != 'IMAGE') |                 if (assetType != AssetTypeEnum.IMAGE) | ||||||
|                   Positioned( |                   Positioned( | ||||||
|                     top: 5, |                     top: 5, | ||||||
|                     right: 5, |                     right: 5, | ||||||
|   | |||||||
| @@ -31,7 +31,8 @@ class ImmichSliverAppBar extends ConsumerWidget { | |||||||
|       pinned: false, |       pinned: false, | ||||||
|       snap: false, |       snap: false, | ||||||
|       shape: const RoundedRectangleBorder( |       shape: const RoundedRectangleBorder( | ||||||
|           borderRadius: BorderRadius.all(Radius.circular(5))), |         borderRadius: BorderRadius.all(Radius.circular(5)), | ||||||
|  |       ), | ||||||
|       leading: Builder( |       leading: Builder( | ||||||
|         builder: (BuildContext context) { |         builder: (BuildContext context) { | ||||||
|           return Stack( |           return Stack( | ||||||
| @@ -99,7 +100,8 @@ class ImmichSliverAppBar extends ConsumerWidget { | |||||||
|                   child: CircularProgressIndicator( |                   child: CircularProgressIndicator( | ||||||
|                     strokeWidth: 1, |                     strokeWidth: 1, | ||||||
|                     valueColor: AlwaysStoppedAnimation<Color>( |                     valueColor: AlwaysStoppedAnimation<Color>( | ||||||
|                         Theme.of(context).primaryColor), |                       Theme.of(context).primaryColor, | ||||||
|  |                     ), | ||||||
|                   ), |                   ), | ||||||
|                 ), |                 ), | ||||||
|               ), |               ), | ||||||
| @@ -117,7 +119,8 @@ class ImmichSliverAppBar extends ConsumerWidget { | |||||||
|                         Icons.cloud_off_rounded, |                         Icons.cloud_off_rounded, | ||||||
|                         size: 8, |                         size: 8, | ||||||
|                       ), |                       ), | ||||||
|                       child: const Icon(Icons.backup_rounded)), |                       child: const Icon(Icons.backup_rounded), | ||||||
|  |                     ), | ||||||
|               onPressed: () async { |               onPressed: () async { | ||||||
|                 var onPop = await AutoRouter.of(context) |                 var onPop = await AutoRouter.of(context) | ||||||
|                     .push(const BackupControllerRoute()); |                     .push(const BackupControllerRoute()); | ||||||
|   | |||||||
| @@ -1,6 +1,5 @@ | |||||||
| import 'package:easy_localization/easy_localization.dart'; | import 'package:easy_localization/easy_localization.dart'; | ||||||
| import 'package:flutter/material.dart'; | import 'package:flutter/material.dart'; | ||||||
| import 'package:intl/intl.dart'; |  | ||||||
|  |  | ||||||
| class MonthlyTitleText extends StatelessWidget { | class MonthlyTitleText extends StatelessWidget { | ||||||
|   const MonthlyTitleText({ |   const MonthlyTitleText({ | ||||||
| @@ -12,7 +11,8 @@ class MonthlyTitleText extends StatelessWidget { | |||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   Widget build(BuildContext context) { |   Widget build(BuildContext context) { | ||||||
|     var monthTitleText = DateFormat("monthly_title_text_date_format".tr()).format(DateTime.parse(isoDate)); |     var monthTitleText = DateFormat("monthly_title_text_date_format".tr()) | ||||||
|  |         .format(DateTime.parse(isoDate)); | ||||||
|  |  | ||||||
|     return SliverToBoxAdapter( |     return SliverToBoxAdapter( | ||||||
|       child: Padding( |       child: Padding( | ||||||
|   | |||||||
| @@ -55,7 +55,8 @@ class ProfileDrawer extends HookConsumerWidget { | |||||||
|           return CircleAvatar( |           return CircleAvatar( | ||||||
|             radius: 35, |             radius: 35, | ||||||
|             backgroundImage: NetworkImage( |             backgroundImage: NetworkImage( | ||||||
|                 '$endpoint/user/profile-image/${authState.userId}?d=${dummmy++}'), |               '$endpoint/user/profile-image/${authState.userId}?d=${dummmy++}', | ||||||
|  |             ), | ||||||
|             backgroundColor: Colors.transparent, |             backgroundColor: Colors.transparent, | ||||||
|           ); |           ); | ||||||
|         } else { |         } else { | ||||||
| @@ -71,7 +72,8 @@ class ProfileDrawer extends HookConsumerWidget { | |||||||
|         return CircleAvatar( |         return CircleAvatar( | ||||||
|           radius: 35, |           radius: 35, | ||||||
|           backgroundImage: NetworkImage( |           backgroundImage: NetworkImage( | ||||||
|               '$endpoint/user/profile-image/${authState.userId}?d=${dummmy++}'), |             '$endpoint/user/profile-image/${authState.userId}?d=${dummmy++}', | ||||||
|  |           ), | ||||||
|           backgroundColor: Colors.transparent, |           backgroundColor: Colors.transparent, | ||||||
|         ); |         ); | ||||||
|       } |       } | ||||||
| @@ -93,7 +95,10 @@ class ProfileDrawer extends HookConsumerWidget { | |||||||
|  |  | ||||||
|     _pickUserProfileImage() async { |     _pickUserProfileImage() async { | ||||||
|       final XFile? image = await ImagePicker().pickImage( |       final XFile? image = await ImagePicker().pickImage( | ||||||
|           source: ImageSource.gallery, maxHeight: 1024, maxWidth: 1024); |         source: ImageSource.gallery, | ||||||
|  |         maxHeight: 1024, | ||||||
|  |         maxWidth: 1024, | ||||||
|  |       ); | ||||||
|  |  | ||||||
|       if (image != null) { |       if (image != null) { | ||||||
|         var success = |         var success = | ||||||
| @@ -101,16 +106,20 @@ class ProfileDrawer extends HookConsumerWidget { | |||||||
|  |  | ||||||
|         if (success) { |         if (success) { | ||||||
|           ref.watch(authenticationProvider.notifier).updateUserProfileImagePath( |           ref.watch(authenticationProvider.notifier).updateUserProfileImagePath( | ||||||
|               ref.read(uploadProfileImageProvider).profileImagePath); |                 ref.read(uploadProfileImageProvider).profileImagePath, | ||||||
|  |               ); | ||||||
|         } |         } | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     useEffect(() { |     useEffect( | ||||||
|       _getPackageInfo(); |       () { | ||||||
|       _buildUserProfileImage(); |         _getPackageInfo(); | ||||||
|       return null; |         _buildUserProfileImage(); | ||||||
|     }, []); |         return null; | ||||||
|  |       }, | ||||||
|  |       [], | ||||||
|  |     ); | ||||||
|     return Drawer( |     return Drawer( | ||||||
|       child: Column( |       child: Column( | ||||||
|         mainAxisAlignment: MainAxisAlignment.spaceBetween, |         mainAxisAlignment: MainAxisAlignment.spaceBetween, | ||||||
| @@ -186,9 +195,10 @@ class ProfileDrawer extends HookConsumerWidget { | |||||||
|                 title: const Text( |                 title: const Text( | ||||||
|                   "profile_drawer_sign_out", |                   "profile_drawer_sign_out", | ||||||
|                   style: TextStyle( |                   style: TextStyle( | ||||||
|                       color: Colors.black54, |                     color: Colors.black54, | ||||||
|                       fontSize: 14, |                     fontSize: 14, | ||||||
|                       fontWeight: FontWeight.bold), |                     fontWeight: FontWeight.bold, | ||||||
|  |                   ), | ||||||
|                 ).tr(), |                 ).tr(), | ||||||
|                 onTap: () async { |                 onTap: () async { | ||||||
|                   bool res = |                   bool res = | ||||||
| @@ -231,9 +241,10 @@ class ProfileDrawer extends HookConsumerWidget { | |||||||
|                             : "profile_drawer_client_server_up_to_date".tr(), |                             : "profile_drawer_client_server_up_to_date".tr(), | ||||||
|                         textAlign: TextAlign.center, |                         textAlign: TextAlign.center, | ||||||
|                         style: TextStyle( |                         style: TextStyle( | ||||||
|                             fontSize: 11, |                           fontSize: 11, | ||||||
|                             color: Theme.of(context).primaryColor, |                           color: Theme.of(context).primaryColor, | ||||||
|                             fontWeight: FontWeight.w600), |                           fontWeight: FontWeight.w600, | ||||||
|  |                         ), | ||||||
|                       ), |                       ), | ||||||
|                     ), |                     ), | ||||||
|                     const Divider(), |                     const Divider(), | ||||||
| @@ -271,7 +282,7 @@ class ProfileDrawer extends HookConsumerWidget { | |||||||
|                           ), |                           ), | ||||||
|                         ), |                         ), | ||||||
|                         Text( |                         Text( | ||||||
|                           "${serverInfoState.serverVersion.major}.${serverInfoState.serverVersion.minor}.${serverInfoState.serverVersion.patch}", |                           "${serverInfoState.serverVersion.major}.${serverInfoState.serverVersion.minor}.${serverInfoState.serverVersion.patch_}", | ||||||
|                           style: TextStyle( |                           style: TextStyle( | ||||||
|                             fontSize: 11, |                             fontSize: 11, | ||||||
|                             color: Colors.grey[500], |                             color: Colors.grey[500], | ||||||
|   | |||||||
| @@ -8,11 +8,11 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; | |||||||
| import 'package:immich_mobile/constants/hive_box.dart'; | import 'package:immich_mobile/constants/hive_box.dart'; | ||||||
| import 'package:immich_mobile/modules/home/providers/home_page_state.provider.dart'; | import 'package:immich_mobile/modules/home/providers/home_page_state.provider.dart'; | ||||||
| import 'package:immich_mobile/modules/login/providers/authentication.provider.dart'; | import 'package:immich_mobile/modules/login/providers/authentication.provider.dart'; | ||||||
| import 'package:immich_mobile/shared/models/immich_asset.model.dart'; |  | ||||||
| import 'package:immich_mobile/routing/router.dart'; | import 'package:immich_mobile/routing/router.dart'; | ||||||
|  | import 'package:openapi/api.dart'; | ||||||
|  |  | ||||||
| class ThumbnailImage extends HookConsumerWidget { | class ThumbnailImage extends HookConsumerWidget { | ||||||
|   final ImmichAsset asset; |   final AssetResponseDto asset; | ||||||
|  |  | ||||||
|   const ThumbnailImage({Key? key, required this.asset}) : super(key: key); |   const ThumbnailImage({Key? key, required this.asset}) : super(key: key); | ||||||
|  |  | ||||||
| @@ -22,14 +22,13 @@ class ThumbnailImage extends HookConsumerWidget { | |||||||
|  |  | ||||||
|     var box = Hive.box(userInfoBox); |     var box = Hive.box(userInfoBox); | ||||||
|     var thumbnailRequestUrl = |     var thumbnailRequestUrl = | ||||||
|         '${box.get(serverEndpointKey)}/asset/file?aid=${asset.deviceAssetId}&did=${asset.deviceId}&isThumb=true'; |         '${box.get(serverEndpointKey)}/asset/thumbnail/${asset.id}'; | ||||||
|  |  | ||||||
|     var selectedAsset = ref.watch(homePageStateProvider).selectedItems; |     var selectedAsset = ref.watch(homePageStateProvider).selectedItems; | ||||||
|     var isMultiSelectEnable = |     var isMultiSelectEnable = | ||||||
|         ref.watch(homePageStateProvider).isMultiSelectEnable; |         ref.watch(homePageStateProvider).isMultiSelectEnable; | ||||||
|     var deviceId = ref.watch(authenticationProvider).deviceId; |     var deviceId = ref.watch(authenticationProvider).deviceId; | ||||||
|  |  | ||||||
|     Widget _buildSelectionIcon(ImmichAsset asset) { |     Widget _buildSelectionIcon(AssetResponseDto asset) { | ||||||
|       if (selectedAsset.contains(asset)) { |       if (selectedAsset.contains(asset)) { | ||||||
|         return Icon( |         return Icon( | ||||||
|           Icons.check_circle, |           Icons.check_circle, | ||||||
| @@ -61,7 +60,7 @@ class ThumbnailImage extends HookConsumerWidget { | |||||||
|               .watch(homePageStateProvider.notifier) |               .watch(homePageStateProvider.notifier) | ||||||
|               .addSingleSelectedItem(asset); |               .addSingleSelectedItem(asset); | ||||||
|         } else { |         } else { | ||||||
|           if (asset.type == 'IMAGE') { |           if (asset.type == AssetTypeEnum.IMAGE) { | ||||||
|             AutoRouter.of(context).push( |             AutoRouter.of(context).push( | ||||||
|               ImageViewerRoute( |               ImageViewerRoute( | ||||||
|                 imageUrl: |                 imageUrl: | ||||||
| @@ -74,9 +73,10 @@ class ThumbnailImage extends HookConsumerWidget { | |||||||
|           } else { |           } else { | ||||||
|             AutoRouter.of(context).push( |             AutoRouter.of(context).push( | ||||||
|               VideoViewerRoute( |               VideoViewerRoute( | ||||||
|                   videoUrl: |                 videoUrl: | ||||||
|                       '${box.get(serverEndpointKey)}/asset/file?aid=${asset.deviceAssetId}&did=${asset.deviceId}', |                     '${box.get(serverEndpointKey)}/asset/file?aid=${asset.deviceAssetId}&did=${asset.deviceId}', | ||||||
|                   asset: asset), |                 asset: asset, | ||||||
|  |               ), | ||||||
|             ); |             ); | ||||||
|           } |           } | ||||||
|         } |         } | ||||||
| @@ -94,14 +94,16 @@ class ThumbnailImage extends HookConsumerWidget { | |||||||
|               decoration: BoxDecoration( |               decoration: BoxDecoration( | ||||||
|                 border: isMultiSelectEnable && selectedAsset.contains(asset) |                 border: isMultiSelectEnable && selectedAsset.contains(asset) | ||||||
|                     ? Border.all( |                     ? Border.all( | ||||||
|                         color: Theme.of(context).primaryColorLight, width: 10) |                         color: Theme.of(context).primaryColorLight, | ||||||
|  |                         width: 10, | ||||||
|  |                       ) | ||||||
|                     : const Border(), |                     : const Border(), | ||||||
|               ), |               ), | ||||||
|               child: CachedNetworkImage( |               child: CachedNetworkImage( | ||||||
|                 cacheKey: "${asset.id}-${cacheKey.value}", |                 cacheKey: "${asset.id}-${cacheKey.value}", | ||||||
|                 width: 300, |                 width: 300, | ||||||
|                 height: 300, |                 height: 300, | ||||||
|                 memCacheHeight: asset.type == 'IMAGE' ? 250 : 400, |                 memCacheHeight: asset.type == AssetTypeEnum.IMAGE ? 250 : 400, | ||||||
|                 fit: BoxFit.cover, |                 fit: BoxFit.cover, | ||||||
|                 imageUrl: thumbnailRequestUrl, |                 imageUrl: thumbnailRequestUrl, | ||||||
|                 httpHeaders: { |                 httpHeaders: { | ||||||
| @@ -112,9 +114,11 @@ class ThumbnailImage extends HookConsumerWidget { | |||||||
|                     Transform.scale( |                     Transform.scale( | ||||||
|                   scale: 0.2, |                   scale: 0.2, | ||||||
|                   child: CircularProgressIndicator( |                   child: CircularProgressIndicator( | ||||||
|                       value: downloadProgress.progress), |                     value: downloadProgress.progress, | ||||||
|  |                   ), | ||||||
|                 ), |                 ), | ||||||
|                 errorWidget: (context, url, error) { |                 errorWidget: (context, url, error) { | ||||||
|  |                   debugPrint("Error getting thumbnail $url = $error"); | ||||||
|                   return Icon( |                   return Icon( | ||||||
|                     Icons.image_not_supported_outlined, |                     Icons.image_not_supported_outlined, | ||||||
|                     color: Theme.of(context).primaryColor, |                     color: Theme.of(context).primaryColor, | ||||||
|   | |||||||
| @@ -26,12 +26,15 @@ class HomePage extends HookConsumerWidget { | |||||||
|         ref.watch(homePageStateProvider).isMultiSelectEnable; |         ref.watch(homePageStateProvider).isMultiSelectEnable; | ||||||
|     var homePageState = ref.watch(homePageStateProvider); |     var homePageState = ref.watch(homePageStateProvider); | ||||||
|  |  | ||||||
|     useEffect(() { |     useEffect( | ||||||
|       ref.read(websocketProvider.notifier).connect(); |       () { | ||||||
|       ref.read(assetProvider.notifier).getAllAsset(); |         ref.read(websocketProvider.notifier).connect(); | ||||||
|       ref.watch(serverInfoProvider.notifier).getServerVersion(); |         ref.read(assetProvider.notifier).getAllAsset(); | ||||||
|       return null; |         ref.watch(serverInfoProvider.notifier).getServerVersion(); | ||||||
|     }, []); |         return null; | ||||||
|  |       }, | ||||||
|  |       [], | ||||||
|  |     ); | ||||||
|  |  | ||||||
|     void reloadAllAsset() { |     void reloadAllAsset() { | ||||||
|       ref.read(assetProvider.notifier).getAllAsset(); |       ref.read(assetProvider.notifier).getAllAsset(); | ||||||
|   | |||||||
| @@ -1,10 +1,8 @@ | |||||||
| import 'dart:convert'; | import 'package:openapi/api.dart'; | ||||||
|  |  | ||||||
| import 'package:immich_mobile/shared/models/device_info.model.dart'; |  | ||||||
|  |  | ||||||
| class AuthenticationState { | class AuthenticationState { | ||||||
|   final String deviceId; |   final String deviceId; | ||||||
|   final String deviceType; |   final DeviceTypeEnum deviceType; | ||||||
|   final String userId; |   final String userId; | ||||||
|   final String userEmail; |   final String userEmail; | ||||||
|   final bool isAuthenticated; |   final bool isAuthenticated; | ||||||
| @@ -13,8 +11,7 @@ class AuthenticationState { | |||||||
|   final bool isAdmin; |   final bool isAdmin; | ||||||
|   final bool shouldChangePassword; |   final bool shouldChangePassword; | ||||||
|   final String profileImagePath; |   final String profileImagePath; | ||||||
|   final DeviceInfoRemote deviceInfo; |   final DeviceInfoResponseDto deviceInfo; | ||||||
|  |  | ||||||
|   AuthenticationState({ |   AuthenticationState({ | ||||||
|     required this.deviceId, |     required this.deviceId, | ||||||
|     required this.deviceType, |     required this.deviceType, | ||||||
| @@ -31,7 +28,7 @@ class AuthenticationState { | |||||||
|  |  | ||||||
|   AuthenticationState copyWith({ |   AuthenticationState copyWith({ | ||||||
|     String? deviceId, |     String? deviceId, | ||||||
|     String? deviceType, |     DeviceTypeEnum? deviceType, | ||||||
|     String? userId, |     String? userId, | ||||||
|     String? userEmail, |     String? userEmail, | ||||||
|     bool? isAuthenticated, |     bool? isAuthenticated, | ||||||
| @@ -40,7 +37,7 @@ class AuthenticationState { | |||||||
|     bool? isAdmin, |     bool? isAdmin, | ||||||
|     bool? shouldChangePassword, |     bool? shouldChangePassword, | ||||||
|     String? profileImagePath, |     String? profileImagePath, | ||||||
|     DeviceInfoRemote? deviceInfo, |     DeviceInfoResponseDto? deviceInfo, | ||||||
|   }) { |   }) { | ||||||
|     return AuthenticationState( |     return AuthenticationState( | ||||||
|       deviceId: deviceId ?? this.deviceId, |       deviceId: deviceId ?? this.deviceId, | ||||||
| @@ -57,45 +54,6 @@ class AuthenticationState { | |||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   Map<String, dynamic> toMap() { |  | ||||||
|     final result = <String, dynamic>{}; |  | ||||||
|  |  | ||||||
|     result.addAll({'deviceId': deviceId}); |  | ||||||
|     result.addAll({'deviceType': deviceType}); |  | ||||||
|     result.addAll({'userId': userId}); |  | ||||||
|     result.addAll({'userEmail': userEmail}); |  | ||||||
|     result.addAll({'isAuthenticated': isAuthenticated}); |  | ||||||
|     result.addAll({'firstName': firstName}); |  | ||||||
|     result.addAll({'lastName': lastName}); |  | ||||||
|     result.addAll({'isAdmin': isAdmin}); |  | ||||||
|     result.addAll({'shouldChangePassword': shouldChangePassword}); |  | ||||||
|     result.addAll({'profileImagePath': profileImagePath}); |  | ||||||
|     result.addAll({'deviceInfo': deviceInfo.toMap()}); |  | ||||||
|  |  | ||||||
|     return result; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   factory AuthenticationState.fromMap(Map<String, dynamic> map) { |  | ||||||
|     return AuthenticationState( |  | ||||||
|       deviceId: map['deviceId'] ?? '', |  | ||||||
|       deviceType: map['deviceType'] ?? '', |  | ||||||
|       userId: map['userId'] ?? '', |  | ||||||
|       userEmail: map['userEmail'] ?? '', |  | ||||||
|       isAuthenticated: map['isAuthenticated'] ?? false, |  | ||||||
|       firstName: map['firstName'] ?? '', |  | ||||||
|       lastName: map['lastName'] ?? '', |  | ||||||
|       isAdmin: map['isAdmin'] ?? false, |  | ||||||
|       shouldChangePassword: map['shouldChangePassword'] ?? false, |  | ||||||
|       profileImagePath: map['profileImagePath'] ?? '', |  | ||||||
|       deviceInfo: DeviceInfoRemote.fromMap(map['deviceInfo']), |  | ||||||
|     ); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   String toJson() => json.encode(toMap()); |  | ||||||
|  |  | ||||||
|   factory AuthenticationState.fromJson(String source) => |  | ||||||
|       AuthenticationState.fromMap(json.decode(source)); |  | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   String toString() { |   String toString() { | ||||||
|     return 'AuthenticationState(deviceId: $deviceId, deviceType: $deviceType, userId: $userId, userEmail: $userEmail, isAuthenticated: $isAuthenticated, firstName: $firstName, lastName: $lastName, isAdmin: $isAdmin, shouldChangePassword: $shouldChangePassword, profileImagePath: $profileImagePath, deviceInfo: $deviceInfo)'; |     return 'AuthenticationState(deviceId: $deviceId, deviceType: $deviceType, userId: $userId, userEmail: $userEmail, isAuthenticated: $isAuthenticated, firstName: $firstName, lastName: $lastName, isAdmin: $isAdmin, shouldChangePassword: $shouldChangePassword, profileImagePath: $profileImagePath, deviceInfo: $deviceInfo)'; | ||||||
|   | |||||||
| @@ -16,9 +16,10 @@ class HiveSavedLoginInfo { | |||||||
|   @HiveField(3) |   @HiveField(3) | ||||||
|   bool isSaveLogin; |   bool isSaveLogin; | ||||||
|  |  | ||||||
|   HiveSavedLoginInfo( |   HiveSavedLoginInfo({ | ||||||
|       {required this.email, |     required this.email, | ||||||
|       required this.password, |     required this.password, | ||||||
|       required this.serverUrl, |     required this.serverUrl, | ||||||
|       required this.isSaveLogin}); |     required this.isSaveLogin, | ||||||
|  |   }); | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,23 +1,23 @@ | |||||||
| import 'package:dio/dio.dart'; |  | ||||||
| import 'package:flutter/material.dart'; | import 'package:flutter/material.dart'; | ||||||
| import 'package:hive/hive.dart'; | import 'package:hive/hive.dart'; | ||||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||||
| import 'package:immich_mobile/constants/hive_box.dart'; | import 'package:immich_mobile/constants/hive_box.dart'; | ||||||
| import 'package:immich_mobile/modules/login/models/authentication_state.model.dart'; | import 'package:immich_mobile/modules/login/models/authentication_state.model.dart'; | ||||||
| import 'package:immich_mobile/modules/login/models/hive_saved_login_info.model.dart'; | import 'package:immich_mobile/modules/login/models/hive_saved_login_info.model.dart'; | ||||||
| import 'package:immich_mobile/modules/login/models/login_response.model.dart'; |  | ||||||
| import 'package:immich_mobile/modules/backup/services/backup.service.dart'; | import 'package:immich_mobile/modules/backup/services/backup.service.dart'; | ||||||
|  | import 'package:immich_mobile/shared/services/api.service.dart'; | ||||||
| import 'package:immich_mobile/shared/services/device_info.service.dart'; | import 'package:immich_mobile/shared/services/device_info.service.dart'; | ||||||
| import 'package:immich_mobile/shared/services/network.service.dart'; | import 'package:openapi/api.dart'; | ||||||
| import 'package:immich_mobile/shared/models/device_info.model.dart'; |  | ||||||
|  |  | ||||||
| class AuthenticationNotifier extends StateNotifier<AuthenticationState> { | class AuthenticationNotifier extends StateNotifier<AuthenticationState> { | ||||||
|   AuthenticationNotifier( |   AuthenticationNotifier( | ||||||
|       this._deviceInfoService, this._backupService, this._networkService) |     this._deviceInfoService, | ||||||
|       : super( |     this._backupService, | ||||||
|  |     this._apiService, | ||||||
|  |   ) : super( | ||||||
|           AuthenticationState( |           AuthenticationState( | ||||||
|             deviceId: "", |             deviceId: "", | ||||||
|             deviceType: "", |             deviceType: DeviceTypeEnum.ANDROID, | ||||||
|             userId: "", |             userId: "", | ||||||
|             userEmail: "", |             userEmail: "", | ||||||
|             firstName: '', |             firstName: '', | ||||||
| @@ -26,12 +26,11 @@ class AuthenticationNotifier extends StateNotifier<AuthenticationState> { | |||||||
|             isAdmin: false, |             isAdmin: false, | ||||||
|             shouldChangePassword: false, |             shouldChangePassword: false, | ||||||
|             isAuthenticated: false, |             isAuthenticated: false, | ||||||
|             deviceInfo: DeviceInfoRemote( |             deviceInfo: DeviceInfoResponseDto( | ||||||
|               id: 0, |               id: 0, | ||||||
|               userId: "", |               userId: "", | ||||||
|               deviceId: "", |               deviceId: "", | ||||||
|               deviceType: "", |               deviceType: DeviceTypeEnum.ANDROID, | ||||||
|               notificationToken: "", |  | ||||||
|               createdAt: "", |               createdAt: "", | ||||||
|               isAutoBackup: false, |               isAutoBackup: false, | ||||||
|             ), |             ), | ||||||
| @@ -40,10 +39,14 @@ class AuthenticationNotifier extends StateNotifier<AuthenticationState> { | |||||||
|  |  | ||||||
|   final DeviceInfoService _deviceInfoService; |   final DeviceInfoService _deviceInfoService; | ||||||
|   final BackupService _backupService; |   final BackupService _backupService; | ||||||
|   final NetworkService _networkService; |   final ApiService _apiService; | ||||||
|  |  | ||||||
|   Future<bool> login(String email, String password, String serverEndpoint, |   Future<bool> login( | ||||||
|       bool isSavedLoginInfo) async { |     String email, | ||||||
|  |     String password, | ||||||
|  |     String serverEndpoint, | ||||||
|  |     bool isSavedLoginInfo, | ||||||
|  |   ) async { | ||||||
|     // Store server endpoint to Hive and test endpoint |     // Store server endpoint to Hive and test endpoint | ||||||
|     if (serverEndpoint[serverEndpoint.length - 1] == "/") { |     if (serverEndpoint[serverEndpoint.length - 1] == "/") { | ||||||
|       var validUrl = serverEndpoint.substring(0, serverEndpoint.length - 1); |       var validUrl = serverEndpoint.substring(0, serverEndpoint.length - 1); | ||||||
| @@ -52,12 +55,12 @@ class AuthenticationNotifier extends StateNotifier<AuthenticationState> { | |||||||
|       Hive.box(userInfoBox).put(serverEndpointKey, serverEndpoint); |       Hive.box(userInfoBox).put(serverEndpointKey, serverEndpoint); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     // Check Server URL validity | ||||||
|     try { |     try { | ||||||
|       bool isServerEndpointVerified = await _networkService.pingServer(); |       _apiService.setEndpoint(Hive.box(userInfoBox).get(serverEndpointKey)); | ||||||
|       if (!isServerEndpointVerified) { |       await _apiService.serverInfoApi.pingServer(); | ||||||
|         return false; |  | ||||||
|       } |  | ||||||
|     } catch (e) { |     } catch (e) { | ||||||
|  |       debugPrint('Invalid Server Endpoint Url $e'); | ||||||
|       return false; |       return false; | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -72,56 +75,73 @@ class AuthenticationNotifier extends StateNotifier<AuthenticationState> { | |||||||
|  |  | ||||||
|     // Make sign-in request |     // Make sign-in request | ||||||
|     try { |     try { | ||||||
|       Response res = await _networkService.postRequest( |       var loginResponse = await _apiService.authenticationApi.login( | ||||||
|           url: 'auth/login', data: {'email': email, 'password': password}); |         LoginCredentialDto( | ||||||
|  |           email: email, | ||||||
|  |           password: password, | ||||||
|  |         ), | ||||||
|  |       ); | ||||||
|  |  | ||||||
|       var payload = LogInReponse.fromJson(res.toString()); |       if (loginResponse == null) { | ||||||
|  |         debugPrint('Login Response is null'); | ||||||
|  |         return false; | ||||||
|  |       } | ||||||
|  |  | ||||||
|       Hive.box(userInfoBox).put(accessTokenKey, payload.accessToken); |       Hive.box(userInfoBox).put(accessTokenKey, loginResponse.accessToken); | ||||||
|  |  | ||||||
|       state = state.copyWith( |       state = state.copyWith( | ||||||
|         isAuthenticated: true, |         isAuthenticated: true, | ||||||
|         userId: payload.userId, |         userId: loginResponse.userId, | ||||||
|         userEmail: payload.userEmail, |         userEmail: loginResponse.userEmail, | ||||||
|         firstName: payload.firstName, |         firstName: loginResponse.firstName, | ||||||
|         lastName: payload.lastName, |         lastName: loginResponse.lastName, | ||||||
|         profileImagePath: payload.profileImagePath, |         profileImagePath: loginResponse.profileImagePath, | ||||||
|         isAdmin: payload.isAdmin, |         isAdmin: loginResponse.isAdmin, | ||||||
|         shouldChangePassword: payload.shouldChangePassword, |         shouldChangePassword: loginResponse.shouldChangePassword, | ||||||
|       ); |       ); | ||||||
|  |  | ||||||
|  |       // Login Success - Set Access Token to API Client | ||||||
|  |       _apiService.setAccessToken(loginResponse.accessToken); | ||||||
|  |  | ||||||
|       if (isSavedLoginInfo) { |       if (isSavedLoginInfo) { | ||||||
|         // Save login info to local storage |         // Save login info to local storage | ||||||
|         Hive.box<HiveSavedLoginInfo>(hiveLoginInfoBox).put( |         Hive.box<HiveSavedLoginInfo>(hiveLoginInfoBox).put( | ||||||
|           savedLoginInfoKey, |           savedLoginInfoKey, | ||||||
|           HiveSavedLoginInfo( |           HiveSavedLoginInfo( | ||||||
|               email: email, |             email: email, | ||||||
|               password: password, |             password: password, | ||||||
|               isSaveLogin: true, |             isSaveLogin: true, | ||||||
|               serverUrl: Hive.box(userInfoBox).get(serverEndpointKey)), |             serverUrl: Hive.box(userInfoBox).get(serverEndpointKey), | ||||||
|  |           ), | ||||||
|         ); |         ); | ||||||
|       } else { |       } else { | ||||||
|         Hive.box<HiveSavedLoginInfo>(hiveLoginInfoBox) |         Hive.box<HiveSavedLoginInfo>(hiveLoginInfoBox) | ||||||
|             .delete(savedLoginInfoKey); |             .delete(savedLoginInfoKey); | ||||||
|       } |       } | ||||||
|     } catch (e) { |     } catch (e) { | ||||||
|  |       debugPrint("Error logging in $e"); | ||||||
|       return false; |       return false; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     // Register device info |     // Register device info | ||||||
|     try { |     try { | ||||||
|       Response res = await _networkService.postRequest( |       DeviceInfoResponseDto? deviceInfo = | ||||||
|         url: 'device-info', |           await _apiService.deviceInfoApi.createDeviceInfo( | ||||||
|         data: { |         CreateDeviceInfoDto( | ||||||
|           'deviceId': state.deviceId, |           deviceId: state.deviceId, | ||||||
|           'deviceType': state.deviceType, |           deviceType: state.deviceType, | ||||||
|         }, |         ), | ||||||
|       ); |       ); | ||||||
|  |  | ||||||
|       DeviceInfoRemote deviceInfo = DeviceInfoRemote.fromJson(res.toString()); |       if (deviceInfo == null) { | ||||||
|  |         debugPrint('Device Info Response is null'); | ||||||
|  |         return false; | ||||||
|  |       } | ||||||
|  |  | ||||||
|       state = state.copyWith(deviceInfo: deviceInfo); |       state = state.copyWith(deviceInfo: deviceInfo); | ||||||
|     } catch (e) { |     } catch (e) { | ||||||
|       debugPrint("ERROR Register Device Info: $e"); |       debugPrint("ERROR Register Device Info: $e"); | ||||||
|  |       return false; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     return true; |     return true; | ||||||
| @@ -129,27 +149,7 @@ class AuthenticationNotifier extends StateNotifier<AuthenticationState> { | |||||||
|  |  | ||||||
|   Future<bool> logout() async { |   Future<bool> logout() async { | ||||||
|     Hive.box(userInfoBox).delete(accessTokenKey); |     Hive.box(userInfoBox).delete(accessTokenKey); | ||||||
|     state = AuthenticationState( |     state = state.copyWith(isAuthenticated: false); | ||||||
|       deviceId: "", |  | ||||||
|       deviceType: "", |  | ||||||
|       userId: "", |  | ||||||
|       userEmail: "", |  | ||||||
|       firstName: '', |  | ||||||
|       lastName: '', |  | ||||||
|       profileImagePath: '', |  | ||||||
|       shouldChangePassword: false, |  | ||||||
|       isAuthenticated: false, |  | ||||||
|       isAdmin: false, |  | ||||||
|       deviceInfo: DeviceInfoRemote( |  | ||||||
|         id: 0, |  | ||||||
|         userId: "", |  | ||||||
|         deviceId: "", |  | ||||||
|         deviceType: "", |  | ||||||
|         notificationToken: "", |  | ||||||
|         createdAt: "", |  | ||||||
|         isAutoBackup: false, |  | ||||||
|       ), |  | ||||||
|     ); |  | ||||||
|  |  | ||||||
|     return true; |     return true; | ||||||
|   } |   } | ||||||
| @@ -157,11 +157,13 @@ class AuthenticationNotifier extends StateNotifier<AuthenticationState> { | |||||||
|   setAutoBackup(bool backupState) async { |   setAutoBackup(bool backupState) async { | ||||||
|     var deviceInfo = await _deviceInfoService.getDeviceInfo(); |     var deviceInfo = await _deviceInfoService.getDeviceInfo(); | ||||||
|     var deviceId = deviceInfo["deviceId"]; |     var deviceId = deviceInfo["deviceId"]; | ||||||
|     var deviceType = deviceInfo["deviceType"]; |  | ||||||
|  |  | ||||||
|     DeviceInfoRemote deviceInfoRemote = |     DeviceTypeEnum deviceType = deviceInfo["deviceType"]; | ||||||
|  |  | ||||||
|  |     DeviceInfoResponseDto updatedDeviceInfo = | ||||||
|         await _backupService.setAutoBackup(backupState, deviceId, deviceType); |         await _backupService.setAutoBackup(backupState, deviceId, deviceType); | ||||||
|     state = state.copyWith(deviceInfo: deviceInfoRemote); |  | ||||||
|  |     state = state.copyWith(deviceInfo: updatedDeviceInfo); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   updateUserProfileImagePath(String path) { |   updateUserProfileImagePath(String path) { | ||||||
| @@ -169,19 +171,20 @@ class AuthenticationNotifier extends StateNotifier<AuthenticationState> { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   Future<bool> changePassword(String newPassword) async { |   Future<bool> changePassword(String newPassword) async { | ||||||
|     Response res = await _networkService.putRequest( |     try { | ||||||
|       url: 'user', |       await _apiService.userApi.updateUser( | ||||||
|       data: { |         UpdateUserDto( | ||||||
|         'id': state.userId, |           id: state.userId, | ||||||
|         'password': newPassword, |           password: newPassword, | ||||||
|         'shouldChangePassword': false, |           shouldChangePassword: false, | ||||||
|       }, |         ), | ||||||
|     ); |       ); | ||||||
|  |  | ||||||
|     if (res.statusCode == 200) { |  | ||||||
|       state = state.copyWith(shouldChangePassword: false); |       state = state.copyWith(shouldChangePassword: false); | ||||||
|  |  | ||||||
|       return true; |       return true; | ||||||
|     } else { |     } catch (e) { | ||||||
|  |       debugPrint("Error changing password $e"); | ||||||
|       return false; |       return false; | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| @@ -192,6 +195,6 @@ final authenticationProvider = | |||||||
|   return AuthenticationNotifier( |   return AuthenticationNotifier( | ||||||
|     ref.watch(deviceInfoServiceProvider), |     ref.watch(deviceInfoServiceProvider), | ||||||
|     ref.watch(backupServiceProvider), |     ref.watch(backupServiceProvider), | ||||||
|     ref.watch(networkServiceProvider), |     ref.watch(apiServiceProvider), | ||||||
|   ); |   ); | ||||||
| }); | }); | ||||||
|   | |||||||
| @@ -140,35 +140,36 @@ class ChangePasswordButton extends ConsumerWidget { | |||||||
|   @override |   @override | ||||||
|   Widget build(BuildContext context, WidgetRef ref) { |   Widget build(BuildContext context, WidgetRef ref) { | ||||||
|     return ElevatedButton( |     return ElevatedButton( | ||||||
|         style: ElevatedButton.styleFrom( |       style: ElevatedButton.styleFrom( | ||||||
|           visualDensity: VisualDensity.standard, |         visualDensity: VisualDensity.standard, | ||||||
|           primary: Theme.of(context).primaryColor, |         primary: Theme.of(context).primaryColor, | ||||||
|           onPrimary: Colors.grey[50], |         onPrimary: Colors.grey[50], | ||||||
|           elevation: 2, |         elevation: 2, | ||||||
|           padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 25), |         padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 25), | ||||||
|         ), |       ), | ||||||
|         onPressed: () async { |       onPressed: () async { | ||||||
|           if (formKey.currentState!.validate()) { |         if (formKey.currentState!.validate()) { | ||||||
|             var isSuccess = await ref |           var isSuccess = await ref | ||||||
|                 .watch(authenticationProvider.notifier) |               .watch(authenticationProvider.notifier) | ||||||
|                 .changePassword(passwordController.value.text); |               .changePassword(passwordController.value.text); | ||||||
|  |  | ||||||
|             if (isSuccess) { |           if (isSuccess) { | ||||||
|               bool res = |             bool res = | ||||||
|                   await ref.watch(authenticationProvider.notifier).logout(); |                 await ref.watch(authenticationProvider.notifier).logout(); | ||||||
|  |  | ||||||
|               if (res) { |             if (res) { | ||||||
|                 ref.watch(backupProvider.notifier).cancelBackup(); |               ref.watch(backupProvider.notifier).cancelBackup(); | ||||||
|                 ref.watch(assetProvider.notifier).clearAllAsset(); |               ref.watch(assetProvider.notifier).clearAllAsset(); | ||||||
|                 ref.watch(websocketProvider.notifier).disconnect(); |               ref.watch(websocketProvider.notifier).disconnect(); | ||||||
|                 AutoRouter.of(context).replace(const LoginRoute()); |               AutoRouter.of(context).replace(const LoginRoute()); | ||||||
|               } |  | ||||||
|             } |             } | ||||||
|           } |           } | ||||||
|         }, |         } | ||||||
|         child: const Text( |       }, | ||||||
|           "Change Password", |       child: const Text( | ||||||
|           style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold), |         "Change Password", | ||||||
|         )); |         style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold), | ||||||
|  |       ), | ||||||
|  |     ); | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -22,22 +22,25 @@ class LoginForm extends HookConsumerWidget { | |||||||
|     final passwordController = |     final passwordController = | ||||||
|         useTextEditingController.fromValue(TextEditingValue.empty); |         useTextEditingController.fromValue(TextEditingValue.empty); | ||||||
|     final serverEndpointController = |     final serverEndpointController = | ||||||
|         useTextEditingController(text: 'login_endpoint_hint'.tr()); |         useTextEditingController(text: 'login_form_endpoint_hint'.tr()); | ||||||
|     final isSaveLoginInfo = useState<bool>(false); |     final isSaveLoginInfo = useState<bool>(false); | ||||||
|  |  | ||||||
|     useEffect(() { |     useEffect( | ||||||
|       var loginInfo = |       () { | ||||||
|           Hive.box<HiveSavedLoginInfo>(hiveLoginInfoBox).get(savedLoginInfoKey); |         var loginInfo = Hive.box<HiveSavedLoginInfo>(hiveLoginInfoBox) | ||||||
|  |             .get(savedLoginInfoKey); | ||||||
|  |  | ||||||
|       if (loginInfo != null) { |         if (loginInfo != null) { | ||||||
|         usernameController.text = loginInfo.email; |           usernameController.text = loginInfo.email; | ||||||
|         passwordController.text = loginInfo.password; |           passwordController.text = loginInfo.password; | ||||||
|         serverEndpointController.text = loginInfo.serverUrl; |           serverEndpointController.text = loginInfo.serverUrl; | ||||||
|         isSaveLoginInfo.value = loginInfo.isSaveLogin; |           isSaveLoginInfo.value = loginInfo.isSaveLogin; | ||||||
|       } |         } | ||||||
|  |  | ||||||
|       return null; |         return null; | ||||||
|     }, []); |       }, | ||||||
|  |       [], | ||||||
|  |     ); | ||||||
|  |  | ||||||
|     return Center( |     return Center( | ||||||
|       child: ConstrainedBox( |       child: ConstrainedBox( | ||||||
| @@ -71,14 +74,16 @@ class LoginForm extends HookConsumerWidget { | |||||||
|                 dense: true, |                 dense: true, | ||||||
|                 side: const BorderSide(color: Colors.grey, width: 1.5), |                 side: const BorderSide(color: Colors.grey, width: 1.5), | ||||||
|                 shape: RoundedRectangleBorder( |                 shape: RoundedRectangleBorder( | ||||||
|                     borderRadius: BorderRadius.circular(5)), |                   borderRadius: BorderRadius.circular(5), | ||||||
|  |                 ), | ||||||
|                 enableFeedback: true, |                 enableFeedback: true, | ||||||
|                 title: const Text( |                 title: const Text( | ||||||
|                   "login_form_save_login", |                   "login_form_save_login", | ||||||
|                   style: TextStyle( |                   style: TextStyle( | ||||||
|                       fontSize: 16, |                     fontSize: 16, | ||||||
|                       fontWeight: FontWeight.bold, |                     fontWeight: FontWeight.bold, | ||||||
|                       color: Colors.grey), |                     color: Colors.grey, | ||||||
|  |                   ), | ||||||
|                 ).tr(), |                 ).tr(), | ||||||
|                 value: isSaveLoginInfo.value, |                 value: isSaveLoginInfo.value, | ||||||
|                 onChanged: (switchValue) { |                 onChanged: (switchValue) { | ||||||
| @@ -108,7 +113,6 @@ class ServerEndpointInput extends StatelessWidget { | |||||||
|       : super(key: key); |       : super(key: key); | ||||||
|  |  | ||||||
|   String? _validateInput(String? url) { |   String? _validateInput(String? url) { | ||||||
|  |  | ||||||
|     if (url?.startsWith(RegExp(r'https?://')) == true) { |     if (url?.startsWith(RegExp(r'https?://')) == true) { | ||||||
|       return null; |       return null; | ||||||
|     } else { |     } else { | ||||||
| @@ -122,7 +126,7 @@ class ServerEndpointInput extends StatelessWidget { | |||||||
|       controller: controller, |       controller: controller, | ||||||
|       decoration: InputDecoration( |       decoration: InputDecoration( | ||||||
|         labelText: 'login_form_endpoint_url'.tr(), |         labelText: 'login_form_endpoint_url'.tr(), | ||||||
|         border: OutlineInputBorder(), |         border: const OutlineInputBorder(), | ||||||
|         hintText: 'login_form_endpoint_hint'.tr(), |         hintText: 'login_form_endpoint_hint'.tr(), | ||||||
|       ), |       ), | ||||||
|       validator: _validateInput, |       validator: _validateInput, | ||||||
| @@ -140,8 +144,9 @@ class EmailInput extends StatelessWidget { | |||||||
|     if (email == null || email == '') return null; |     if (email == null || email == '') return null; | ||||||
|     if (email.endsWith(' ')) return 'login_form_err_trailing_whitespace'.tr(); |     if (email.endsWith(' ')) return 'login_form_err_trailing_whitespace'.tr(); | ||||||
|     if (email.startsWith(' ')) return 'login_form_err_leading_whitespace'.tr(); |     if (email.startsWith(' ')) return 'login_form_err_leading_whitespace'.tr(); | ||||||
|     if (email.contains(' ') || !email.contains('@')) |     if (email.contains(' ') || !email.contains('@')) { | ||||||
|       return 'login_form_err_invalid_email'.tr(); |       return 'login_form_err_invalid_email'.tr(); | ||||||
|  |     } | ||||||
|     return null; |     return null; | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -151,7 +156,7 @@ class EmailInput extends StatelessWidget { | |||||||
|       controller: controller, |       controller: controller, | ||||||
|       decoration: InputDecoration( |       decoration: InputDecoration( | ||||||
|         labelText: 'login_form_label_email'.tr(), |         labelText: 'login_form_label_email'.tr(), | ||||||
|         border: OutlineInputBorder(), |         border: const OutlineInputBorder(), | ||||||
|         hintText: 'login_form_email_hint'.tr(), |         hintText: 'login_form_email_hint'.tr(), | ||||||
|       ), |       ), | ||||||
|       validator: _validateInput, |       validator: _validateInput, | ||||||
| @@ -171,9 +176,10 @@ class PasswordInput extends StatelessWidget { | |||||||
|       obscureText: true, |       obscureText: true, | ||||||
|       controller: controller, |       controller: controller, | ||||||
|       decoration: InputDecoration( |       decoration: InputDecoration( | ||||||
|           labelText: 'login_form_label_password'.tr(), |         labelText: 'login_form_label_password'.tr(), | ||||||
|           border: OutlineInputBorder(), |         border: const OutlineInputBorder(), | ||||||
|           hintText: 'login_form_password_hint'.tr()), |         hintText: 'login_form_password_hint'.tr(), | ||||||
|  |       ), | ||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
| } | } | ||||||
| @@ -195,43 +201,47 @@ class LoginButton extends ConsumerWidget { | |||||||
|   @override |   @override | ||||||
|   Widget build(BuildContext context, WidgetRef ref) { |   Widget build(BuildContext context, WidgetRef ref) { | ||||||
|     return ElevatedButton( |     return ElevatedButton( | ||||||
|         style: ElevatedButton.styleFrom( |       style: ElevatedButton.styleFrom( | ||||||
|           visualDensity: VisualDensity.standard, |         visualDensity: VisualDensity.standard, | ||||||
|           primary: Theme.of(context).primaryColor, |         primary: Theme.of(context).primaryColor, | ||||||
|           onPrimary: Colors.grey[50], |         onPrimary: Colors.grey[50], | ||||||
|           elevation: 2, |         elevation: 2, | ||||||
|           padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 25), |         padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 25), | ||||||
|         ), |       ), | ||||||
|         onPressed: () async { |       onPressed: () async { | ||||||
|           // This will remove current cache asset state of previous user login. |         // This will remove current cache asset state of previous user login. | ||||||
|           ref.watch(assetProvider.notifier).clearAllAsset(); |         ref.watch(assetProvider.notifier).clearAllAsset(); | ||||||
|  |  | ||||||
|           var isAuthenticated = await ref |         var isAuthenticated = | ||||||
|               .watch(authenticationProvider.notifier) |             await ref.watch(authenticationProvider.notifier).login( | ||||||
|               .login(emailController.text, passwordController.text, |                   emailController.text, | ||||||
|                   serverEndpointController.text, isSavedLoginInfo); |                   passwordController.text, | ||||||
|  |                   serverEndpointController.text, | ||||||
|  |                   isSavedLoginInfo, | ||||||
|  |                 ); | ||||||
|  |  | ||||||
|           if (isAuthenticated) { |         if (isAuthenticated) { | ||||||
|             // Resume backup (if enable) then navigate |           // Resume backup (if enable) then navigate | ||||||
|  |  | ||||||
|             if (ref.watch(authenticationProvider).shouldChangePassword && |           if (ref.watch(authenticationProvider).shouldChangePassword && | ||||||
|                 !ref.watch(authenticationProvider).isAdmin) { |               !ref.watch(authenticationProvider).isAdmin) { | ||||||
|               AutoRouter.of(context).push(const ChangePasswordRoute()); |             AutoRouter.of(context).push(const ChangePasswordRoute()); | ||||||
|             } else { |  | ||||||
|               ref.watch(backupProvider.notifier).resumeBackup(); |  | ||||||
|               AutoRouter.of(context).pushNamed("/tab-controller-page"); |  | ||||||
|             } |  | ||||||
|           } else { |           } else { | ||||||
|             ImmichToast.show( |             ref.watch(backupProvider.notifier).resumeBackup(); | ||||||
|               context: context, |             AutoRouter.of(context).pushNamed("/tab-controller-page"); | ||||||
|               msg: "login_failed".tr(), |  | ||||||
|               toastType: ToastType.error, |  | ||||||
|             ); |  | ||||||
|           } |           } | ||||||
|         }, |         } else { | ||||||
|         child: const Text( |           ImmichToast.show( | ||||||
|           "login_form_button_text", |             context: context, | ||||||
|           style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold), |             msg: "login_form_failed_login".tr(), | ||||||
|         ).tr()); |             toastType: ToastType.error, | ||||||
|  |           ); | ||||||
|  |         } | ||||||
|  |       }, | ||||||
|  |       child: const Text( | ||||||
|  |         "login_form_button_text", | ||||||
|  |         style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold), | ||||||
|  |       ).tr(), | ||||||
|  |     ); | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,84 +0,0 @@ | |||||||
| import 'dart:convert'; |  | ||||||
|  |  | ||||||
| class CuratedLocation { |  | ||||||
|   final String id; |  | ||||||
|   final String city; |  | ||||||
|   final String resizePath; |  | ||||||
|   final String deviceAssetId; |  | ||||||
|   final String deviceId; |  | ||||||
|  |  | ||||||
|   CuratedLocation({ |  | ||||||
|     required this.id, |  | ||||||
|     required this.city, |  | ||||||
|     required this.resizePath, |  | ||||||
|     required this.deviceAssetId, |  | ||||||
|     required this.deviceId, |  | ||||||
|   }); |  | ||||||
|  |  | ||||||
|   CuratedLocation copyWith({ |  | ||||||
|     String? id, |  | ||||||
|     String? city, |  | ||||||
|     String? resizePath, |  | ||||||
|     String? deviceAssetId, |  | ||||||
|     String? deviceId, |  | ||||||
|   }) { |  | ||||||
|     return CuratedLocation( |  | ||||||
|       id: id ?? this.id, |  | ||||||
|       city: city ?? this.city, |  | ||||||
|       resizePath: resizePath ?? this.resizePath, |  | ||||||
|       deviceAssetId: deviceAssetId ?? this.deviceAssetId, |  | ||||||
|       deviceId: deviceId ?? this.deviceId, |  | ||||||
|     ); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   Map<String, dynamic> toMap() { |  | ||||||
|     return { |  | ||||||
|       'id': id, |  | ||||||
|       'city': city, |  | ||||||
|       'resizePath': resizePath, |  | ||||||
|       'deviceAssetId': deviceAssetId, |  | ||||||
|       'deviceId': deviceId, |  | ||||||
|     }; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   factory CuratedLocation.fromMap(Map<String, dynamic> map) { |  | ||||||
|     return CuratedLocation( |  | ||||||
|       id: map['id'] ?? '', |  | ||||||
|       city: map['city'] ?? '', |  | ||||||
|       resizePath: map['resizePath'] ?? '', |  | ||||||
|       deviceAssetId: map['deviceAssetId'] ?? '', |  | ||||||
|       deviceId: map['deviceId'] ?? '', |  | ||||||
|     ); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   String toJson() => json.encode(toMap()); |  | ||||||
|  |  | ||||||
|   factory CuratedLocation.fromJson(String source) => |  | ||||||
|       CuratedLocation.fromMap(json.decode(source)); |  | ||||||
|  |  | ||||||
|   @override |  | ||||||
|   String toString() { |  | ||||||
|     return 'CuratedLocation(id: $id, city: $city, resizePath: $resizePath, deviceAssetId: $deviceAssetId, deviceId: $deviceId)'; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   @override |  | ||||||
|   bool operator ==(Object other) { |  | ||||||
|     if (identical(this, other)) return true; |  | ||||||
|  |  | ||||||
|     return other is CuratedLocation && |  | ||||||
|         other.id == id && |  | ||||||
|         other.city == city && |  | ||||||
|         other.resizePath == resizePath && |  | ||||||
|         other.deviceAssetId == deviceAssetId && |  | ||||||
|         other.deviceId == deviceId; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   @override |  | ||||||
|   int get hashCode { |  | ||||||
|     return id.hashCode ^ |  | ||||||
|         city.hashCode ^ |  | ||||||
|         resizePath.hashCode ^ |  | ||||||
|         deviceAssetId.hashCode ^ |  | ||||||
|         deviceId.hashCode; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| @@ -1,85 +0,0 @@ | |||||||
| import 'dart:convert'; |  | ||||||
|  |  | ||||||
| class CuratedObject { |  | ||||||
|   final String id; |  | ||||||
|   final String object; |  | ||||||
|   final String resizePath; |  | ||||||
|   final String deviceAssetId; |  | ||||||
|   final String deviceId; |  | ||||||
|   CuratedObject({ |  | ||||||
|     required this.id, |  | ||||||
|     required this.object, |  | ||||||
|     required this.resizePath, |  | ||||||
|     required this.deviceAssetId, |  | ||||||
|     required this.deviceId, |  | ||||||
|   }); |  | ||||||
|  |  | ||||||
|   CuratedObject copyWith({ |  | ||||||
|     String? id, |  | ||||||
|     String? object, |  | ||||||
|     String? resizePath, |  | ||||||
|     String? deviceAssetId, |  | ||||||
|     String? deviceId, |  | ||||||
|   }) { |  | ||||||
|     return CuratedObject( |  | ||||||
|       id: id ?? this.id, |  | ||||||
|       object: object ?? this.object, |  | ||||||
|       resizePath: resizePath ?? this.resizePath, |  | ||||||
|       deviceAssetId: deviceAssetId ?? this.deviceAssetId, |  | ||||||
|       deviceId: deviceId ?? this.deviceId, |  | ||||||
|     ); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   Map<String, dynamic> toMap() { |  | ||||||
|     final result = <String, dynamic>{}; |  | ||||||
|  |  | ||||||
|     result.addAll({'id': id}); |  | ||||||
|     result.addAll({'object': object}); |  | ||||||
|     result.addAll({'resizePath': resizePath}); |  | ||||||
|     result.addAll({'deviceAssetId': deviceAssetId}); |  | ||||||
|     result.addAll({'deviceId': deviceId}); |  | ||||||
|  |  | ||||||
|     return result; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   factory CuratedObject.fromMap(Map<String, dynamic> map) { |  | ||||||
|     return CuratedObject( |  | ||||||
|       id: map['id'] ?? '', |  | ||||||
|       object: map['object'] ?? '', |  | ||||||
|       resizePath: map['resizePath'] ?? '', |  | ||||||
|       deviceAssetId: map['deviceAssetId'] ?? '', |  | ||||||
|       deviceId: map['deviceId'] ?? '', |  | ||||||
|     ); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   String toJson() => json.encode(toMap()); |  | ||||||
|  |  | ||||||
|   factory CuratedObject.fromJson(String source) => |  | ||||||
|       CuratedObject.fromMap(json.decode(source)); |  | ||||||
|  |  | ||||||
|   @override |  | ||||||
|   String toString() { |  | ||||||
|     return 'CuratedObject(id: $id, object: $object, resizePath: $resizePath, deviceAssetId: $deviceAssetId, deviceId: $deviceId)'; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   @override |  | ||||||
|   bool operator ==(Object other) { |  | ||||||
|     if (identical(this, other)) return true; |  | ||||||
|  |  | ||||||
|     return other is CuratedObject && |  | ||||||
|         other.id == id && |  | ||||||
|         other.object == object && |  | ||||||
|         other.resizePath == resizePath && |  | ||||||
|         other.deviceAssetId == deviceAssetId && |  | ||||||
|         other.deviceId == deviceId; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   @override |  | ||||||
|   int get hashCode { |  | ||||||
|     return id.hashCode ^ |  | ||||||
|         object.hashCode ^ |  | ||||||
|         resizePath.hashCode ^ |  | ||||||
|         deviceAssetId.hashCode ^ |  | ||||||
|         deviceId.hashCode; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| @@ -1,13 +1,13 @@ | |||||||
| import 'dart:convert'; | import 'dart:convert'; | ||||||
|  |  | ||||||
| import 'package:collection/collection.dart'; | import 'package:collection/collection.dart'; | ||||||
| import 'package:immich_mobile/shared/models/immich_asset.model.dart'; | import 'package:openapi/api.dart'; | ||||||
|  |  | ||||||
| class SearchResultPageState { | class SearchResultPageState { | ||||||
|   final bool isLoading; |   final bool isLoading; | ||||||
|   final bool isSuccess; |   final bool isSuccess; | ||||||
|   final bool isError; |   final bool isError; | ||||||
|   final List<ImmichAsset> searchResult; |   final List<AssetResponseDto> searchResult; | ||||||
|  |  | ||||||
|   SearchResultPageState({ |   SearchResultPageState({ | ||||||
|     required this.isLoading, |     required this.isLoading, | ||||||
| @@ -20,7 +20,7 @@ class SearchResultPageState { | |||||||
|     bool? isLoading, |     bool? isLoading, | ||||||
|     bool? isSuccess, |     bool? isSuccess, | ||||||
|     bool? isError, |     bool? isError, | ||||||
|     List<ImmichAsset>? searchResult, |     List<AssetResponseDto>? searchResult, | ||||||
|   }) { |   }) { | ||||||
|     return SearchResultPageState( |     return SearchResultPageState( | ||||||
|       isLoading: isLoading ?? this.isLoading, |       isLoading: isLoading ?? this.isLoading, | ||||||
| @@ -35,7 +35,7 @@ class SearchResultPageState { | |||||||
|       'isLoading': isLoading, |       'isLoading': isLoading, | ||||||
|       'isSuccess': isSuccess, |       'isSuccess': isSuccess, | ||||||
|       'isError': isError, |       'isError': isError, | ||||||
|       'searchResult': searchResult.map((x) => x.toMap()).toList(), |       'searchResult': searchResult.map((x) => x.toJson()).toList(), | ||||||
|     }; |     }; | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -44,8 +44,9 @@ class SearchResultPageState { | |||||||
|       isLoading: map['isLoading'] ?? false, |       isLoading: map['isLoading'] ?? false, | ||||||
|       isSuccess: map['isSuccess'] ?? false, |       isSuccess: map['isSuccess'] ?? false, | ||||||
|       isError: map['isError'] ?? false, |       isError: map['isError'] ?? false, | ||||||
|       searchResult: List<ImmichAsset>.from( |       searchResult: List<AssetResponseDto>.from( | ||||||
|           map['searchResult']?.map((x) => ImmichAsset.fromMap(x))), |         map['searchResult']?.map((x) => AssetResponseDto.mapFromJson(x)), | ||||||
|  |       ), | ||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,9 +1,8 @@ | |||||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||||
| import 'package:immich_mobile/modules/search/models/curated_location.model.dart'; |  | ||||||
| import 'package:immich_mobile/modules/search/models/curated_object.model.dart'; |  | ||||||
| import 'package:immich_mobile/modules/search/models/search_page_state.model.dart'; | import 'package:immich_mobile/modules/search/models/search_page_state.model.dart'; | ||||||
|  |  | ||||||
| import 'package:immich_mobile/modules/search/services/search.service.dart'; | import 'package:immich_mobile/modules/search/services/search.service.dart'; | ||||||
|  | import 'package:openapi/api.dart'; | ||||||
|  |  | ||||||
| class SearchPageStateNotifier extends StateNotifier<SearchPageState> { | class SearchPageStateNotifier extends StateNotifier<SearchPageState> { | ||||||
|   SearchPageStateNotifier(this._searchService) |   SearchPageStateNotifier(this._searchService) | ||||||
| @@ -58,7 +57,7 @@ final searchPageStateProvider = | |||||||
| }); | }); | ||||||
|  |  | ||||||
| final getCuratedLocationProvider = | final getCuratedLocationProvider = | ||||||
|     FutureProvider.autoDispose<List<CuratedLocation>>((ref) async { |     FutureProvider.autoDispose<List<CuratedLocationsResponseDto>>((ref) async { | ||||||
|   final SearchService searchService = ref.watch(searchServiceProvider); |   final SearchService searchService = ref.watch(searchServiceProvider); | ||||||
|  |  | ||||||
|   var curatedLocation = await searchService.getCuratedLocation(); |   var curatedLocation = await searchService.getCuratedLocation(); | ||||||
| @@ -66,7 +65,7 @@ final getCuratedLocationProvider = | |||||||
| }); | }); | ||||||
|  |  | ||||||
| final getCuratedObjectProvider = | final getCuratedObjectProvider = | ||||||
|     FutureProvider.autoDispose<List<CuratedObject>>((ref) async { |     FutureProvider.autoDispose<List<CuratedObjectsResponseDto>>((ref) async { | ||||||
|   final SearchService searchService = ref.watch(searchServiceProvider); |   final SearchService searchService = ref.watch(searchServiceProvider); | ||||||
|  |  | ||||||
|   var curatedObject = await searchService.getCuratedObjects(); |   var curatedObject = await searchService.getCuratedObjects(); | ||||||
|   | |||||||
| @@ -3,8 +3,8 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; | |||||||
| import 'package:immich_mobile/modules/search/models/search_result_page_state.model.dart'; | import 'package:immich_mobile/modules/search/models/search_result_page_state.model.dart'; | ||||||
|  |  | ||||||
| import 'package:immich_mobile/modules/search/services/search.service.dart'; | import 'package:immich_mobile/modules/search/services/search.service.dart'; | ||||||
| import 'package:immich_mobile/shared/models/immich_asset.model.dart'; |  | ||||||
| import 'package:intl/intl.dart'; | import 'package:intl/intl.dart'; | ||||||
|  | import 'package:openapi/api.dart'; | ||||||
|  |  | ||||||
| class SearchResultPageNotifier extends StateNotifier<SearchResultPageState> { | class SearchResultPageNotifier extends StateNotifier<SearchResultPageState> { | ||||||
|   SearchResultPageNotifier(this._searchService) |   SearchResultPageNotifier(this._searchService) | ||||||
| @@ -21,19 +21,29 @@ class SearchResultPageNotifier extends StateNotifier<SearchResultPageState> { | |||||||
|  |  | ||||||
|   void search(String searchTerm) async { |   void search(String searchTerm) async { | ||||||
|     state = state.copyWith( |     state = state.copyWith( | ||||||
|         searchResult: [], isError: false, isLoading: true, isSuccess: false); |       searchResult: [], | ||||||
|  |       isError: false, | ||||||
|  |       isLoading: true, | ||||||
|  |       isSuccess: false, | ||||||
|  |     ); | ||||||
|  |  | ||||||
|     List<ImmichAsset>? assets = await _searchService.searchAsset(searchTerm); |     List<AssetResponseDto>? assets = | ||||||
|  |         await _searchService.searchAsset(searchTerm); | ||||||
|  |  | ||||||
|     if (assets != null) { |     if (assets != null) { | ||||||
|       state = state.copyWith( |       state = state.copyWith( | ||||||
|           searchResult: assets, |         searchResult: assets, | ||||||
|           isError: false, |         isError: false, | ||||||
|           isLoading: false, |         isLoading: false, | ||||||
|           isSuccess: true); |         isSuccess: true, | ||||||
|  |       ); | ||||||
|     } else { |     } else { | ||||||
|       state = state.copyWith( |       state = state.copyWith( | ||||||
|           searchResult: [], isError: true, isLoading: false, isSuccess: false); |         searchResult: [], | ||||||
|  |         isError: true, | ||||||
|  |         isLoading: false, | ||||||
|  |         isSuccess: false, | ||||||
|  |       ); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| } | } | ||||||
| @@ -48,7 +58,11 @@ final searchResultGroupByDateTimeProvider = StateProvider((ref) { | |||||||
|   var assets = ref.watch(searchResultPageProvider).searchResult; |   var assets = ref.watch(searchResultPageProvider).searchResult; | ||||||
|  |  | ||||||
|   assets.sortByCompare<DateTime>( |   assets.sortByCompare<DateTime>( | ||||||
|       (e) => DateTime.parse(e.createdAt), (a, b) => b.compareTo(a)); |     (e) => DateTime.parse(e.createdAt), | ||||||
|   return assets.groupListsBy((element) => |     (a, b) => b.compareTo(a), | ||||||
|       DateFormat('y-MM-dd').format(DateTime.parse(element.createdAt))); |   ); | ||||||
|  |   return assets.groupListsBy( | ||||||
|  |     (element) => | ||||||
|  |         DateFormat('y-MM-dd').format(DateTime.parse(element.createdAt)), | ||||||
|  |   ); | ||||||
| }); | }); | ||||||
|   | |||||||
| @@ -1,79 +1,54 @@ | |||||||
| import 'dart:convert'; |  | ||||||
|  |  | ||||||
| import 'package:flutter/material.dart'; | import 'package:flutter/material.dart'; | ||||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||||
| import 'package:immich_mobile/modules/search/models/curated_location.model.dart'; | import 'package:immich_mobile/shared/services/api.service.dart'; | ||||||
| import 'package:immich_mobile/modules/search/models/curated_object.model.dart'; | import 'package:openapi/api.dart'; | ||||||
| import 'package:immich_mobile/shared/models/immich_asset.model.dart'; |  | ||||||
| import 'package:immich_mobile/shared/services/network.service.dart'; |  | ||||||
|  |  | ||||||
| final searchServiceProvider = | final searchServiceProvider = Provider( | ||||||
|     Provider((ref) => SearchService(ref.watch(networkServiceProvider))); |   (ref) => SearchService( | ||||||
|  |     ref.watch(apiServiceProvider), | ||||||
|  |   ), | ||||||
|  | ); | ||||||
|  |  | ||||||
| class SearchService { | class SearchService { | ||||||
|   final NetworkService _networkService; |   final ApiService _apiService; | ||||||
|   SearchService(this._networkService); |   SearchService(this._apiService); | ||||||
|  |  | ||||||
|   Future<List<String>?> getUserSuggestedSearchTerms() async { |   Future<List<String>?> getUserSuggestedSearchTerms() async { | ||||||
|     try { |     try { | ||||||
|       var res = await _networkService.getRequest(url: "asset/searchTerm"); |       return await _apiService.assetApi.getAssetSearchTerms(); | ||||||
|       List<dynamic> decodedData = jsonDecode(res.toString()); |  | ||||||
|  |  | ||||||
|       return List.from(decodedData); |  | ||||||
|     } catch (e) { |     } catch (e) { | ||||||
|       debugPrint("[ERROR] [getUserSuggestedSearchTerms] ${e.toString()}"); |       debugPrint("[ERROR] [getUserSuggestedSearchTerms] ${e.toString()}"); | ||||||
|       return []; |       return []; | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   Future<List<ImmichAsset>?> searchAsset(String searchTerm) async { |   Future<List<AssetResponseDto>?> searchAsset(String searchTerm) async { | ||||||
|     try { |     try { | ||||||
|       var res = await _networkService.postRequest( |       return await _apiService.assetApi | ||||||
|         url: "asset/search", |           .searchAsset(SearchAssetDto(searchTerm: searchTerm)); | ||||||
|         data: {"searchTerm": searchTerm}, |  | ||||||
|       ); |  | ||||||
|  |  | ||||||
|       List<dynamic> decodedData = jsonDecode(res.toString()); |  | ||||||
|  |  | ||||||
|       List<ImmichAsset> result = |  | ||||||
|           List.from(decodedData.map((a) => ImmichAsset.fromMap(a))); |  | ||||||
|  |  | ||||||
|       return result; |  | ||||||
|     } catch (e) { |     } catch (e) { | ||||||
|       debugPrint("[ERROR] [searchAsset] ${e.toString()}"); |       debugPrint("[ERROR] [searchAsset] ${e.toString()}"); | ||||||
|       return null; |       return null; | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   Future<List<CuratedLocation>?> getCuratedLocation() async { |   Future<List<CuratedLocationsResponseDto>?> getCuratedLocation() async { | ||||||
|     try { |     try { | ||||||
|       var res = await _networkService.getRequest(url: "asset/allLocation"); |       var locations = await _apiService.assetApi.getCuratedLocations(); | ||||||
|  |  | ||||||
|       List<dynamic> decodedData = jsonDecode(res.toString()); |       return locations; | ||||||
|  |  | ||||||
|       List<CuratedLocation> result = |  | ||||||
|           List.from(decodedData.map((a) => CuratedLocation.fromMap(a))); |  | ||||||
|  |  | ||||||
|       return result; |  | ||||||
|     } catch (e) { |     } catch (e) { | ||||||
|       debugPrint("[ERROR] [getCuratedLocation] ${e.toString()}"); |       debugPrint("Error [getCuratedLocation] ${e.toString()}"); | ||||||
|       throw Error(); |       return []; | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   Future<List<CuratedObject>?> getCuratedObjects() async { |   Future<List<CuratedObjectsResponseDto>?> getCuratedObjects() async { | ||||||
|     try { |     try { | ||||||
|       var res = await _networkService.getRequest(url: "asset/allObjects"); |       return await _apiService.assetApi.getCuratedObjects(); | ||||||
|  |  | ||||||
|       List<dynamic> decodedData = jsonDecode(res.toString()); |  | ||||||
|  |  | ||||||
|       List<CuratedObject> result = |  | ||||||
|           List.from(decodedData.map((a) => CuratedObject.fromMap(a))); |  | ||||||
|  |  | ||||||
|       return result; |  | ||||||
|     } catch (e) { |     } catch (e) { | ||||||
|       debugPrint("[ERROR] [CuratedObject] ${e.toString()}"); |       debugPrint("Error [getCuratedObjects] ${e.toString()}"); | ||||||
|       throw Error(); |       throw []; | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -5,9 +5,11 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; | |||||||
| import 'package:immich_mobile/modules/search/providers/search_page_state.provider.dart'; | import 'package:immich_mobile/modules/search/providers/search_page_state.provider.dart'; | ||||||
|  |  | ||||||
| class SearchBar extends HookConsumerWidget with PreferredSizeWidget { | class SearchBar extends HookConsumerWidget with PreferredSizeWidget { | ||||||
|   SearchBar( |   SearchBar({ | ||||||
|       {Key? key, required this.searchFocusNode, required this.onSubmitted}) |     Key? key, | ||||||
|       : super(key: key); |     required this.searchFocusNode, | ||||||
|  |     required this.onSubmitted, | ||||||
|  |   }) : super(key: key); | ||||||
|  |  | ||||||
|   final FocusNode searchFocusNode; |   final FocusNode searchFocusNode; | ||||||
|   final Function(String) onSubmitted; |   final Function(String) onSubmitted; | ||||||
| @@ -26,7 +28,8 @@ class SearchBar extends HookConsumerWidget with PreferredSizeWidget { | |||||||
|                 ref.watch(searchPageStateProvider.notifier).disableSearch(); |                 ref.watch(searchPageStateProvider.notifier).disableSearch(); | ||||||
|                 searchTermController.clear(); |                 searchTermController.clear(); | ||||||
|               }, |               }, | ||||||
|               icon: const Icon(Icons.arrow_back_ios_rounded)) |               icon: const Icon(Icons.arrow_back_ios_rounded), | ||||||
|  |             ) | ||||||
|           : const Icon(Icons.search_rounded), |           : const Icon(Icons.search_rounded), | ||||||
|       title: TextField( |       title: TextField( | ||||||
|         controller: searchTermController, |         controller: searchTermController, | ||||||
| @@ -50,10 +53,10 @@ class SearchBar extends HookConsumerWidget with PreferredSizeWidget { | |||||||
|         }, |         }, | ||||||
|         decoration: InputDecoration( |         decoration: InputDecoration( | ||||||
|           hintText: 'search_bar_hint'.tr(), |           hintText: 'search_bar_hint'.tr(), | ||||||
|           enabledBorder: UnderlineInputBorder( |           enabledBorder: const UnderlineInputBorder( | ||||||
|             borderSide: BorderSide(color: Colors.transparent), |             borderSide: BorderSide(color: Colors.transparent), | ||||||
|           ), |           ), | ||||||
|           focusedBorder: UnderlineInputBorder( |           focusedBorder: const UnderlineInputBorder( | ||||||
|             borderSide: BorderSide(color: Colors.transparent), |             borderSide: BorderSide(color: Colors.transparent), | ||||||
|           ), |           ), | ||||||
|         ), |         ), | ||||||
|   | |||||||
| @@ -2,15 +2,14 @@ import 'package:cached_network_image/cached_network_image.dart'; | |||||||
| import 'package:flutter/material.dart'; | import 'package:flutter/material.dart'; | ||||||
| import 'package:hive_flutter/hive_flutter.dart'; | import 'package:hive_flutter/hive_flutter.dart'; | ||||||
| import 'package:immich_mobile/constants/hive_box.dart'; | import 'package:immich_mobile/constants/hive_box.dart'; | ||||||
| import 'package:immich_mobile/utils/capitalize_first_letter.dart'; |  | ||||||
|  |  | ||||||
| class ThumbnailWithInfo extends StatelessWidget { | class ThumbnailWithInfo extends StatelessWidget { | ||||||
|   const ThumbnailWithInfo( |   const ThumbnailWithInfo({ | ||||||
|       {Key? key, |     Key? key, | ||||||
|       required this.textInfo, |     required this.textInfo, | ||||||
|       required this.imageUrl, |     required this.imageUrl, | ||||||
|       required this.onTap}) |     required this.onTap, | ||||||
|       : super(key: key); |   }) : super(key: key); | ||||||
|  |  | ||||||
|   final String textInfo; |   final String textInfo; | ||||||
|   final String imageUrl; |   final String imageUrl; | ||||||
|   | |||||||
| @@ -5,8 +5,6 @@ import 'package:flutter_hooks/flutter_hooks.dart'; | |||||||
| import 'package:hive_flutter/hive_flutter.dart'; | import 'package:hive_flutter/hive_flutter.dart'; | ||||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||||
| import 'package:immich_mobile/constants/hive_box.dart'; | import 'package:immich_mobile/constants/hive_box.dart'; | ||||||
| import 'package:immich_mobile/modules/search/models/curated_location.model.dart'; |  | ||||||
| import 'package:immich_mobile/modules/search/models/curated_object.model.dart'; |  | ||||||
| import 'package:immich_mobile/modules/search/providers/search_page_state.provider.dart'; | import 'package:immich_mobile/modules/search/providers/search_page_state.provider.dart'; | ||||||
| import 'package:immich_mobile/modules/search/ui/search_bar.dart'; | import 'package:immich_mobile/modules/search/ui/search_bar.dart'; | ||||||
| import 'package:immich_mobile/modules/search/ui/search_suggestion_list.dart'; | import 'package:immich_mobile/modules/search/ui/search_suggestion_list.dart'; | ||||||
| @@ -14,6 +12,7 @@ import 'package:immich_mobile/modules/search/ui/thumbnail_with_info.dart'; | |||||||
| import 'package:immich_mobile/routing/router.dart'; | import 'package:immich_mobile/routing/router.dart'; | ||||||
| import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart'; | import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart'; | ||||||
| import 'package:immich_mobile/utils/capitalize_first_letter.dart'; | import 'package:immich_mobile/utils/capitalize_first_letter.dart'; | ||||||
|  | import 'package:openapi/api.dart'; | ||||||
|  |  | ||||||
| // ignore: must_be_immutable | // ignore: must_be_immutable | ||||||
| class SearchPage extends HookConsumerWidget { | class SearchPage extends HookConsumerWidget { | ||||||
| @@ -25,15 +24,18 @@ class SearchPage extends HookConsumerWidget { | |||||||
|   Widget build(BuildContext context, WidgetRef ref) { |   Widget build(BuildContext context, WidgetRef ref) { | ||||||
|     var box = Hive.box(userInfoBox); |     var box = Hive.box(userInfoBox); | ||||||
|     final isSearchEnabled = ref.watch(searchPageStateProvider).isSearchEnabled; |     final isSearchEnabled = ref.watch(searchPageStateProvider).isSearchEnabled; | ||||||
|     AsyncValue<List<CuratedLocation>> curatedLocation = |     AsyncValue<List<CuratedLocationsResponseDto>> curatedLocation = | ||||||
|         ref.watch(getCuratedLocationProvider); |         ref.watch(getCuratedLocationProvider); | ||||||
|     AsyncValue<List<CuratedObject>> curatedObjects = |     AsyncValue<List<CuratedObjectsResponseDto>> curatedObjects = | ||||||
|         ref.watch(getCuratedObjectProvider); |         ref.watch(getCuratedObjectProvider); | ||||||
|  |  | ||||||
|     useEffect(() { |     useEffect( | ||||||
|       searchFocusNode = FocusNode(); |       () { | ||||||
|       return () => searchFocusNode.dispose(); |         searchFocusNode = FocusNode(); | ||||||
|     }, []); |         return () => searchFocusNode.dispose(); | ||||||
|  |       }, | ||||||
|  |       [], | ||||||
|  |     ); | ||||||
|  |  | ||||||
|     _onSearchSubmitted(String searchTerm) async { |     _onSearchSubmitted(String searchTerm) async { | ||||||
|       searchFocusNode.unfocus(); |       searchFocusNode.unfocus(); | ||||||
| @@ -58,16 +60,16 @@ class SearchPage extends HookConsumerWidget { | |||||||
|                     scrollDirection: Axis.horizontal, |                     scrollDirection: Axis.horizontal, | ||||||
|                     itemCount: curatedLocation.value?.length, |                     itemCount: curatedLocation.value?.length, | ||||||
|                     itemBuilder: ((context, index) { |                     itemBuilder: ((context, index) { | ||||||
|                       CuratedLocation locationInfo = curatedLocations[index]; |                       var locationInfo = curatedLocations[index]; | ||||||
|                       var thumbnailRequestUrl = |                       var thumbnailRequestUrl = | ||||||
|                           '${box.get(serverEndpointKey)}/asset/file?aid=${locationInfo.deviceAssetId}&did=${locationInfo.deviceId}&isThumb=true'; |                           '${box.get(serverEndpointKey)}/asset/thumbnail/${locationInfo.id}'; | ||||||
|  |  | ||||||
|                       return ThumbnailWithInfo( |                       return ThumbnailWithInfo( | ||||||
|                         imageUrl: thumbnailRequestUrl, |                         imageUrl: thumbnailRequestUrl, | ||||||
|                         textInfo: locationInfo.city, |                         textInfo: locationInfo.city, | ||||||
|                         onTap: () { |                         onTap: () { | ||||||
|                           AutoRouter.of(context).push( |                           AutoRouter.of(context).push( | ||||||
|                               SearchResultRoute(searchTerm: locationInfo.city)); |                             SearchResultRoute(searchTerm: locationInfo.city), | ||||||
|  |                           ); | ||||||
|                         }, |                         }, | ||||||
|                       ); |                       ); | ||||||
|                     }), |                     }), | ||||||
| @@ -109,7 +111,7 @@ class SearchPage extends HookConsumerWidget { | |||||||
|                     scrollDirection: Axis.horizontal, |                     scrollDirection: Axis.horizontal, | ||||||
|                     itemCount: curatedObjects.value?.length, |                     itemCount: curatedObjects.value?.length, | ||||||
|                     itemBuilder: ((context, index) { |                     itemBuilder: ((context, index) { | ||||||
|                       CuratedObject curatedObjectInfo = objects[index]; |                       var curatedObjectInfo = objects[index]; | ||||||
|                       var thumbnailRequestUrl = |                       var thumbnailRequestUrl = | ||||||
|                           '${box.get(serverEndpointKey)}/asset/file?aid=${curatedObjectInfo.deviceAssetId}&did=${curatedObjectInfo.deviceId}&isThumb=true'; |                           '${box.get(serverEndpointKey)}/asset/file?aid=${curatedObjectInfo.deviceAssetId}&did=${curatedObjectInfo.deviceId}&isThumb=true'; | ||||||
|  |  | ||||||
| @@ -117,9 +119,12 @@ class SearchPage extends HookConsumerWidget { | |||||||
|                         imageUrl: thumbnailRequestUrl, |                         imageUrl: thumbnailRequestUrl, | ||||||
|                         textInfo: curatedObjectInfo.object, |                         textInfo: curatedObjectInfo.object, | ||||||
|                         onTap: () { |                         onTap: () { | ||||||
|                           AutoRouter.of(context).push(SearchResultRoute( |                           AutoRouter.of(context).push( | ||||||
|  |                             SearchResultRoute( | ||||||
|                               searchTerm: curatedObjectInfo.object |                               searchTerm: curatedObjectInfo.object | ||||||
|                                   .capitalizeFirstLetter())); |                                   .capitalizeFirstLetter(), | ||||||
|  |                             ), | ||||||
|  |                           ); | ||||||
|                         }, |                         }, | ||||||
|                       ); |                       ); | ||||||
|                     }), |                     }), | ||||||
| @@ -160,7 +165,7 @@ class SearchPage extends HookConsumerWidget { | |||||||
|             ListView( |             ListView( | ||||||
|               children: [ |               children: [ | ||||||
|                 Padding( |                 Padding( | ||||||
|                   padding: EdgeInsets.all(16.0), |                   padding: const EdgeInsets.all(16.0), | ||||||
|                   child: const Text( |                   child: const Text( | ||||||
|                     "search_page_places", |                     "search_page_places", | ||||||
|                     style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16), |                     style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16), | ||||||
| @@ -168,8 +173,8 @@ class SearchPage extends HookConsumerWidget { | |||||||
|                 ), |                 ), | ||||||
|                 _buildPlaces(), |                 _buildPlaces(), | ||||||
|                 Padding( |                 Padding( | ||||||
|                   padding: EdgeInsets.all(16.0), |                   padding: const EdgeInsets.all(16.0), | ||||||
|                   child: const  Text( |                   child: const Text( | ||||||
|                     "search_page_things", |                     "search_page_things", | ||||||
|                     style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16), |                     style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16), | ||||||
|                   ).tr(), |                   ).tr(), | ||||||
|   | |||||||
| @@ -29,13 +29,18 @@ class SearchResultPage extends HookConsumerWidget { | |||||||
|  |  | ||||||
|     late FocusNode searchFocusNode; |     late FocusNode searchFocusNode; | ||||||
|  |  | ||||||
|     useEffect(() { |     useEffect( | ||||||
|       searchFocusNode = FocusNode(); |       () { | ||||||
|  |         searchFocusNode = FocusNode(); | ||||||
|  |  | ||||||
|       Future.delayed(Duration.zero, |         Future.delayed( | ||||||
|           () => ref.read(searchResultPageProvider.notifier).search(searchTerm)); |           Duration.zero, | ||||||
|       return () => searchFocusNode.dispose(); |           () => ref.read(searchResultPageProvider.notifier).search(searchTerm), | ||||||
|     }, []); |         ); | ||||||
|  |         return () => searchFocusNode.dispose(); | ||||||
|  |       }, | ||||||
|  |       [], | ||||||
|  |     ); | ||||||
|  |  | ||||||
|     _onSearchSubmitted(String newSearchTerm) { |     _onSearchSubmitted(String newSearchTerm) { | ||||||
|       debugPrint("Re-Search with $newSearchTerm"); |       debugPrint("Re-Search with $newSearchTerm"); | ||||||
| @@ -69,10 +74,10 @@ class SearchResultPage extends HookConsumerWidget { | |||||||
|         }, |         }, | ||||||
|         decoration: InputDecoration( |         decoration: InputDecoration( | ||||||
|           hintText: 'search_result_page_new_search_hint'.tr(), |           hintText: 'search_result_page_new_search_hint'.tr(), | ||||||
|           enabledBorder: UnderlineInputBorder( |           enabledBorder: const UnderlineInputBorder( | ||||||
|             borderSide: BorderSide(color: Colors.transparent), |             borderSide: BorderSide(color: Colors.transparent), | ||||||
|           ), |           ), | ||||||
|           focusedBorder: UnderlineInputBorder( |           focusedBorder: const UnderlineInputBorder( | ||||||
|             borderSide: BorderSide(color: Colors.transparent), |             borderSide: BorderSide(color: Colors.transparent), | ||||||
|           ), |           ), | ||||||
|         ), |         ), | ||||||
| @@ -90,9 +95,10 @@ class SearchResultPage extends HookConsumerWidget { | |||||||
|             Text( |             Text( | ||||||
|               currentSearchTerm.value, |               currentSearchTerm.value, | ||||||
|               style: TextStyle( |               style: TextStyle( | ||||||
|                   color: Theme.of(context).primaryColor, |                 color: Theme.of(context).primaryColor, | ||||||
|                   fontSize: 13, |                 fontSize: 13, | ||||||
|                   fontWeight: FontWeight.bold), |                 fontWeight: FontWeight.bold, | ||||||
|  |               ), | ||||||
|               maxLines: 1, |               maxLines: 1, | ||||||
|             ), |             ), | ||||||
|             Icon( |             Icon( | ||||||
| @@ -116,9 +122,10 @@ class SearchResultPage extends HookConsumerWidget { | |||||||
|  |  | ||||||
|       if (searchResultPageState.isLoading) { |       if (searchResultPageState.isLoading) { | ||||||
|         return Center( |         return Center( | ||||||
|             child: SpinKitDancingSquare( |           child: SpinKitDancingSquare( | ||||||
|           color: Theme.of(context).primaryColor, |             color: Theme.of(context).primaryColor, | ||||||
|         )); |           ), | ||||||
|  |         ); | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       if (searchResultPageState.isSuccess) { |       if (searchResultPageState.isSuccess) { | ||||||
| @@ -184,11 +191,12 @@ class SearchResultPage extends HookConsumerWidget { | |||||||
|           icon: const Icon(Icons.arrow_back_ios_rounded), |           icon: const Icon(Icons.arrow_back_ios_rounded), | ||||||
|         ), |         ), | ||||||
|         title: GestureDetector( |         title: GestureDetector( | ||||||
|             onTap: () { |           onTap: () { | ||||||
|               isNewSearch.value = true; |             isNewSearch.value = true; | ||||||
|               searchFocusNode.requestFocus(); |             searchFocusNode.requestFocus(); | ||||||
|             }, |           }, | ||||||
|             child: isNewSearch.value ? _buildTextField() : _buildChip()), |           child: isNewSearch.value ? _buildTextField() : _buildChip(), | ||||||
|  |         ), | ||||||
|         centerTitle: false, |         centerTitle: false, | ||||||
|       ), |       ), | ||||||
|       body: GestureDetector( |       body: GestureDetector( | ||||||
|   | |||||||
| @@ -1,12 +1,10 @@ | |||||||
| import 'dart:convert'; |  | ||||||
|  |  | ||||||
| import 'package:collection/collection.dart'; | import 'package:collection/collection.dart'; | ||||||
|  |  | ||||||
| import 'package:immich_mobile/shared/models/immich_asset.model.dart'; | import 'package:openapi/api.dart'; | ||||||
|  |  | ||||||
| class AssetSelectionPageResult { | class AssetSelectionPageResult { | ||||||
|   final Set<ImmichAsset> selectedNewAsset; |   final Set<AssetResponseDto> selectedNewAsset; | ||||||
|   final Set<ImmichAsset> selectedAdditionalAsset; |   final Set<AssetResponseDto> selectedAdditionalAsset; | ||||||
|   final bool isAlbumExist; |   final bool isAlbumExist; | ||||||
|  |  | ||||||
|   AssetSelectionPageResult({ |   AssetSelectionPageResult({ | ||||||
| @@ -16,8 +14,8 @@ class AssetSelectionPageResult { | |||||||
|   }); |   }); | ||||||
|  |  | ||||||
|   AssetSelectionPageResult copyWith({ |   AssetSelectionPageResult copyWith({ | ||||||
|     Set<ImmichAsset>? selectedNewAsset, |     Set<AssetResponseDto>? selectedNewAsset, | ||||||
|     Set<ImmichAsset>? selectedAdditionalAsset, |     Set<AssetResponseDto>? selectedAdditionalAsset, | ||||||
|     bool? isAlbumExist, |     bool? isAlbumExist, | ||||||
|   }) { |   }) { | ||||||
|     return AssetSelectionPageResult( |     return AssetSelectionPageResult( | ||||||
| @@ -28,35 +26,6 @@ class AssetSelectionPageResult { | |||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   Map<String, dynamic> toMap() { |  | ||||||
|     final result = <String, dynamic>{}; |  | ||||||
|  |  | ||||||
|     result.addAll( |  | ||||||
|         {'selectedNewAsset': selectedNewAsset.map((x) => x.toMap()).toList()}); |  | ||||||
|     result.addAll({ |  | ||||||
|       'selectedAdditionalAsset': |  | ||||||
|           selectedAdditionalAsset.map((x) => x.toMap()).toList() |  | ||||||
|     }); |  | ||||||
|     result.addAll({'isAlbumExist': isAlbumExist}); |  | ||||||
|  |  | ||||||
|     return result; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   factory AssetSelectionPageResult.fromMap(Map<String, dynamic> map) { |  | ||||||
|     return AssetSelectionPageResult( |  | ||||||
|       selectedNewAsset: Set<ImmichAsset>.from( |  | ||||||
|           map['selectedNewAsset']?.map((x) => ImmichAsset.fromMap(x))), |  | ||||||
|       selectedAdditionalAsset: Set<ImmichAsset>.from( |  | ||||||
|           map['selectedAdditionalAsset']?.map((x) => ImmichAsset.fromMap(x))), |  | ||||||
|       isAlbumExist: map['isAlbumExist'] ?? false, |  | ||||||
|     ); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   String toJson() => json.encode(toMap()); |  | ||||||
|  |  | ||||||
|   factory AssetSelectionPageResult.fromJson(String source) => |  | ||||||
|       AssetSelectionPageResult.fromMap(json.decode(source)); |  | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   String toString() => |   String toString() => | ||||||
|       'AssetSelectionPageResult(selectedNewAsset: $selectedNewAsset, selectedAdditionalAsset: $selectedAdditionalAsset, isAlbumExist: $isAlbumExist)'; |       'AssetSelectionPageResult(selectedNewAsset: $selectedNewAsset, selectedAdditionalAsset: $selectedAdditionalAsset, isAlbumExist: $isAlbumExist)'; | ||||||
|   | |||||||
| @@ -1,14 +1,12 @@ | |||||||
| import 'dart:convert'; |  | ||||||
|  |  | ||||||
| import 'package:collection/collection.dart'; | import 'package:collection/collection.dart'; | ||||||
|  |  | ||||||
| import 'package:immich_mobile/shared/models/immich_asset.model.dart'; | import 'package:openapi/api.dart'; | ||||||
|  |  | ||||||
| class AssetSelectionState { | class AssetSelectionState { | ||||||
|   final Set<String> selectedMonths; |   final Set<String> selectedMonths; | ||||||
|   final Set<ImmichAsset> selectedNewAssetsForAlbum; |   final Set<AssetResponseDto> selectedNewAssetsForAlbum; | ||||||
|   final Set<ImmichAsset> selectedAdditionalAssetsForAlbum; |   final Set<AssetResponseDto> selectedAdditionalAssetsForAlbum; | ||||||
|   final Set<ImmichAsset> selectedAssetsInAlbumViewer; |   final Set<AssetResponseDto> selectedAssetsInAlbumViewer; | ||||||
|   final bool isMultiselectEnable; |   final bool isMultiselectEnable; | ||||||
|  |  | ||||||
|   /// Indicate the asset selection page is navigated from existing album |   /// Indicate the asset selection page is navigated from existing album | ||||||
| @@ -24,9 +22,9 @@ class AssetSelectionState { | |||||||
|  |  | ||||||
|   AssetSelectionState copyWith({ |   AssetSelectionState copyWith({ | ||||||
|     Set<String>? selectedMonths, |     Set<String>? selectedMonths, | ||||||
|     Set<ImmichAsset>? selectedNewAssetsForAlbum, |     Set<AssetResponseDto>? selectedNewAssetsForAlbum, | ||||||
|     Set<ImmichAsset>? selectedAdditionalAssetsForAlbum, |     Set<AssetResponseDto>? selectedAdditionalAssetsForAlbum, | ||||||
|     Set<ImmichAsset>? selectedAssetsInAlbumViewer, |     Set<AssetResponseDto>? selectedAssetsInAlbumViewer, | ||||||
|     bool? isMultiselectEnable, |     bool? isMultiselectEnable, | ||||||
|     bool? isAlbumExist, |     bool? isAlbumExist, | ||||||
|   }) { |   }) { | ||||||
| @@ -43,49 +41,6 @@ class AssetSelectionState { | |||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   Map<String, dynamic> toMap() { |  | ||||||
|     final result = <String, dynamic>{}; |  | ||||||
|  |  | ||||||
|     result.addAll({'selectedMonths': selectedMonths.toList()}); |  | ||||||
|     result.addAll({ |  | ||||||
|       'selectedNewAssetsForAlbum': |  | ||||||
|           selectedNewAssetsForAlbum.map((x) => x.toMap()).toList() |  | ||||||
|     }); |  | ||||||
|     result.addAll({ |  | ||||||
|       'selectedAdditionalAssetsForAlbum': |  | ||||||
|           selectedAdditionalAssetsForAlbum.map((x) => x.toMap()).toList() |  | ||||||
|     }); |  | ||||||
|     result.addAll({ |  | ||||||
|       'selectedAssetsInAlbumViewer': |  | ||||||
|           selectedAssetsInAlbumViewer.map((x) => x.toMap()).toList() |  | ||||||
|     }); |  | ||||||
|     result.addAll({'isMultiselectEnable': isMultiselectEnable}); |  | ||||||
|     result.addAll({'isAlbumExist': isAlbumExist}); |  | ||||||
|  |  | ||||||
|     return result; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   factory AssetSelectionState.fromMap(Map<String, dynamic> map) { |  | ||||||
|     return AssetSelectionState( |  | ||||||
|       selectedMonths: Set<String>.from(map['selectedMonths']), |  | ||||||
|       selectedNewAssetsForAlbum: Set<ImmichAsset>.from( |  | ||||||
|           map['selectedNewAssetsForAlbum']?.map((x) => ImmichAsset.fromMap(x))), |  | ||||||
|       selectedAdditionalAssetsForAlbum: Set<ImmichAsset>.from( |  | ||||||
|           map['selectedAdditionalAssetsForAlbum'] |  | ||||||
|               ?.map((x) => ImmichAsset.fromMap(x))), |  | ||||||
|       selectedAssetsInAlbumViewer: Set<ImmichAsset>.from( |  | ||||||
|           map['selectedAssetsInAlbumViewer'] |  | ||||||
|               ?.map((x) => ImmichAsset.fromMap(x))), |  | ||||||
|       isMultiselectEnable: map['isMultiselectEnable'] ?? false, |  | ||||||
|       isAlbumExist: map['isAlbumExist'] ?? false, |  | ||||||
|     ); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   String toJson() => json.encode(toMap()); |  | ||||||
|  |  | ||||||
|   factory AssetSelectionState.fromJson(String source) => |  | ||||||
|       AssetSelectionState.fromMap(json.decode(source)); |  | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   String toString() { |   String toString() { | ||||||
|     return 'AssetSelectionState(selectedMonths: $selectedMonths, selectedNewAssetsForAlbum: $selectedNewAssetsForAlbum, selectedAdditionalAssetsForAlbum: $selectedAdditionalAssetsForAlbum, selectedAssetsInAlbumViewer: $selectedAssetsInAlbumViewer, isMultiselectEnable: $isMultiselectEnable, isAlbumExist: $isAlbumExist)'; |     return 'AssetSelectionState(selectedMonths: $selectedMonths, selectedNewAssetsForAlbum: $selectedNewAssetsForAlbum, selectedAdditionalAssetsForAlbum: $selectedAdditionalAssetsForAlbum, selectedAssetsInAlbumViewer: $selectedAssetsInAlbumViewer, isMultiselectEnable: $isMultiselectEnable, isAlbumExist: $isAlbumExist)'; | ||||||
| @@ -99,10 +54,14 @@ class AssetSelectionState { | |||||||
|     return other is AssetSelectionState && |     return other is AssetSelectionState && | ||||||
|         setEquals(other.selectedMonths, selectedMonths) && |         setEquals(other.selectedMonths, selectedMonths) && | ||||||
|         setEquals(other.selectedNewAssetsForAlbum, selectedNewAssetsForAlbum) && |         setEquals(other.selectedNewAssetsForAlbum, selectedNewAssetsForAlbum) && | ||||||
|         setEquals(other.selectedAdditionalAssetsForAlbum, |  | ||||||
|             selectedAdditionalAssetsForAlbum) && |  | ||||||
|         setEquals( |         setEquals( | ||||||
|             other.selectedAssetsInAlbumViewer, selectedAssetsInAlbumViewer) && |           other.selectedAdditionalAssetsForAlbum, | ||||||
|  |           selectedAdditionalAssetsForAlbum, | ||||||
|  |         ) && | ||||||
|  |         setEquals( | ||||||
|  |           other.selectedAssetsInAlbumViewer, | ||||||
|  |           selectedAssetsInAlbumViewer, | ||||||
|  |         ) && | ||||||
|         other.isMultiselectEnable == isMultiselectEnable && |         other.isMultiselectEnable == isMultiselectEnable && | ||||||
|         other.isAlbumExist == isAlbumExist; |         other.isAlbumExist == isAlbumExist; | ||||||
|   } |   } | ||||||
|   | |||||||
| @@ -1,117 +0,0 @@ | |||||||
| import 'dart:convert'; |  | ||||||
|  |  | ||||||
| import 'package:collection/collection.dart'; |  | ||||||
|  |  | ||||||
| import 'package:immich_mobile/shared/models/immich_asset.model.dart'; |  | ||||||
| import 'package:immich_mobile/shared/models/user.model.dart'; |  | ||||||
|  |  | ||||||
| class SharedAlbum { |  | ||||||
|   final String id; |  | ||||||
|   final String ownerId; |  | ||||||
|   final String albumName; |  | ||||||
|   final String createdAt; |  | ||||||
|   final String? albumThumbnailAssetId; |  | ||||||
|   final List<User> sharedUsers; |  | ||||||
|   final List<ImmichAsset>? assets; |  | ||||||
|  |  | ||||||
|   SharedAlbum({ |  | ||||||
|     required this.id, |  | ||||||
|     required this.ownerId, |  | ||||||
|     required this.albumName, |  | ||||||
|     required this.createdAt, |  | ||||||
|     required this.albumThumbnailAssetId, |  | ||||||
|     required this.sharedUsers, |  | ||||||
|     this.assets, |  | ||||||
|   }); |  | ||||||
|  |  | ||||||
|   SharedAlbum copyWith({ |  | ||||||
|     String? id, |  | ||||||
|     String? ownerId, |  | ||||||
|     String? albumName, |  | ||||||
|     String? createdAt, |  | ||||||
|     String? albumThumbnailAssetId, |  | ||||||
|     List<User>? sharedUsers, |  | ||||||
|     List<ImmichAsset>? assets, |  | ||||||
|   }) { |  | ||||||
|     return SharedAlbum( |  | ||||||
|       id: id ?? this.id, |  | ||||||
|       ownerId: ownerId ?? this.ownerId, |  | ||||||
|       albumName: albumName ?? this.albumName, |  | ||||||
|       createdAt: createdAt ?? this.createdAt, |  | ||||||
|       albumThumbnailAssetId: |  | ||||||
|           albumThumbnailAssetId ?? this.albumThumbnailAssetId, |  | ||||||
|       sharedUsers: sharedUsers ?? this.sharedUsers, |  | ||||||
|       assets: assets ?? this.assets, |  | ||||||
|     ); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   Map<String, dynamic> toMap() { |  | ||||||
|     final result = <String, dynamic>{}; |  | ||||||
|  |  | ||||||
|     result.addAll({'id': id}); |  | ||||||
|     result.addAll({'ownerId': ownerId}); |  | ||||||
|     result.addAll({'albumName': albumName}); |  | ||||||
|     result.addAll({'createdAt': createdAt}); |  | ||||||
|     if (albumThumbnailAssetId != null) { |  | ||||||
|       result.addAll({'albumThumbnailAssetId': albumThumbnailAssetId}); |  | ||||||
|     } |  | ||||||
|     result.addAll({'sharedUsers': sharedUsers.map((x) => x.toMap()).toList()}); |  | ||||||
|     if (assets != null) { |  | ||||||
|       result.addAll({'assets': assets!.map((x) => x.toMap()).toList()}); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     return result; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   factory SharedAlbum.fromMap(Map<String, dynamic> map) { |  | ||||||
|     return SharedAlbum( |  | ||||||
|       id: map['id'] ?? '', |  | ||||||
|       ownerId: map['ownerId'] ?? '', |  | ||||||
|       albumName: map['albumName'] ?? '', |  | ||||||
|       createdAt: map['createdAt'] ?? '', |  | ||||||
|       albumThumbnailAssetId: map['albumThumbnailAssetId'], |  | ||||||
|       sharedUsers: |  | ||||||
|           List<User>.from(map['sharedUsers']?.map((x) => User.fromMap(x))), |  | ||||||
|       assets: map['assets'] != null |  | ||||||
|           ? List<ImmichAsset>.from( |  | ||||||
|               map['assets']?.map((x) => ImmichAsset.fromMap(x))) |  | ||||||
|           : null, |  | ||||||
|     ); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   String toJson() => json.encode(toMap()); |  | ||||||
|  |  | ||||||
|   factory SharedAlbum.fromJson(String source) => |  | ||||||
|       SharedAlbum.fromMap(json.decode(source)); |  | ||||||
|  |  | ||||||
|   @override |  | ||||||
|   String toString() { |  | ||||||
|     return 'SharedAlbum(id: $id, ownerId: $ownerId, albumName: $albumName, createdAt: $createdAt, albumThumbnailAssetId: $albumThumbnailAssetId, sharedUsers: $sharedUsers, assets: $assets)'; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   @override |  | ||||||
|   bool operator ==(Object other) { |  | ||||||
|     if (identical(this, other)) return true; |  | ||||||
|     final listEquals = const DeepCollectionEquality().equals; |  | ||||||
|  |  | ||||||
|     return other is SharedAlbum && |  | ||||||
|         other.id == id && |  | ||||||
|         other.ownerId == ownerId && |  | ||||||
|         other.albumName == albumName && |  | ||||||
|         other.createdAt == createdAt && |  | ||||||
|         other.albumThumbnailAssetId == albumThumbnailAssetId && |  | ||||||
|         listEquals(other.sharedUsers, sharedUsers) && |  | ||||||
|         listEquals(other.assets, assets); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   @override |  | ||||||
|   int get hashCode { |  | ||||||
|     return id.hashCode ^ |  | ||||||
|         ownerId.hashCode ^ |  | ||||||
|         albumName.hashCode ^ |  | ||||||
|         createdAt.hashCode ^ |  | ||||||
|         albumThumbnailAssetId.hashCode ^ |  | ||||||
|         sharedUsers.hashCode ^ |  | ||||||
|         assets.hashCode; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| @@ -13,4 +13,5 @@ class AlbumTitleNotifier extends StateNotifier<String> { | |||||||
| } | } | ||||||
|  |  | ||||||
| final albumTitleProvider = StateNotifierProvider<AlbumTitleNotifier, String>( | final albumTitleProvider = StateNotifierProvider<AlbumTitleNotifier, String>( | ||||||
|     (ref) => AlbumTitleNotifier()); |   (ref) => AlbumTitleNotifier(), | ||||||
|  | ); | ||||||
|   | |||||||
| @@ -30,7 +30,10 @@ class AlbumViewerNotifier extends StateNotifier<AlbumViewerPageState> { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   Future<bool> changeAlbumTitle( |   Future<bool> changeAlbumTitle( | ||||||
|       String albumId, String ownerId, String newAlbumTitle) async { |     String albumId, | ||||||
|  |     String ownerId, | ||||||
|  |     String newAlbumTitle, | ||||||
|  |   ) async { | ||||||
|     SharedAlbumService service = ref.watch(sharedAlbumServiceProvider); |     SharedAlbumService service = ref.watch(sharedAlbumServiceProvider); | ||||||
|  |  | ||||||
|     bool isSuccess = |     bool isSuccess = | ||||||
|   | |||||||
| @@ -1,41 +1,46 @@ | |||||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||||
| import 'package:immich_mobile/modules/sharing/models/asset_selection_state.model.dart'; | import 'package:immich_mobile/modules/sharing/models/asset_selection_state.model.dart'; | ||||||
|  |  | ||||||
| import 'package:immich_mobile/shared/models/immich_asset.model.dart'; | import 'package:openapi/api.dart'; | ||||||
|  |  | ||||||
| class AssetSelectionNotifier extends StateNotifier<AssetSelectionState> { | class AssetSelectionNotifier extends StateNotifier<AssetSelectionState> { | ||||||
|   AssetSelectionNotifier() |   AssetSelectionNotifier() | ||||||
|       : super(AssetSelectionState( |       : super( | ||||||
|           selectedNewAssetsForAlbum: {}, |           AssetSelectionState( | ||||||
|           selectedMonths: {}, |             selectedNewAssetsForAlbum: {}, | ||||||
|           selectedAdditionalAssetsForAlbum: {}, |             selectedMonths: {}, | ||||||
|           selectedAssetsInAlbumViewer: {}, |             selectedAdditionalAssetsForAlbum: {}, | ||||||
|           isAlbumExist: false, |             selectedAssetsInAlbumViewer: {}, | ||||||
|           isMultiselectEnable: false, |             isAlbumExist: false, | ||||||
|         )); |             isMultiselectEnable: false, | ||||||
|  |           ), | ||||||
|  |         ); | ||||||
|  |  | ||||||
|   void setIsAlbumExist(bool isAlbumExist) { |   void setIsAlbumExist(bool isAlbumExist) { | ||||||
|     state = state.copyWith(isAlbumExist: isAlbumExist); |     state = state.copyWith(isAlbumExist: isAlbumExist); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   void removeAssetsInMonth( |   void removeAssetsInMonth( | ||||||
|       String removedMonth, List<ImmichAsset> assetsInMonth) { |     String removedMonth, | ||||||
|     Set<ImmichAsset> currentAssetList = state.selectedNewAssetsForAlbum; |     List<AssetResponseDto> assetsInMonth, | ||||||
|  |   ) { | ||||||
|  |     Set<AssetResponseDto> currentAssetList = state.selectedNewAssetsForAlbum; | ||||||
|     Set<String> currentMonthList = state.selectedMonths; |     Set<String> currentMonthList = state.selectedMonths; | ||||||
|  |  | ||||||
|     currentMonthList |     currentMonthList | ||||||
|         .removeWhere((selectedMonth) => selectedMonth == removedMonth); |         .removeWhere((selectedMonth) => selectedMonth == removedMonth); | ||||||
|  |  | ||||||
|     for (ImmichAsset asset in assetsInMonth) { |     for (AssetResponseDto asset in assetsInMonth) { | ||||||
|       currentAssetList.removeWhere((e) => e.id == asset.id); |       currentAssetList.removeWhere((e) => e.id == asset.id); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     state = state.copyWith( |     state = state.copyWith( | ||||||
|         selectedNewAssetsForAlbum: currentAssetList, |       selectedNewAssetsForAlbum: currentAssetList, | ||||||
|         selectedMonths: currentMonthList); |       selectedMonths: currentMonthList, | ||||||
|  |     ); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   void addAdditionalAssets(List<ImmichAsset> assets) { |   void addAdditionalAssets(List<AssetResponseDto> assets) { | ||||||
|     state = state.copyWith( |     state = state.copyWith( | ||||||
|       selectedAdditionalAssetsForAlbum: { |       selectedAdditionalAssetsForAlbum: { | ||||||
|         ...state.selectedAdditionalAssetsForAlbum, |         ...state.selectedAdditionalAssetsForAlbum, | ||||||
| @@ -44,7 +49,7 @@ class AssetSelectionNotifier extends StateNotifier<AssetSelectionState> { | |||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   void addAllAssetsInMonth(String month, List<ImmichAsset> assetsInMonth) { |   void addAllAssetsInMonth(String month, List<AssetResponseDto> assetsInMonth) { | ||||||
|     state = state.copyWith( |     state = state.copyWith( | ||||||
|       selectedMonths: {...state.selectedMonths, month}, |       selectedMonths: {...state.selectedMonths, month}, | ||||||
|       selectedNewAssetsForAlbum: { |       selectedNewAssetsForAlbum: { | ||||||
| @@ -54,7 +59,7 @@ class AssetSelectionNotifier extends StateNotifier<AssetSelectionState> { | |||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   void addNewAssets(List<ImmichAsset> assets) { |   void addNewAssets(List<AssetResponseDto> assets) { | ||||||
|     state = state.copyWith( |     state = state.copyWith( | ||||||
|       selectedNewAssetsForAlbum: { |       selectedNewAssetsForAlbum: { | ||||||
|         ...state.selectedNewAssetsForAlbum, |         ...state.selectedNewAssetsForAlbum, | ||||||
| @@ -63,20 +68,20 @@ class AssetSelectionNotifier extends StateNotifier<AssetSelectionState> { | |||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   void removeSelectedNewAssets(List<ImmichAsset> assets) { |   void removeSelectedNewAssets(List<AssetResponseDto> assets) { | ||||||
|     Set<ImmichAsset> currentList = state.selectedNewAssetsForAlbum; |     Set<AssetResponseDto> currentList = state.selectedNewAssetsForAlbum; | ||||||
|  |  | ||||||
|     for (ImmichAsset asset in assets) { |     for (AssetResponseDto asset in assets) { | ||||||
|       currentList.removeWhere((e) => e.id == asset.id); |       currentList.removeWhere((e) => e.id == asset.id); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     state = state.copyWith(selectedNewAssetsForAlbum: currentList); |     state = state.copyWith(selectedNewAssetsForAlbum: currentList); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   void removeSelectedAdditionalAssets(List<ImmichAsset> assets) { |   void removeSelectedAdditionalAssets(List<AssetResponseDto> assets) { | ||||||
|     Set<ImmichAsset> currentList = state.selectedAdditionalAssetsForAlbum; |     Set<AssetResponseDto> currentList = state.selectedAdditionalAssetsForAlbum; | ||||||
|  |  | ||||||
|     for (ImmichAsset asset in assets) { |     for (AssetResponseDto asset in assets) { | ||||||
|       currentList.removeWhere((e) => e.id == asset.id); |       currentList.removeWhere((e) => e.id == asset.id); | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -104,7 +109,7 @@ class AssetSelectionNotifier extends StateNotifier<AssetSelectionState> { | |||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   void addAssetsInAlbumViewer(List<ImmichAsset> assets) { |   void addAssetsInAlbumViewer(List<AssetResponseDto> assets) { | ||||||
|     state = state.copyWith( |     state = state.copyWith( | ||||||
|       selectedAssetsInAlbumViewer: { |       selectedAssetsInAlbumViewer: { | ||||||
|         ...state.selectedAssetsInAlbumViewer, |         ...state.selectedAssetsInAlbumViewer, | ||||||
| @@ -113,10 +118,10 @@ class AssetSelectionNotifier extends StateNotifier<AssetSelectionState> { | |||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   void removeAssetsInAlbumViewer(List<ImmichAsset> assets) { |   void removeAssetsInAlbumViewer(List<AssetResponseDto> assets) { | ||||||
|     Set<ImmichAsset> currentList = state.selectedAssetsInAlbumViewer; |     Set<AssetResponseDto> currentList = state.selectedAssetsInAlbumViewer; | ||||||
|  |  | ||||||
|     for (ImmichAsset asset in assets) { |     for (AssetResponseDto asset in assets) { | ||||||
|       currentList.removeWhere((e) => e.id == asset.id); |       currentList.removeWhere((e) => e.id == asset.id); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,17 +1,19 @@ | |||||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||||
| import 'package:immich_mobile/modules/sharing/models/shared_album.model.dart'; |  | ||||||
| import 'package:immich_mobile/modules/sharing/services/shared_album.service.dart'; | import 'package:immich_mobile/modules/sharing/services/shared_album.service.dart'; | ||||||
|  | import 'package:openapi/api.dart'; | ||||||
|  |  | ||||||
| class SharedAlbumNotifier extends StateNotifier<List<SharedAlbum>> { | class SharedAlbumNotifier extends StateNotifier<List<AlbumResponseDto>> { | ||||||
|   SharedAlbumNotifier(this._sharedAlbumService) : super([]); |   SharedAlbumNotifier(this._sharedAlbumService) : super([]); | ||||||
|  |  | ||||||
|   final SharedAlbumService _sharedAlbumService; |   final SharedAlbumService _sharedAlbumService; | ||||||
|  |  | ||||||
|   getAllSharedAlbums() async { |   getAllSharedAlbums() async { | ||||||
|     List<SharedAlbum> sharedAlbums = |     List<AlbumResponseDto>? sharedAlbums = | ||||||
|         await _sharedAlbumService.getAllSharedAlbum(); |         await _sharedAlbumService.getAllSharedAlbum(); | ||||||
|  |  | ||||||
|     state = sharedAlbums; |     if (sharedAlbums != null) { | ||||||
|  |       state = sharedAlbums; | ||||||
|  |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   Future<bool> deleteAlbum(String albumId) async { |   Future<bool> deleteAlbum(String albumId) async { | ||||||
| @@ -37,7 +39,9 @@ class SharedAlbumNotifier extends StateNotifier<List<SharedAlbum>> { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   Future<bool> removeAssetFromAlbum( |   Future<bool> removeAssetFromAlbum( | ||||||
|       String albumId, List<String> assetIds) async { |     String albumId, | ||||||
|  |     List<String> assetIds, | ||||||
|  |   ) async { | ||||||
|     var res = await _sharedAlbumService.removeAssetFromAlbum(albumId, assetIds); |     var res = await _sharedAlbumService.removeAssetFromAlbum(albumId, assetIds); | ||||||
|  |  | ||||||
|     if (res) { |     if (res) { | ||||||
| @@ -49,12 +53,12 @@ class SharedAlbumNotifier extends StateNotifier<List<SharedAlbum>> { | |||||||
| } | } | ||||||
|  |  | ||||||
| final sharedAlbumProvider = | final sharedAlbumProvider = | ||||||
|     StateNotifierProvider<SharedAlbumNotifier, List<SharedAlbum>>((ref) { |     StateNotifierProvider<SharedAlbumNotifier, List<AlbumResponseDto>>((ref) { | ||||||
|   return SharedAlbumNotifier(ref.watch(sharedAlbumServiceProvider)); |   return SharedAlbumNotifier(ref.watch(sharedAlbumServiceProvider)); | ||||||
| }); | }); | ||||||
|  |  | ||||||
| final sharedAlbumDetailProvider = FutureProvider.autoDispose | final sharedAlbumDetailProvider = FutureProvider.autoDispose | ||||||
|     .family<SharedAlbum, String>((ref, albumId) async { |     .family<AlbumResponseDto?, String>((ref, albumId) async { | ||||||
|   final SharedAlbumService sharedAlbumService = |   final SharedAlbumService sharedAlbumService = | ||||||
|       ref.watch(sharedAlbumServiceProvider); |       ref.watch(sharedAlbumServiceProvider); | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,10 +1,10 @@ | |||||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||||
| import 'package:immich_mobile/shared/models/user.model.dart'; |  | ||||||
| import 'package:immich_mobile/shared/services/user.service.dart'; | import 'package:immich_mobile/shared/services/user.service.dart'; | ||||||
|  | import 'package:openapi/api.dart'; | ||||||
|  |  | ||||||
| final suggestedSharedUsersProvider = | final suggestedSharedUsersProvider = | ||||||
|     FutureProvider.autoDispose<List<User>>((ref) async { |     FutureProvider.autoDispose<List<UserResponseDto>>((ref) async { | ||||||
|   UserService userService = ref.watch(userServiceProvider); |   UserService userService = ref.watch(userServiceProvider); | ||||||
|  |  | ||||||
|   return await userService.getAllUsersInfo(); |   return await userService.getAllUsersInfo(isAll: false) ?? []; | ||||||
| }); | }); | ||||||
|   | |||||||
| @@ -1,73 +1,69 @@ | |||||||
| import 'dart:async'; | import 'dart:async'; | ||||||
| import 'dart:convert'; |  | ||||||
|  |  | ||||||
| import 'package:dio/dio.dart'; |  | ||||||
| import 'package:flutter/foundation.dart'; | import 'package:flutter/foundation.dart'; | ||||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||||
| import 'package:immich_mobile/modules/sharing/models/shared_album.model.dart'; | import 'package:immich_mobile/shared/services/api.service.dart'; | ||||||
| import 'package:immich_mobile/shared/models/immich_asset.model.dart'; | import 'package:openapi/api.dart'; | ||||||
| import 'package:immich_mobile/shared/services/network.service.dart'; |  | ||||||
|  |  | ||||||
| final sharedAlbumServiceProvider = | final sharedAlbumServiceProvider = Provider( | ||||||
|     Provider((ref) => SharedAlbumService(ref.watch(networkServiceProvider))); |   (ref) => SharedAlbumService( | ||||||
|  |     ref.watch(apiServiceProvider), | ||||||
|  |   ), | ||||||
|  | ); | ||||||
|  |  | ||||||
| class SharedAlbumService { | class SharedAlbumService { | ||||||
|   final NetworkService _networkService; |   final ApiService _apiService; | ||||||
|   SharedAlbumService(this._networkService); |   SharedAlbumService(this._apiService); | ||||||
|  |  | ||||||
|   Future<List<SharedAlbum>> getAllSharedAlbum() async { |   Future<List<AlbumResponseDto>?> getAllSharedAlbum() async { | ||||||
|     try { |     try { | ||||||
|       var res = await _networkService.getRequest(url: 'album?shared=true'); |       return await _apiService.albumApi.getAllAlbums(shared: true); | ||||||
|       List<dynamic> decodedData = jsonDecode(res.toString()); |  | ||||||
|       List<SharedAlbum> result = |  | ||||||
|           List.from(decodedData.map((e) => SharedAlbum.fromMap(e))); |  | ||||||
|  |  | ||||||
|       return result; |  | ||||||
|     } catch (e) { |     } catch (e) { | ||||||
|       debugPrint("Error getAllSharedAlbum  ${e.toString()}"); |       debugPrint("Error getAllSharedAlbum  ${e.toString()}"); | ||||||
|  |       return null; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     return []; |  | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   Future<bool> createSharedAlbum(String albumName, Set<ImmichAsset> assets, |   Future<bool> createSharedAlbum( | ||||||
|       List<String> sharedUserIds) async { |     String albumName, | ||||||
|  |     Set<AssetResponseDto> assets, | ||||||
|  |     List<String> sharedUserIds, | ||||||
|  |   ) async { | ||||||
|     try { |     try { | ||||||
|       var res = await _networkService.postRequest(url: 'album', data: { |       _apiService.albumApi.createAlbum( | ||||||
|         "albumName": albumName, |         CreateAlbumDto( | ||||||
|         "sharedWithUserIds": sharedUserIds, |           albumName: albumName, | ||||||
|         "assetIds": assets.map((asset) => asset.id).toList(), |           assetIds: assets.map((asset) => asset.id).toList(), | ||||||
|       }); |           sharedWithUserIds: sharedUserIds, | ||||||
|  |         ), | ||||||
|  |       ); | ||||||
|  |  | ||||||
|       return res != null; |       return true; | ||||||
|     } catch (e) { |     } catch (e) { | ||||||
|       debugPrint("Error createSharedAlbum  ${e.toString()}"); |       debugPrint("Error createSharedAlbum  ${e.toString()}"); | ||||||
|       return false; |       return false; | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   Future<SharedAlbum> getAlbumDetail(String albumId) async { |   Future<AlbumResponseDto?> getAlbumDetail(String albumId) async { | ||||||
|     try { |     try { | ||||||
|       var res = await _networkService.getRequest(url: 'album/$albumId'); |       return await _apiService.albumApi.getAlbumInfo(albumId); | ||||||
|       dynamic decodedData = jsonDecode(res.toString()); |  | ||||||
|       SharedAlbum result = SharedAlbum.fromMap(decodedData); |  | ||||||
|  |  | ||||||
|       return result; |  | ||||||
|     } catch (e) { |     } catch (e) { | ||||||
|       throw Exception('Error getAllSharedAlbum  ${e.toString()}'); |       debugPrint('Error [getAlbumDetail] ${e.toString()}'); | ||||||
|  |       return null; | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   Future<bool> addAdditionalAssetToAlbum( |   Future<bool> addAdditionalAssetToAlbum( | ||||||
|       Set<ImmichAsset> assets, String albumId) async { |     Set<AssetResponseDto> assets, | ||||||
|  |     String albumId, | ||||||
|  |   ) async { | ||||||
|     try { |     try { | ||||||
|       var res = |       var result = await _apiService.albumApi.addAssetsToAlbum( | ||||||
|           await _networkService.putRequest(url: 'album/$albumId/assets', data: { |         albumId, | ||||||
|         "albumId": albumId, |         AddAssetsDto(assetIds: assets.map((asset) => asset.id).toList()), | ||||||
|         "assetIds": assets.map((asset) => asset.id).toList(), |       ); | ||||||
|       }); |       return result != null; | ||||||
|  |  | ||||||
|       return res != null; |  | ||||||
|     } catch (e) { |     } catch (e) { | ||||||
|       debugPrint("Error addAdditionalAssetToAlbum  ${e.toString()}"); |       debugPrint("Error addAdditionalAssetToAlbum  ${e.toString()}"); | ||||||
|       return false; |       return false; | ||||||
| @@ -75,14 +71,16 @@ class SharedAlbumService { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   Future<bool> addAdditionalUserToAlbum( |   Future<bool> addAdditionalUserToAlbum( | ||||||
|       List<String> sharedUserIds, String albumId) async { |     List<String> sharedUserIds, | ||||||
|  |     String albumId, | ||||||
|  |   ) async { | ||||||
|     try { |     try { | ||||||
|       var res = |       var result = await _apiService.albumApi.addUsersToAlbum( | ||||||
|           await _networkService.putRequest(url: 'album/$albumId/users', data: { |         albumId, | ||||||
|         "sharedUserIds": sharedUserIds, |         AddUsersDto(sharedUserIds: sharedUserIds), | ||||||
|       }); |       ); | ||||||
|  |  | ||||||
|       return res != null; |       return result != null; | ||||||
|     } catch (e) { |     } catch (e) { | ||||||
|       debugPrint("Error addAdditionalUserToAlbum  ${e.toString()}"); |       debugPrint("Error addAdditionalUserToAlbum  ${e.toString()}"); | ||||||
|       return false; |       return false; | ||||||
| @@ -91,12 +89,7 @@ class SharedAlbumService { | |||||||
|  |  | ||||||
|   Future<bool> deleteAlbum(String albumId) async { |   Future<bool> deleteAlbum(String albumId) async { | ||||||
|     try { |     try { | ||||||
|       Response res = await _networkService.deleteRequest(url: 'album/$albumId'); |       await _apiService.albumApi.deleteAlbum(albumId); | ||||||
|  |  | ||||||
|       if (res.statusCode != 200) { |  | ||||||
|         return false; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       return true; |       return true; | ||||||
|     } catch (e) { |     } catch (e) { | ||||||
|       debugPrint("Error deleteAlbum  ${e.toString()}"); |       debugPrint("Error deleteAlbum  ${e.toString()}"); | ||||||
| @@ -106,12 +99,7 @@ class SharedAlbumService { | |||||||
|  |  | ||||||
|   Future<bool> leaveAlbum(String albumId) async { |   Future<bool> leaveAlbum(String albumId) async { | ||||||
|     try { |     try { | ||||||
|       Response res = |       await _apiService.albumApi.removeUserFromAlbum(albumId, "me"); | ||||||
|           await _networkService.deleteRequest(url: 'album/$albumId/user/me'); |  | ||||||
|  |  | ||||||
|       if (res.statusCode != 200) { |  | ||||||
|         return false; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       return true; |       return true; | ||||||
|     } catch (e) { |     } catch (e) { | ||||||
| @@ -121,16 +109,14 @@ class SharedAlbumService { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   Future<bool> removeAssetFromAlbum( |   Future<bool> removeAssetFromAlbum( | ||||||
|       String albumId, List<String> assetIds) async { |     String albumId, | ||||||
|  |     List<String> assetIds, | ||||||
|  |   ) async { | ||||||
|     try { |     try { | ||||||
|       Response res = await _networkService |       await _apiService.albumApi.removeAssetFromAlbum( | ||||||
|           .deleteRequest(url: 'album/$albumId/assets', data: { |         albumId, | ||||||
|         "assetIds": assetIds, |         RemoveAssetsDto(assetIds: assetIds), | ||||||
|       }); |       ); | ||||||
|  |  | ||||||
|       if (res.statusCode != 200) { |  | ||||||
|         return false; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       return true; |       return true; | ||||||
|     } catch (e) { |     } catch (e) { | ||||||
| @@ -140,17 +126,18 @@ class SharedAlbumService { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   Future<bool> changeTitleAlbum( |   Future<bool> changeTitleAlbum( | ||||||
|       String albumId, String ownerId, String newAlbumTitle) async { |     String albumId, | ||||||
|  |     String ownerId, | ||||||
|  |     String newAlbumTitle, | ||||||
|  |   ) async { | ||||||
|     try { |     try { | ||||||
|       Response res = |       await _apiService.albumApi.updateAlbumInfo( | ||||||
|           await _networkService.patchRequest(url: 'album/$albumId/', data: { |         albumId, | ||||||
|         "ownerId": ownerId, |         UpdateAlbumDto( | ||||||
|         "albumName": newAlbumTitle, |           ownerId: ownerId, | ||||||
|       }); |           albumName: newAlbumTitle, | ||||||
|  |         ), | ||||||
|       if (res.statusCode != 200) { |       ); | ||||||
|         return false; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       return true; |       return true; | ||||||
|     } catch (e) { |     } catch (e) { | ||||||
|   | |||||||
| @@ -5,12 +5,12 @@ class AlbumActionOutlinedButton extends StatelessWidget { | |||||||
|   final String labelText; |   final String labelText; | ||||||
|   final IconData iconData; |   final IconData iconData; | ||||||
|  |  | ||||||
|   const AlbumActionOutlinedButton( |   const AlbumActionOutlinedButton({ | ||||||
|       {Key? key, |     Key? key, | ||||||
|       this.onPressed, |     this.onPressed, | ||||||
|       required this.labelText, |     required this.labelText, | ||||||
|       required this.iconData}) |     required this.iconData, | ||||||
|       : super(key: key); |   }) : super(key: key); | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   Widget build(BuildContext context) { |   Widget build(BuildContext context) { | ||||||
| @@ -31,7 +31,10 @@ class AlbumActionOutlinedButton extends StatelessWidget { | |||||||
|         label: Text( |         label: Text( | ||||||
|           labelText, |           labelText, | ||||||
|           style: const TextStyle( |           style: const TextStyle( | ||||||
|               fontSize: 12, fontWeight: FontWeight.bold, color: Colors.black87), |             fontSize: 12, | ||||||
|  |             fontWeight: FontWeight.bold, | ||||||
|  |             color: Colors.black87, | ||||||
|  |           ), | ||||||
|         ), |         ), | ||||||
|         onPressed: onPressed, |         onPressed: onPressed, | ||||||
|       ), |       ), | ||||||
|   | |||||||
| @@ -31,7 +31,10 @@ class AlbumTitleTextField extends ConsumerWidget { | |||||||
|       }, |       }, | ||||||
|       focusNode: albumTitleTextFieldFocusNode, |       focusNode: albumTitleTextFieldFocusNode, | ||||||
|       style: TextStyle( |       style: TextStyle( | ||||||
|           fontSize: 28, color: Colors.grey[700], fontWeight: FontWeight.bold), |         fontSize: 28, | ||||||
|  |         color: Colors.grey[700], | ||||||
|  |         fontWeight: FontWeight.bold, | ||||||
|  |       ), | ||||||
|       controller: albumTitleController, |       controller: albumTitleController, | ||||||
|       onTap: () { |       onTap: () { | ||||||
|         isAlbumTitleTextFieldFocus.value = true; |         isAlbumTitleTextFieldFocus.value = true; | ||||||
|   | |||||||
| @@ -4,24 +4,24 @@ import 'package:flutter/material.dart'; | |||||||
| import 'package:fluttertoast/fluttertoast.dart'; | import 'package:fluttertoast/fluttertoast.dart'; | ||||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||||
| import 'package:immich_mobile/constants/immich_colors.dart'; | import 'package:immich_mobile/constants/immich_colors.dart'; | ||||||
| import 'package:immich_mobile/modules/sharing/models/shared_album.model.dart'; |  | ||||||
| import 'package:immich_mobile/modules/sharing/providers/album_viewer.provider.dart'; | import 'package:immich_mobile/modules/sharing/providers/album_viewer.provider.dart'; | ||||||
| import 'package:immich_mobile/modules/sharing/providers/asset_selection.provider.dart'; | import 'package:immich_mobile/modules/sharing/providers/asset_selection.provider.dart'; | ||||||
| import 'package:immich_mobile/modules/sharing/providers/shared_album.provider.dart'; | import 'package:immich_mobile/modules/sharing/providers/shared_album.provider.dart'; | ||||||
| import 'package:immich_mobile/routing/router.dart'; | import 'package:immich_mobile/routing/router.dart'; | ||||||
| import 'package:immich_mobile/shared/ui/immich_toast.dart'; | import 'package:immich_mobile/shared/ui/immich_toast.dart'; | ||||||
| import 'package:immich_mobile/shared/views/immich_loading_overlay.dart'; | import 'package:immich_mobile/shared/views/immich_loading_overlay.dart'; | ||||||
|  | import 'package:openapi/api.dart'; | ||||||
|  |  | ||||||
| class AlbumViewerAppbar extends HookConsumerWidget with PreferredSizeWidget { | class AlbumViewerAppbar extends HookConsumerWidget with PreferredSizeWidget { | ||||||
|   const AlbumViewerAppbar({ |   const AlbumViewerAppbar({ | ||||||
|     Key? key, |     Key? key, | ||||||
|     required AsyncValue<SharedAlbum> albumInfo, |     required AsyncValue<AlbumResponseDto?> albumInfo, | ||||||
|     required this.userId, |     required this.userId, | ||||||
|     required this.albumId, |     required this.albumId, | ||||||
|   })  : _albumInfo = albumInfo, |   })  : _albumInfo = albumInfo, | ||||||
|         super(key: key); |         super(key: key); | ||||||
|  |  | ||||||
|   final AsyncValue<SharedAlbum> _albumInfo; |   final AsyncValue<AlbumResponseDto?> _albumInfo; | ||||||
|   final String userId; |   final String userId; | ||||||
|   final String albumId; |   final String albumId; | ||||||
|  |  | ||||||
| @@ -105,7 +105,7 @@ class AlbumViewerAppbar extends HookConsumerWidget with PreferredSizeWidget { | |||||||
|  |  | ||||||
|     _buildBottomSheetActionButton() { |     _buildBottomSheetActionButton() { | ||||||
|       if (isMultiSelectionEnable) { |       if (isMultiSelectionEnable) { | ||||||
|         if (_albumInfo.asData?.value.ownerId == userId) { |         if (_albumInfo.asData?.value?.ownerId == userId) { | ||||||
|           return ListTile( |           return ListTile( | ||||||
|             leading: const Icon(Icons.delete_sweep_rounded), |             leading: const Icon(Icons.delete_sweep_rounded), | ||||||
|             title: const Text( |             title: const Text( | ||||||
| @@ -118,7 +118,7 @@ class AlbumViewerAppbar extends HookConsumerWidget with PreferredSizeWidget { | |||||||
|           return const SizedBox(); |           return const SizedBox(); | ||||||
|         } |         } | ||||||
|       } else { |       } else { | ||||||
|         if (_albumInfo.asData?.value.ownerId == userId) { |         if (_albumInfo.asData?.value?.ownerId == userId) { | ||||||
|           return ListTile( |           return ListTile( | ||||||
|             leading: const Icon(Icons.delete_forever_rounded), |             leading: const Icon(Icons.delete_forever_rounded), | ||||||
|             title: const Text( |             title: const Text( | ||||||
|   | |||||||
| @@ -2,15 +2,17 @@ import 'package:easy_localization/easy_localization.dart'; | |||||||
| import 'package:flutter/material.dart'; | import 'package:flutter/material.dart'; | ||||||
| import 'package:flutter_hooks/flutter_hooks.dart'; | import 'package:flutter_hooks/flutter_hooks.dart'; | ||||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||||
| import 'package:immich_mobile/modules/sharing/models/shared_album.model.dart'; |  | ||||||
| import 'package:immich_mobile/modules/sharing/providers/album_viewer.provider.dart'; | import 'package:immich_mobile/modules/sharing/providers/album_viewer.provider.dart'; | ||||||
|  | import 'package:openapi/api.dart'; | ||||||
|  |  | ||||||
| class AlbumViewerEditableTitle extends HookConsumerWidget { | class AlbumViewerEditableTitle extends HookConsumerWidget { | ||||||
|   final SharedAlbum albumInfo; |   final AlbumResponseDto albumInfo; | ||||||
|   final FocusNode titleFocusNode; |   final FocusNode titleFocusNode; | ||||||
|   const AlbumViewerEditableTitle( |   const AlbumViewerEditableTitle({ | ||||||
|       {Key? key, required this.albumInfo, required this.titleFocusNode}) |     Key? key, | ||||||
|       : super(key: key); |     required this.albumInfo, | ||||||
|  |     required this.titleFocusNode, | ||||||
|  |   }) : super(key: key); | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   Widget build(BuildContext context, WidgetRef ref) { |   Widget build(BuildContext context, WidgetRef ref) { | ||||||
| @@ -24,12 +26,15 @@ class AlbumViewerEditableTitle extends HookConsumerWidget { | |||||||
|       } |       } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     useEffect(() { |     useEffect( | ||||||
|       titleFocusNode.addListener(onFocusModeChange); |       () { | ||||||
|       return () { |         titleFocusNode.addListener(onFocusModeChange); | ||||||
|         titleFocusNode.removeListener(onFocusModeChange); |         return () { | ||||||
|       }; |           titleFocusNode.removeListener(onFocusModeChange); | ||||||
|     }, []); |         }; | ||||||
|  |       }, | ||||||
|  |       [], | ||||||
|  |     ); | ||||||
|  |  | ||||||
|     return TextField( |     return TextField( | ||||||
|       onChanged: (value) { |       onChanged: (value) { | ||||||
|   | |||||||
| @@ -7,11 +7,11 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; | |||||||
| import 'package:immich_mobile/constants/hive_box.dart'; | import 'package:immich_mobile/constants/hive_box.dart'; | ||||||
| import 'package:immich_mobile/modules/login/providers/authentication.provider.dart'; | import 'package:immich_mobile/modules/login/providers/authentication.provider.dart'; | ||||||
| import 'package:immich_mobile/modules/sharing/providers/asset_selection.provider.dart'; | import 'package:immich_mobile/modules/sharing/providers/asset_selection.provider.dart'; | ||||||
| import 'package:immich_mobile/shared/models/immich_asset.model.dart'; |  | ||||||
| import 'package:immich_mobile/routing/router.dart'; | import 'package:immich_mobile/routing/router.dart'; | ||||||
|  | import 'package:openapi/api.dart'; | ||||||
|  |  | ||||||
| class AlbumViewerThumbnail extends HookConsumerWidget { | class AlbumViewerThumbnail extends HookConsumerWidget { | ||||||
|   final ImmichAsset asset; |   final AssetResponseDto asset; | ||||||
|  |  | ||||||
|   const AlbumViewerThumbnail({Key? key, required this.asset}) : super(key: key); |   const AlbumViewerThumbnail({Key? key, required this.asset}) : super(key: key); | ||||||
|  |  | ||||||
| @@ -20,7 +20,7 @@ class AlbumViewerThumbnail extends HookConsumerWidget { | |||||||
|     final cacheKey = useState(1); |     final cacheKey = useState(1); | ||||||
|     var box = Hive.box(userInfoBox); |     var box = Hive.box(userInfoBox); | ||||||
|     var thumbnailRequestUrl = |     var thumbnailRequestUrl = | ||||||
|         '${box.get(serverEndpointKey)}/asset/file?aid=${asset.deviceAssetId}&did=${asset.deviceId}&isThumb=true'; |         '${box.get(serverEndpointKey)}/asset/thumbnail/${asset.id}'; | ||||||
|     var deviceId = ref.watch(authenticationProvider).deviceId; |     var deviceId = ref.watch(authenticationProvider).deviceId; | ||||||
|     final selectedAssetsInAlbumViewer = |     final selectedAssetsInAlbumViewer = | ||||||
|         ref.watch(assetSelectionProvider).selectedAssetsInAlbumViewer; |         ref.watch(assetSelectionProvider).selectedAssetsInAlbumViewer; | ||||||
| @@ -28,7 +28,7 @@ class AlbumViewerThumbnail extends HookConsumerWidget { | |||||||
|         ref.watch(assetSelectionProvider).isMultiselectEnable; |         ref.watch(assetSelectionProvider).isMultiselectEnable; | ||||||
|  |  | ||||||
|     _viewAsset() { |     _viewAsset() { | ||||||
|       if (asset.type == 'IMAGE') { |       if (asset.type == AssetTypeEnum.IMAGE) { | ||||||
|         AutoRouter.of(context).push( |         AutoRouter.of(context).push( | ||||||
|           ImageViewerRoute( |           ImageViewerRoute( | ||||||
|             imageUrl: |             imageUrl: | ||||||
| @@ -41,9 +41,10 @@ class AlbumViewerThumbnail extends HookConsumerWidget { | |||||||
|       } else { |       } else { | ||||||
|         AutoRouter.of(context).push( |         AutoRouter.of(context).push( | ||||||
|           VideoViewerRoute( |           VideoViewerRoute( | ||||||
|               videoUrl: |             videoUrl: | ||||||
|                   '${box.get(serverEndpointKey)}/asset/file?aid=${asset.deviceAssetId}&did=${asset.deviceId}', |                 '${box.get(serverEndpointKey)}/asset/file?aid=${asset.deviceAssetId}&did=${asset.deviceId}', | ||||||
|               asset: asset), |             asset: asset, | ||||||
|  |           ), | ||||||
|         ); |         ); | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
| @@ -170,16 +171,13 @@ class AlbumViewerThumbnail extends HookConsumerWidget { | |||||||
|     return GestureDetector( |     return GestureDetector( | ||||||
|       onTap: isMultiSelectionEnable ? _handleSelectionGesture : _viewAsset, |       onTap: isMultiSelectionEnable ? _handleSelectionGesture : _viewAsset, | ||||||
|       onLongPress: _enableMultiSelection, |       onLongPress: _enableMultiSelection, | ||||||
|       child: Hero( |       child: Stack( | ||||||
|         tag: asset.id, |         children: [ | ||||||
|         child: Stack( |           _buildThumbnailImage(), | ||||||
|           children: [ |           _buildAssetStoreLocationIcon(), | ||||||
|             _buildThumbnailImage(), |           if (asset.type != AssetTypeEnum.IMAGE) _buildVideoLabel(), | ||||||
|             _buildAssetStoreLocationIcon(), |           if (isMultiSelectionEnable) _buildAssetSelectionIcon(), | ||||||
|             if (asset.type != 'IMAGE') _buildVideoLabel(), |         ], | ||||||
|             if (isMultiSelectionEnable) _buildAssetSelectionIcon(), |  | ||||||
|           ], |  | ||||||
|         ), |  | ||||||
|       ), |       ), | ||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
|   | |||||||
| @@ -1,10 +1,10 @@ | |||||||
| import 'package:flutter/material.dart'; | import 'package:flutter/material.dart'; | ||||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||||
| import 'package:immich_mobile/modules/sharing/ui/selection_thumbnail_image.dart'; | import 'package:immich_mobile/modules/sharing/ui/selection_thumbnail_image.dart'; | ||||||
| import 'package:immich_mobile/shared/models/immich_asset.model.dart'; | import 'package:openapi/api.dart'; | ||||||
|  |  | ||||||
| class AssetGridByMonth extends HookConsumerWidget { | class AssetGridByMonth extends HookConsumerWidget { | ||||||
|   final List<ImmichAsset> assetGroup; |   final List<AssetResponseDto> assetGroup; | ||||||
|   const AssetGridByMonth({Key? key, required this.assetGroup}) |   const AssetGridByMonth({Key? key, required this.assetGroup}) | ||||||
|       : super(key: key); |       : super(key: key); | ||||||
|   @override |   @override | ||||||
|   | |||||||
| @@ -2,15 +2,17 @@ import 'package:flutter/material.dart'; | |||||||
| import 'package:flutter/services.dart'; | import 'package:flutter/services.dart'; | ||||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||||
| import 'package:immich_mobile/modules/sharing/providers/asset_selection.provider.dart'; | import 'package:immich_mobile/modules/sharing/providers/asset_selection.provider.dart'; | ||||||
| import 'package:immich_mobile/shared/models/immich_asset.model.dart'; | import 'package:openapi/api.dart'; | ||||||
|  |  | ||||||
| class MonthGroupTitle extends HookConsumerWidget { | class MonthGroupTitle extends HookConsumerWidget { | ||||||
|   final String month; |   final String month; | ||||||
|   final List<ImmichAsset> assetGroup; |   final List<AssetResponseDto> assetGroup; | ||||||
|  |  | ||||||
|   const MonthGroupTitle( |   const MonthGroupTitle({ | ||||||
|       {Key? key, required this.month, required this.assetGroup}) |     Key? key, | ||||||
|       : super(key: key); |     required this.month, | ||||||
|  |     required this.assetGroup, | ||||||
|  |   }) : super(key: key); | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   Widget build(BuildContext context, WidgetRef ref) { |   Widget build(BuildContext context, WidgetRef ref) { | ||||||
| @@ -75,7 +77,11 @@ class MonthGroupTitle extends HookConsumerWidget { | |||||||
|     return SliverToBoxAdapter( |     return SliverToBoxAdapter( | ||||||
|       child: Padding( |       child: Padding( | ||||||
|         padding: const EdgeInsets.only( |         padding: const EdgeInsets.only( | ||||||
|             top: 29.0, bottom: 29.0, left: 14.0, right: 8.0), |           top: 29.0, | ||||||
|  |           bottom: 29.0, | ||||||
|  |           left: 14.0, | ||||||
|  |           right: 8.0, | ||||||
|  |         ), | ||||||
|         child: Row( |         child: Row( | ||||||
|           children: [ |           children: [ | ||||||
|             GestureDetector( |             GestureDetector( | ||||||
|   | |||||||
| @@ -5,10 +5,10 @@ import 'package:hive_flutter/hive_flutter.dart'; | |||||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||||
| import 'package:immich_mobile/constants/hive_box.dart'; | import 'package:immich_mobile/constants/hive_box.dart'; | ||||||
| import 'package:immich_mobile/modules/sharing/providers/asset_selection.provider.dart'; | import 'package:immich_mobile/modules/sharing/providers/asset_selection.provider.dart'; | ||||||
| import 'package:immich_mobile/shared/models/immich_asset.model.dart'; | import 'package:openapi/api.dart'; | ||||||
|  |  | ||||||
| class SelectionThumbnailImage extends HookConsumerWidget { | class SelectionThumbnailImage extends HookConsumerWidget { | ||||||
|   final ImmichAsset asset; |   final AssetResponseDto asset; | ||||||
|  |  | ||||||
|   const SelectionThumbnailImage({Key? key, required this.asset}) |   const SelectionThumbnailImage({Key? key, required this.asset}) | ||||||
|       : super(key: key); |       : super(key: key); | ||||||
| @@ -18,14 +18,14 @@ class SelectionThumbnailImage extends HookConsumerWidget { | |||||||
|     final cacheKey = useState(1); |     final cacheKey = useState(1); | ||||||
|     var box = Hive.box(userInfoBox); |     var box = Hive.box(userInfoBox); | ||||||
|     var thumbnailRequestUrl = |     var thumbnailRequestUrl = | ||||||
|         '${box.get(serverEndpointKey)}/asset/file?aid=${asset.deviceAssetId}&did=${asset.deviceId}&isThumb=true'; |         '${box.get(serverEndpointKey)}/asset/thumbnail/${asset.id}'; | ||||||
|     var selectedAsset = |     var selectedAsset = | ||||||
|         ref.watch(assetSelectionProvider).selectedNewAssetsForAlbum; |         ref.watch(assetSelectionProvider).selectedNewAssetsForAlbum; | ||||||
|     var newAssetsForAlbum = |     var newAssetsForAlbum = | ||||||
|         ref.watch(assetSelectionProvider).selectedAdditionalAssetsForAlbum; |         ref.watch(assetSelectionProvider).selectedAdditionalAssetsForAlbum; | ||||||
|     var isAlbumExist = ref.watch(assetSelectionProvider).isAlbumExist; |     var isAlbumExist = ref.watch(assetSelectionProvider).isAlbumExist; | ||||||
|  |  | ||||||
|     Widget _buildSelectionIcon(ImmichAsset asset) { |     Widget _buildSelectionIcon(AssetResponseDto asset) { | ||||||
|       if (selectedAsset.contains(asset) && !isAlbumExist) { |       if (selectedAsset.contains(asset) && !isAlbumExist) { | ||||||
|         return Icon( |         return Icon( | ||||||
|           Icons.check_circle, |           Icons.check_circle, | ||||||
| @@ -103,7 +103,7 @@ class SelectionThumbnailImage extends HookConsumerWidget { | |||||||
|               cacheKey: "${asset.id}-${cacheKey.value}", |               cacheKey: "${asset.id}-${cacheKey.value}", | ||||||
|               width: 150, |               width: 150, | ||||||
|               height: 150, |               height: 150, | ||||||
|               memCacheHeight: asset.type == 'IMAGE' ? 150 : 150, |               memCacheHeight: asset.type == AssetTypeEnum.IMAGE ? 150 : 150, | ||||||
|               fit: BoxFit.cover, |               fit: BoxFit.cover, | ||||||
|               imageUrl: thumbnailRequestUrl, |               imageUrl: thumbnailRequestUrl, | ||||||
|               httpHeaders: { |               httpHeaders: { | ||||||
| @@ -131,14 +131,14 @@ class SelectionThumbnailImage extends HookConsumerWidget { | |||||||
|               child: _buildSelectionIcon(asset), |               child: _buildSelectionIcon(asset), | ||||||
|             ), |             ), | ||||||
|           ), |           ), | ||||||
|           if (asset.type != 'IMAGE') |           if (asset.type != AssetTypeEnum.IMAGE) | ||||||
|             Positioned( |             Positioned( | ||||||
|               bottom: 5, |               bottom: 5, | ||||||
|               right: 5, |               right: 5, | ||||||
|               child: Row( |               child: Row( | ||||||
|                 children: [ |                 children: [ | ||||||
|                   Text( |                   Text( | ||||||
|                     '${asset.duration?.substring(0, 7)}', |                     asset.duration.substring(0, 7), | ||||||
|                     style: const TextStyle( |                     style: const TextStyle( | ||||||
|                       color: Colors.white, |                       color: Colors.white, | ||||||
|                       fontSize: 10, |                       fontSize: 10, | ||||||
|   | |||||||
| @@ -4,10 +4,10 @@ import 'package:flutter_hooks/flutter_hooks.dart'; | |||||||
| import 'package:hive_flutter/hive_flutter.dart'; | import 'package:hive_flutter/hive_flutter.dart'; | ||||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||||
| import 'package:immich_mobile/constants/hive_box.dart'; | import 'package:immich_mobile/constants/hive_box.dart'; | ||||||
| import 'package:immich_mobile/shared/models/immich_asset.model.dart'; | import 'package:openapi/api.dart'; | ||||||
|  |  | ||||||
| class SharedAlbumThumbnailImage extends HookConsumerWidget { | class SharedAlbumThumbnailImage extends HookConsumerWidget { | ||||||
|   final ImmichAsset asset; |   final AssetResponseDto asset; | ||||||
|  |  | ||||||
|   const SharedAlbumThumbnailImage({Key? key, required this.asset}) |   const SharedAlbumThumbnailImage({Key? key, required this.asset}) | ||||||
|       : super(key: key); |       : super(key: key); | ||||||
| @@ -18,7 +18,7 @@ class SharedAlbumThumbnailImage extends HookConsumerWidget { | |||||||
|  |  | ||||||
|     var box = Hive.box(userInfoBox); |     var box = Hive.box(userInfoBox); | ||||||
|     var thumbnailRequestUrl = |     var thumbnailRequestUrl = | ||||||
|         '${box.get(serverEndpointKey)}/asset/file?aid=${asset.deviceAssetId}&did=${asset.deviceId}&isThumb=true'; |         '${box.get(serverEndpointKey)}/asset/thumbnail/${asset.id}'; | ||||||
|  |  | ||||||
|     return GestureDetector( |     return GestureDetector( | ||||||
|       onTap: () { |       onTap: () { | ||||||
| @@ -30,7 +30,7 @@ class SharedAlbumThumbnailImage extends HookConsumerWidget { | |||||||
|             cacheKey: "${asset.id}-${cacheKey.value}", |             cacheKey: "${asset.id}-${cacheKey.value}", | ||||||
|             width: 500, |             width: 500, | ||||||
|             height: 500, |             height: 500, | ||||||
|             memCacheHeight: asset.type == 'IMAGE' ? 500 : 500, |             memCacheHeight: 500, | ||||||
|             fit: BoxFit.cover, |             fit: BoxFit.cover, | ||||||
|             imageUrl: thumbnailRequestUrl, |             imageUrl: thumbnailRequestUrl, | ||||||
|             httpHeaders: {"Authorization": "Bearer ${box.get(accessTokenKey)}"}, |             httpHeaders: {"Authorization": "Bearer ${box.get(accessTokenKey)}"}, | ||||||
|   | |||||||
| @@ -40,7 +40,8 @@ class SharingSliverAppBar extends StatelessWidget { | |||||||
|                   child: TextButton.icon( |                   child: TextButton.icon( | ||||||
|                     style: ButtonStyle( |                     style: ButtonStyle( | ||||||
|                       backgroundColor: MaterialStateProperty.all( |                       backgroundColor: MaterialStateProperty.all( | ||||||
|                           Theme.of(context).primaryColor.withAlpha(20)), |                         Theme.of(context).primaryColor.withAlpha(20), | ||||||
|  |                       ), | ||||||
|                       // foregroundColor: MaterialStateProperty.all(Colors.white), |                       // foregroundColor: MaterialStateProperty.all(Colors.white), | ||||||
|                     ), |                     ), | ||||||
|                     onPressed: () { |                     onPressed: () { | ||||||
| @@ -65,7 +66,8 @@ class SharingSliverAppBar extends StatelessWidget { | |||||||
|                   child: TextButton.icon( |                   child: TextButton.icon( | ||||||
|                     style: ButtonStyle( |                     style: ButtonStyle( | ||||||
|                       backgroundColor: MaterialStateProperty.all( |                       backgroundColor: MaterialStateProperty.all( | ||||||
|                           Theme.of(context).primaryColor.withAlpha(20)), |                         Theme.of(context).primaryColor.withAlpha(20), | ||||||
|  |                       ), | ||||||
|                       // foregroundColor: MaterialStateProperty.all(Colors.white), |                       // foregroundColor: MaterialStateProperty.all(Colors.white), | ||||||
|                     ), |                     ), | ||||||
|                     onPressed: null, |                     onPressed: null, | ||||||
|   | |||||||
| @@ -7,7 +7,6 @@ import 'package:immich_mobile/constants/immich_colors.dart'; | |||||||
| import 'package:immich_mobile/modules/home/ui/draggable_scrollbar.dart'; | import 'package:immich_mobile/modules/home/ui/draggable_scrollbar.dart'; | ||||||
| import 'package:immich_mobile/modules/login/providers/authentication.provider.dart'; | import 'package:immich_mobile/modules/login/providers/authentication.provider.dart'; | ||||||
| import 'package:immich_mobile/modules/sharing/models/asset_selection_page_result.model.dart'; | import 'package:immich_mobile/modules/sharing/models/asset_selection_page_result.model.dart'; | ||||||
| import 'package:immich_mobile/modules/sharing/models/shared_album.model.dart'; |  | ||||||
| import 'package:immich_mobile/modules/sharing/providers/asset_selection.provider.dart'; | import 'package:immich_mobile/modules/sharing/providers/asset_selection.provider.dart'; | ||||||
| import 'package:immich_mobile/modules/sharing/providers/shared_album.provider.dart'; | import 'package:immich_mobile/modules/sharing/providers/shared_album.provider.dart'; | ||||||
| import 'package:immich_mobile/modules/sharing/services/shared_album.service.dart'; | import 'package:immich_mobile/modules/sharing/services/shared_album.service.dart'; | ||||||
| @@ -19,7 +18,7 @@ import 'package:immich_mobile/routing/router.dart'; | |||||||
| import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart'; | import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart'; | ||||||
| import 'package:immich_mobile/shared/ui/immich_sliver_persistent_app_bar_delegate.dart'; | import 'package:immich_mobile/shared/ui/immich_sliver_persistent_app_bar_delegate.dart'; | ||||||
| import 'package:immich_mobile/shared/views/immich_loading_overlay.dart'; | import 'package:immich_mobile/shared/views/immich_loading_overlay.dart'; | ||||||
| import 'package:intl/intl.dart'; | import 'package:openapi/api.dart'; | ||||||
|  |  | ||||||
| class AlbumViewerPage extends HookConsumerWidget { | class AlbumViewerPage extends HookConsumerWidget { | ||||||
|   final String albumId; |   final String albumId; | ||||||
| @@ -30,18 +29,18 @@ class AlbumViewerPage extends HookConsumerWidget { | |||||||
|   Widget build(BuildContext context, WidgetRef ref) { |   Widget build(BuildContext context, WidgetRef ref) { | ||||||
|     FocusNode titleFocusNode = useFocusNode(); |     FocusNode titleFocusNode = useFocusNode(); | ||||||
|     ScrollController scrollController = useScrollController(); |     ScrollController scrollController = useScrollController(); | ||||||
|     AsyncValue<SharedAlbum> albumInfo = |     AsyncValue<AlbumResponseDto?> albumInfo = | ||||||
|         ref.watch(sharedAlbumDetailProvider(albumId)); |         ref.watch(sharedAlbumDetailProvider(albumId)); | ||||||
|  |  | ||||||
|     final userId = ref.watch(authenticationProvider).userId; |     final userId = ref.watch(authenticationProvider).userId; | ||||||
|  |  | ||||||
|     /// Find out if the assets in album exist on the device |     /// 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. |     /// If they exist, add to selected asset state to show they are already selected. | ||||||
|     void _onAddPhotosPressed(SharedAlbum albumInfo) async { |     void _onAddPhotosPressed(AlbumResponseDto albumInfo) async { | ||||||
|       if (albumInfo.assets?.isNotEmpty == true) { |       if (albumInfo.assets.isNotEmpty == true) { | ||||||
|         ref |         ref | ||||||
|             .watch(assetSelectionProvider.notifier) |             .watch(assetSelectionProvider.notifier) | ||||||
|             .addNewAssets(albumInfo.assets!.toList()); |             .addNewAssets(albumInfo.assets.toList()); | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       ref.watch(assetSelectionProvider.notifier).setIsAlbumExist(true); |       ref.watch(assetSelectionProvider.notifier).setIsAlbumExist(true); | ||||||
| @@ -57,7 +56,9 @@ class AlbumViewerPage extends HookConsumerWidget { | |||||||
|           var isSuccess = await ref |           var isSuccess = await ref | ||||||
|               .watch(sharedAlbumServiceProvider) |               .watch(sharedAlbumServiceProvider) | ||||||
|               .addAdditionalAssetToAlbum( |               .addAdditionalAssetToAlbum( | ||||||
|                   returnPayload.selectedAdditionalAsset, albumId); |                 returnPayload.selectedAdditionalAsset, | ||||||
|  |                 albumId, | ||||||
|  |               ); | ||||||
|  |  | ||||||
|           if (isSuccess) { |           if (isSuccess) { | ||||||
|             ref.refresh(sharedAlbumDetailProvider(albumId)); |             ref.refresh(sharedAlbumDetailProvider(albumId)); | ||||||
| @@ -72,10 +73,11 @@ class AlbumViewerPage extends HookConsumerWidget { | |||||||
|       } |       } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     void _onAddUsersPressed(SharedAlbum albumInfo) async { |     void _onAddUsersPressed(AlbumResponseDto albumInfo) async { | ||||||
|       List<String>? sharedUserIds = await AutoRouter.of(context) |       List<String>? sharedUserIds = | ||||||
|           .push<List<String>?>( |           await AutoRouter.of(context).push<List<String>?>( | ||||||
|               SelectAdditionalUserForSharingRoute(albumInfo: albumInfo)); |         SelectAdditionalUserForSharingRoute(albumInfo: albumInfo), | ||||||
|  |       ); | ||||||
|  |  | ||||||
|       if (sharedUserIds != null) { |       if (sharedUserIds != null) { | ||||||
|         ImmichLoadingOverlayController.appLoader.show(); |         ImmichLoadingOverlayController.appLoader.show(); | ||||||
| @@ -92,7 +94,7 @@ class AlbumViewerPage extends HookConsumerWidget { | |||||||
|       } |       } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     Widget _buildTitle(SharedAlbum albumInfo) { |     Widget _buildTitle(AlbumResponseDto albumInfo) { | ||||||
|       return Padding( |       return Padding( | ||||||
|         padding: const EdgeInsets.only(left: 8, right: 8, top: 16), |         padding: const EdgeInsets.only(left: 8, right: 8, top: 16), | ||||||
|         child: userId == albumInfo.ownerId |         child: userId == albumInfo.ownerId | ||||||
| @@ -102,19 +104,24 @@ class AlbumViewerPage extends HookConsumerWidget { | |||||||
|               ) |               ) | ||||||
|             : Padding( |             : Padding( | ||||||
|                 padding: const EdgeInsets.only(left: 8.0), |                 padding: const EdgeInsets.only(left: 8.0), | ||||||
|                 child: Text(albumInfo.albumName, |                 child: Text( | ||||||
|                     style: const TextStyle( |                   albumInfo.albumName, | ||||||
|                         fontSize: 24, fontWeight: FontWeight.bold)), |                   style: const TextStyle( | ||||||
|  |                     fontSize: 24, | ||||||
|  |                     fontWeight: FontWeight.bold, | ||||||
|  |                   ), | ||||||
|  |                 ), | ||||||
|               ), |               ), | ||||||
|       ); |       ); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     Widget _buildAlbumDateRange(SharedAlbum albumInfo) { |     Widget _buildAlbumDateRange(AlbumResponseDto albumInfo) { | ||||||
|       String startDate = ""; |       String startDate = ""; | ||||||
|       DateTime parsedStartDate = |       DateTime parsedStartDate = | ||||||
|           DateTime.parse(albumInfo.assets!.first.createdAt); |           DateTime.parse(albumInfo.assets.first.createdAt); | ||||||
|       DateTime parsedEndDate = DateTime.parse( |       DateTime parsedEndDate = DateTime.parse( | ||||||
|           albumInfo.assets?.last.createdAt ?? '11111111'); //Need default. |         albumInfo.assets.last.createdAt, | ||||||
|  |       ); //Need default. | ||||||
|  |  | ||||||
|       if (parsedStartDate.year == parsedEndDate.year) { |       if (parsedStartDate.year == parsedEndDate.year) { | ||||||
|         startDate = DateFormat('LLL d').format(parsedStartDate); |         startDate = DateFormat('LLL d').format(parsedStartDate); | ||||||
| @@ -129,18 +136,21 @@ class AlbumViewerPage extends HookConsumerWidget { | |||||||
|         child: Text( |         child: Text( | ||||||
|           "$startDate-$endDate", |           "$startDate-$endDate", | ||||||
|           style: const TextStyle( |           style: const TextStyle( | ||||||
|               fontSize: 14, fontWeight: FontWeight.bold, color: Colors.grey), |             fontSize: 14, | ||||||
|  |             fontWeight: FontWeight.bold, | ||||||
|  |             color: Colors.grey, | ||||||
|  |           ), | ||||||
|         ), |         ), | ||||||
|       ); |       ); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     Widget _buildHeader(SharedAlbum albumInfo) { |     Widget _buildHeader(AlbumResponseDto albumInfo) { | ||||||
|       return SliverToBoxAdapter( |       return SliverToBoxAdapter( | ||||||
|         child: Column( |         child: Column( | ||||||
|           crossAxisAlignment: CrossAxisAlignment.start, |           crossAxisAlignment: CrossAxisAlignment.start, | ||||||
|           children: [ |           children: [ | ||||||
|             _buildTitle(albumInfo), |             _buildTitle(albumInfo), | ||||||
|             if (albumInfo.assets?.isNotEmpty == true) |             if (albumInfo.assets.isNotEmpty == true) | ||||||
|               _buildAlbumDateRange(albumInfo), |               _buildAlbumDateRange(albumInfo), | ||||||
|             SizedBox( |             SizedBox( | ||||||
|               height: 60, |               height: 60, | ||||||
| @@ -172,8 +182,8 @@ class AlbumViewerPage extends HookConsumerWidget { | |||||||
|       ); |       ); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     Widget _buildImageGrid(SharedAlbum albumInfo) { |     Widget _buildImageGrid(AlbumResponseDto albumInfo) { | ||||||
|       if (albumInfo.assets?.isNotEmpty == true) { |       if (albumInfo.assets.isNotEmpty) { | ||||||
|         return SliverPadding( |         return SliverPadding( | ||||||
|           padding: const EdgeInsets.only(top: 10.0), |           padding: const EdgeInsets.only(top: 10.0), | ||||||
|           sliver: SliverGrid( |           sliver: SliverGrid( | ||||||
| @@ -184,9 +194,9 @@ class AlbumViewerPage extends HookConsumerWidget { | |||||||
|             ), |             ), | ||||||
|             delegate: SliverChildBuilderDelegate( |             delegate: SliverChildBuilderDelegate( | ||||||
|               (BuildContext context, int index) { |               (BuildContext context, int index) { | ||||||
|                 return AlbumViewerThumbnail(asset: albumInfo.assets![index]); |                 return AlbumViewerThumbnail(asset: albumInfo.assets[index]); | ||||||
|               }, |               }, | ||||||
|               childCount: albumInfo.assets?.length, |               childCount: albumInfo.assets.length, | ||||||
|             ), |             ), | ||||||
|           ), |           ), | ||||||
|         ); |         ); | ||||||
| @@ -194,7 +204,7 @@ class AlbumViewerPage extends HookConsumerWidget { | |||||||
|       return const SliverToBoxAdapter(); |       return const SliverToBoxAdapter(); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     Widget _buildControlButton(SharedAlbum albumInfo) { |     Widget _buildControlButton(AlbumResponseDto albumInfo) { | ||||||
|       return Padding( |       return Padding( | ||||||
|         padding: const EdgeInsets.only(left: 16.0, top: 8, bottom: 8), |         padding: const EdgeInsets.only(left: 16.0, top: 8, bottom: 8), | ||||||
|         child: SizedBox( |         child: SizedBox( | ||||||
| @@ -219,7 +229,7 @@ class AlbumViewerPage extends HookConsumerWidget { | |||||||
|       ); |       ); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     Widget _buildBody(SharedAlbum albumInfo) { |     Widget _buildBody(AlbumResponseDto albumInfo) { | ||||||
|       return GestureDetector( |       return GestureDetector( | ||||||
|         onTap: () { |         onTap: () { | ||||||
|           titleFocusNode.unfocus(); |           titleFocusNode.unfocus(); | ||||||
| @@ -252,9 +262,16 @@ class AlbumViewerPage extends HookConsumerWidget { | |||||||
|  |  | ||||||
|     return Scaffold( |     return Scaffold( | ||||||
|       appBar: AlbumViewerAppbar( |       appBar: AlbumViewerAppbar( | ||||||
|           albumInfo: albumInfo, userId: userId, albumId: albumId), |         albumInfo: albumInfo, | ||||||
|  |         userId: userId, | ||||||
|  |         albumId: albumId, | ||||||
|  |       ), | ||||||
|       body: albumInfo.when( |       body: albumInfo.when( | ||||||
|         data: (albumInfo) => _buildBody(albumInfo), |         data: (albumInfo) => albumInfo != null | ||||||
|  |             ? _buildBody(albumInfo) | ||||||
|  |             : const Center( | ||||||
|  |                 child: CircularProgressIndicator(), | ||||||
|  |               ), | ||||||
|         error: (e, _) => Center(child: Text("Error loading album info $e")), |         error: (e, _) => Center(child: Text("Error loading album info $e")), | ||||||
|         loading: () => const Center( |         loading: () => const Center( | ||||||
|           child: ImmichLoadingIndicator(), |           child: ImmichLoadingIndicator(), | ||||||
|   | |||||||
| @@ -56,10 +56,11 @@ class CreateSharedAlbumPage extends HookConsumerWidget { | |||||||
|           left: 10, |           left: 10, | ||||||
|         ), |         ), | ||||||
|         child: AlbumTitleTextField( |         child: AlbumTitleTextField( | ||||||
|             isAlbumTitleEmpty: isAlbumTitleEmpty, |           isAlbumTitleEmpty: isAlbumTitleEmpty, | ||||||
|             albumTitleTextFieldFocusNode: albumTitleTextFieldFocusNode, |           albumTitleTextFieldFocusNode: albumTitleTextFieldFocusNode, | ||||||
|             albumTitleController: albumTitleController, |           albumTitleController: albumTitleController, | ||||||
|             isAlbumTitleTextFieldFocus: isAlbumTitleTextFieldFocus), |           isAlbumTitleTextFieldFocus: isAlbumTitleTextFieldFocus, | ||||||
|  |         ), | ||||||
|       ); |       ); | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -67,8 +68,8 @@ class CreateSharedAlbumPage extends HookConsumerWidget { | |||||||
|       if (selectedAssets.isEmpty) { |       if (selectedAssets.isEmpty) { | ||||||
|         return SliverToBoxAdapter( |         return SliverToBoxAdapter( | ||||||
|           child: Padding( |           child: Padding( | ||||||
|             padding: EdgeInsets.only(top: 200, left: 18), |             padding: const EdgeInsets.only(top: 200, left: 18), | ||||||
|             child: Text( |             child: const Text( | ||||||
|               'create_shared_album_page_share_add_assets', |               'create_shared_album_page_share_add_assets', | ||||||
|               style: TextStyle(fontSize: 12), |               style: TextStyle(fontSize: 12), | ||||||
|             ).tr(), |             ).tr(), | ||||||
| @@ -86,13 +87,16 @@ class CreateSharedAlbumPage extends HookConsumerWidget { | |||||||
|             padding: const EdgeInsets.only(top: 16, left: 18, right: 18), |             padding: const EdgeInsets.only(top: 16, left: 18, right: 18), | ||||||
|             child: OutlinedButton.icon( |             child: OutlinedButton.icon( | ||||||
|               style: OutlinedButton.styleFrom( |               style: OutlinedButton.styleFrom( | ||||||
|                   alignment: Alignment.centerLeft, |                 alignment: Alignment.centerLeft, | ||||||
|                   padding: |                 padding: | ||||||
|                       const EdgeInsets.symmetric(vertical: 22, horizontal: 16), |                     const EdgeInsets.symmetric(vertical: 22, horizontal: 16), | ||||||
|                   side: const BorderSide( |                 side: const BorderSide( | ||||||
|                       color: Color.fromARGB(255, 206, 206, 206)), |                   color: Color.fromARGB(255, 206, 206, 206), | ||||||
|                   shape: RoundedRectangleBorder( |                 ), | ||||||
|                       borderRadius: BorderRadius.circular(5))), |                 shape: RoundedRectangleBorder( | ||||||
|  |                   borderRadius: BorderRadius.circular(5), | ||||||
|  |                 ), | ||||||
|  |               ), | ||||||
|               onPressed: _onSelectPhotosButtonPressed, |               onPressed: _onSelectPhotosButtonPressed, | ||||||
|               icon: const Icon(Icons.add_rounded), |               icon: const Icon(Icons.add_rounded), | ||||||
|               label: Padding( |               label: Padding( | ||||||
| @@ -100,9 +104,10 @@ class CreateSharedAlbumPage extends HookConsumerWidget { | |||||||
|                 child: Text( |                 child: Text( | ||||||
|                   'create_shared_album_page_share_select_photos', |                   'create_shared_album_page_share_select_photos', | ||||||
|                   style: TextStyle( |                   style: TextStyle( | ||||||
|                       fontSize: 16, |                     fontSize: 16, | ||||||
|                       color: Colors.grey[700], |                     color: Colors.grey[700], | ||||||
|                       fontWeight: FontWeight.bold), |                     fontWeight: FontWeight.bold, | ||||||
|  |                   ), | ||||||
|                 ).tr(), |                 ).tr(), | ||||||
|               ), |               ), | ||||||
|             ), |             ), | ||||||
| @@ -147,7 +152,8 @@ class CreateSharedAlbumPage extends HookConsumerWidget { | |||||||
|                 return GestureDetector( |                 return GestureDetector( | ||||||
|                   onTap: _onBackgroundTapped, |                   onTap: _onBackgroundTapped, | ||||||
|                   child: SharedAlbumThumbnailImage( |                   child: SharedAlbumThumbnailImage( | ||||||
|                       asset: selectedAssets.toList()[index]), |                     asset: selectedAssets.toList()[index], | ||||||
|  |                   ), | ||||||
|                 ); |                 ); | ||||||
|               }, |               }, | ||||||
|               childCount: selectedAssets.length, |               childCount: selectedAssets.length, | ||||||
| @@ -160,58 +166,60 @@ class CreateSharedAlbumPage extends HookConsumerWidget { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     return Scaffold( |     return Scaffold( | ||||||
|         appBar: AppBar( |       appBar: AppBar( | ||||||
|           elevation: 0, |         elevation: 0, | ||||||
|           centerTitle: false, |         centerTitle: false, | ||||||
|           leading: IconButton( |         leading: IconButton( | ||||||
|               onPressed: () { |           onPressed: () { | ||||||
|                 ref.watch(assetSelectionProvider.notifier).removeAll(); |             ref.watch(assetSelectionProvider.notifier).removeAll(); | ||||||
|                 AutoRouter.of(context).pop(); |             AutoRouter.of(context).pop(); | ||||||
|               }, |           }, | ||||||
|               icon: const Icon(Icons.close_rounded)), |           icon: const Icon(Icons.close_rounded), | ||||||
|           title: const Text( |         ), | ||||||
|             'share_create_album', |         title: const Text( | ||||||
|             style: TextStyle(color: Colors.black), |           'share_create_album', | ||||||
|           ).tr(), |           style: TextStyle(color: Colors.black), | ||||||
|           actions: [ |         ).tr(), | ||||||
|             TextButton( |         actions: [ | ||||||
|               onPressed: albumTitleController.text.isNotEmpty |           TextButton( | ||||||
|                   ? _showSelectUserPage |             onPressed: albumTitleController.text.isNotEmpty | ||||||
|                   : null, |                 ? _showSelectUserPage | ||||||
|               child: Text( |                 : null, | ||||||
|                 'create_shared_album_page_share'.tr(), |             child: Text( | ||||||
|                 style: TextStyle( |               'create_shared_album_page_share'.tr(), | ||||||
|                   fontWeight: FontWeight.bold, |               style: const TextStyle( | ||||||
|  |                 fontWeight: FontWeight.bold, | ||||||
|  |               ), | ||||||
|  |             ), | ||||||
|  |           ), | ||||||
|  |         ], | ||||||
|  |       ), | ||||||
|  |       body: GestureDetector( | ||||||
|  |         onTap: _onBackgroundTapped, | ||||||
|  |         child: CustomScrollView( | ||||||
|  |           slivers: [ | ||||||
|  |             SliverAppBar( | ||||||
|  |               elevation: 5, | ||||||
|  |               automaticallyImplyLeading: false, | ||||||
|  |               // leading: Container(), | ||||||
|  |               pinned: true, | ||||||
|  |               floating: false, | ||||||
|  |               bottom: PreferredSize( | ||||||
|  |                 preferredSize: const Size.fromHeight(66.0), | ||||||
|  |                 child: Column( | ||||||
|  |                   children: [ | ||||||
|  |                     _buildTitleInputField(), | ||||||
|  |                     if (selectedAssets.isNotEmpty) _buildControlButton(), | ||||||
|  |                   ], | ||||||
|                 ), |                 ), | ||||||
|               ), |               ), | ||||||
|             ), |             ), | ||||||
|  |             _buildTitle(), | ||||||
|  |             _buildSelectPhotosButton(), | ||||||
|  |             _buildSelectedImageGrid(), | ||||||
|           ], |           ], | ||||||
|         ), |         ), | ||||||
|         body: GestureDetector( |       ), | ||||||
|           onTap: _onBackgroundTapped, |     ); | ||||||
|           child: CustomScrollView( |  | ||||||
|             slivers: [ |  | ||||||
|               SliverAppBar( |  | ||||||
|                 elevation: 5, |  | ||||||
|                 automaticallyImplyLeading: false, |  | ||||||
|                 // leading: Container(), |  | ||||||
|                 pinned: true, |  | ||||||
|                 floating: false, |  | ||||||
|                 bottom: PreferredSize( |  | ||||||
|                   preferredSize: const Size.fromHeight(66.0), |  | ||||||
|                   child: Column( |  | ||||||
|                     children: [ |  | ||||||
|                       _buildTitleInputField(), |  | ||||||
|                       if (selectedAssets.isNotEmpty) _buildControlButton(), |  | ||||||
|                     ], |  | ||||||
|                   ), |  | ||||||
|                 ), |  | ||||||
|               ), |  | ||||||
|               _buildTitle(), |  | ||||||
|               _buildSelectPhotosButton(), |  | ||||||
|               _buildSelectedImageGrid(), |  | ||||||
|             ], |  | ||||||
|           ), |  | ||||||
|         )); |  | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -3,29 +3,28 @@ import 'package:easy_localization/easy_localization.dart'; | |||||||
| import 'package:flutter/material.dart'; | import 'package:flutter/material.dart'; | ||||||
| import 'package:flutter_hooks/flutter_hooks.dart'; | import 'package:flutter_hooks/flutter_hooks.dart'; | ||||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||||
| import 'package:immich_mobile/modules/sharing/models/shared_album.model.dart'; |  | ||||||
| import 'package:immich_mobile/modules/sharing/providers/suggested_shared_users.provider.dart'; | import 'package:immich_mobile/modules/sharing/providers/suggested_shared_users.provider.dart'; | ||||||
| import 'package:immich_mobile/shared/models/user.model.dart'; |  | ||||||
| import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart'; | import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart'; | ||||||
|  | import 'package:openapi/api.dart'; | ||||||
|  |  | ||||||
| class SelectAdditionalUserForSharingPage extends HookConsumerWidget { | class SelectAdditionalUserForSharingPage extends HookConsumerWidget { | ||||||
|   final SharedAlbum albumInfo; |   final AlbumResponseDto albumInfo; | ||||||
|  |  | ||||||
|   const SelectAdditionalUserForSharingPage({Key? key, required this.albumInfo}) |   const SelectAdditionalUserForSharingPage({Key? key, required this.albumInfo}) | ||||||
|       : super(key: key); |       : super(key: key); | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   Widget build(BuildContext context, WidgetRef ref) { |   Widget build(BuildContext context, WidgetRef ref) { | ||||||
|     AsyncValue<List<User>> suggestedShareUsers = |     AsyncValue<List<UserResponseDto>> suggestedShareUsers = | ||||||
|         ref.watch(suggestedSharedUsersProvider); |         ref.watch(suggestedSharedUsersProvider); | ||||||
|     final sharedUsersList = useState<Set<User>>({}); |     final sharedUsersList = useState<Set<UserResponseDto>>({}); | ||||||
|  |  | ||||||
|     _addNewUsersHandler() { |     _addNewUsersHandler() { | ||||||
|       AutoRouter.of(context) |       AutoRouter.of(context) | ||||||
|           .pop(sharedUsersList.value.map((e) => e.id).toList()); |           .pop(sharedUsersList.value.map((e) => e.id).toList()); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     _buildTileIcon(User user) { |     _buildTileIcon(UserResponseDto user) { | ||||||
|       if (sharedUsersList.value.contains(user)) { |       if (sharedUsersList.value.contains(user)) { | ||||||
|         return CircleAvatar( |         return CircleAvatar( | ||||||
|           backgroundColor: Theme.of(context).primaryColor, |           backgroundColor: Theme.of(context).primaryColor, | ||||||
| @@ -43,7 +42,7 @@ class SelectAdditionalUserForSharingPage extends HookConsumerWidget { | |||||||
|       } |       } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     _buildUserList(List<User> users) { |     _buildUserList(List<UserResponseDto> users) { | ||||||
|       List<Widget> usersChip = []; |       List<Widget> usersChip = []; | ||||||
|  |  | ||||||
|       for (var user in sharedUsersList.value) { |       for (var user in sharedUsersList.value) { | ||||||
| @@ -55,9 +54,10 @@ class SelectAdditionalUserForSharingPage extends HookConsumerWidget { | |||||||
|               label: Text( |               label: Text( | ||||||
|                 user.email, |                 user.email, | ||||||
|                 style: const TextStyle( |                 style: const TextStyle( | ||||||
|                     fontSize: 12, |                   fontSize: 12, | ||||||
|                     color: Colors.black87, |                   color: Colors.black87, | ||||||
|                     fontWeight: FontWeight.bold), |                   fontWeight: FontWeight.bold, | ||||||
|  |                 ), | ||||||
|               ), |               ), | ||||||
|             ), |             ), | ||||||
|           ), |           ), | ||||||
| @@ -70,13 +70,14 @@ class SelectAdditionalUserForSharingPage extends HookConsumerWidget { | |||||||
|             children: [...usersChip], |             children: [...usersChip], | ||||||
|           ), |           ), | ||||||
|           Padding( |           Padding( | ||||||
|             padding: EdgeInsets.all(16.0), |             padding: const EdgeInsets.all(16.0), | ||||||
|             child: Text( |             child: Text( | ||||||
|               'select_additional_user_for_sharing_page_suggestions'.tr(), |               'select_additional_user_for_sharing_page_suggestions'.tr(), | ||||||
|               style: TextStyle( |               style: const TextStyle( | ||||||
|                   fontSize: 14, |                 fontSize: 14, | ||||||
|                   color: Colors.grey, |                 color: Colors.grey, | ||||||
|                   fontWeight: FontWeight.bold), |                 fontWeight: FontWeight.bold, | ||||||
|  |               ), | ||||||
|             ), |             ), | ||||||
|           ), |           ), | ||||||
|           ListView.builder( |           ListView.builder( | ||||||
| @@ -87,13 +88,16 @@ class SelectAdditionalUserForSharingPage extends HookConsumerWidget { | |||||||
|                 title: Text( |                 title: Text( | ||||||
|                   users[index].email, |                   users[index].email, | ||||||
|                   style: const TextStyle( |                   style: const TextStyle( | ||||||
|                       fontSize: 14, fontWeight: FontWeight.bold), |                     fontSize: 14, | ||||||
|  |                     fontWeight: FontWeight.bold, | ||||||
|  |                   ), | ||||||
|                 ), |                 ), | ||||||
|                 onTap: () { |                 onTap: () { | ||||||
|                   if (sharedUsersList.value.contains(users[index])) { |                   if (sharedUsersList.value.contains(users[index])) { | ||||||
|                     sharedUsersList.value = sharedUsersList.value |                     sharedUsersList.value = sharedUsersList.value | ||||||
|                         .where((selectedUser) => |                         .where( | ||||||
|                             selectedUser.id != users[index].id) |                           (selectedUser) => selectedUser.id != users[index].id, | ||||||
|  |                         ) | ||||||
|                         .toSet(); |                         .toSet(); | ||||||
|                   } else { |                   } else { | ||||||
|                     sharedUsersList.value = { |                     sharedUsersList.value = { | ||||||
| @@ -139,7 +143,8 @@ class SelectAdditionalUserForSharingPage extends HookConsumerWidget { | |||||||
|         data: (users) { |         data: (users) { | ||||||
|           for (var sharedUsers in albumInfo.sharedUsers) { |           for (var sharedUsers in albumInfo.sharedUsers) { | ||||||
|             users.removeWhere( |             users.removeWhere( | ||||||
|                 (u) => u.id == sharedUsers.id || u.id == albumInfo.ownerId); |               (u) => u.id == sharedUsers.id || u.id == albumInfo.ownerId, | ||||||
|  |             ); | ||||||
|           } |           } | ||||||
|  |  | ||||||
|           return _buildUserList(users); |           return _buildUserList(users); | ||||||
|   | |||||||
| @@ -9,15 +9,16 @@ import 'package:immich_mobile/modules/sharing/providers/shared_album.provider.da | |||||||
| import 'package:immich_mobile/modules/sharing/providers/suggested_shared_users.provider.dart'; | import 'package:immich_mobile/modules/sharing/providers/suggested_shared_users.provider.dart'; | ||||||
| import 'package:immich_mobile/modules/sharing/services/shared_album.service.dart'; | import 'package:immich_mobile/modules/sharing/services/shared_album.service.dart'; | ||||||
| import 'package:immich_mobile/routing/router.dart'; | import 'package:immich_mobile/routing/router.dart'; | ||||||
| import 'package:immich_mobile/shared/models/user.model.dart'; |  | ||||||
| import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart'; | import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart'; | ||||||
|  | import 'package:openapi/api.dart'; | ||||||
|  |  | ||||||
| class SelectUserForSharingPage extends HookConsumerWidget { | class SelectUserForSharingPage extends HookConsumerWidget { | ||||||
|   const SelectUserForSharingPage({Key? key}) : super(key: key); |   const SelectUserForSharingPage({Key? key}) : super(key: key); | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   Widget build(BuildContext context, WidgetRef ref) { |   Widget build(BuildContext context, WidgetRef ref) { | ||||||
|     final sharedUsersList = useState<Set<User>>({}); |     final sharedUsersList = useState<Set<UserResponseDto>>({}); | ||||||
|     AsyncValue<List<User>> suggestedShareUsers = |     AsyncValue<List<UserResponseDto>> suggestedShareUsers = | ||||||
|         ref.watch(suggestedSharedUsersProvider); |         ref.watch(suggestedSharedUsersProvider); | ||||||
|  |  | ||||||
|     _createSharedAlbum() async { |     _createSharedAlbum() async { | ||||||
| @@ -37,10 +38,14 @@ class SelectUserForSharingPage extends HookConsumerWidget { | |||||||
|             .navigate(const TabControllerRoute(children: [SharingRoute()])); |             .navigate(const TabControllerRoute(children: [SharingRoute()])); | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       ScaffoldMessenger(child: SnackBar(content: Text('select_user_for_sharing_page_err_album').tr())); |       ScaffoldMessenger( | ||||||
|  |         child: SnackBar( | ||||||
|  |           content: const Text('select_user_for_sharing_page_err_album').tr(), | ||||||
|  |         ), | ||||||
|  |       ); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     _buildTileIcon(User user) { |     _buildTileIcon(UserResponseDto user) { | ||||||
|       if (sharedUsersList.value.contains(user)) { |       if (sharedUsersList.value.contains(user)) { | ||||||
|         return CircleAvatar( |         return CircleAvatar( | ||||||
|           backgroundColor: Theme.of(context).primaryColor, |           backgroundColor: Theme.of(context).primaryColor, | ||||||
| @@ -58,7 +63,7 @@ class SelectUserForSharingPage extends HookConsumerWidget { | |||||||
|       } |       } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     _buildUserList(List<User> users) { |     _buildUserList(List<UserResponseDto> users) { | ||||||
|       List<Widget> usersChip = []; |       List<Widget> usersChip = []; | ||||||
|  |  | ||||||
|       for (var user in sharedUsersList.value) { |       for (var user in sharedUsersList.value) { | ||||||
| @@ -70,9 +75,10 @@ class SelectUserForSharingPage extends HookConsumerWidget { | |||||||
|               label: Text( |               label: Text( | ||||||
|                 user.email, |                 user.email, | ||||||
|                 style: const TextStyle( |                 style: const TextStyle( | ||||||
|                     fontSize: 12, |                   fontSize: 12, | ||||||
|                     color: Colors.black87, |                   color: Colors.black87, | ||||||
|                     fontWeight: FontWeight.bold), |                   fontWeight: FontWeight.bold, | ||||||
|  |                 ), | ||||||
|               ), |               ), | ||||||
|             ), |             ), | ||||||
|           ), |           ), | ||||||
| @@ -85,13 +91,14 @@ class SelectUserForSharingPage extends HookConsumerWidget { | |||||||
|             children: [...usersChip], |             children: [...usersChip], | ||||||
|           ), |           ), | ||||||
|           Padding( |           Padding( | ||||||
|             padding: EdgeInsets.all(16.0), |             padding: const EdgeInsets.all(16.0), | ||||||
|             child: Text( |             child: const Text( | ||||||
|               'share_suggestions', |               'select_user_for_sharing_page_share_suggestions', | ||||||
|               style: TextStyle( |               style: TextStyle( | ||||||
|                   fontSize: 14, |                 fontSize: 14, | ||||||
|                   color: Colors.grey, |                 color: Colors.grey, | ||||||
|                   fontWeight: FontWeight.bold), |                 fontWeight: FontWeight.bold, | ||||||
|  |               ), | ||||||
|             ).tr(), |             ).tr(), | ||||||
|           ), |           ), | ||||||
|           ListView.builder( |           ListView.builder( | ||||||
| @@ -102,13 +109,16 @@ class SelectUserForSharingPage extends HookConsumerWidget { | |||||||
|                 title: Text( |                 title: Text( | ||||||
|                   users[index].email, |                   users[index].email, | ||||||
|                   style: const TextStyle( |                   style: const TextStyle( | ||||||
|                       fontSize: 14, fontWeight: FontWeight.bold), |                     fontSize: 14, | ||||||
|  |                     fontWeight: FontWeight.bold, | ||||||
|  |                   ), | ||||||
|                 ), |                 ), | ||||||
|                 onTap: () { |                 onTap: () { | ||||||
|                   if (sharedUsersList.value.contains(users[index])) { |                   if (sharedUsersList.value.contains(users[index])) { | ||||||
|                     sharedUsersList.value = sharedUsersList.value |                     sharedUsersList.value = sharedUsersList.value | ||||||
|                         .where((selectedUser) => |                         .where( | ||||||
|                             selectedUser.id != users[index].id) |                           (selectedUser) => selectedUser.id != users[index].id, | ||||||
|  |                         ) | ||||||
|                         .toSet(); |                         .toSet(); | ||||||
|                   } else { |                   } else { | ||||||
|                     sharedUsersList.value = { |                     sharedUsersList.value = { | ||||||
| @@ -141,12 +151,13 @@ class SelectUserForSharingPage extends HookConsumerWidget { | |||||||
|         ), |         ), | ||||||
|         actions: [ |         actions: [ | ||||||
|           TextButton( |           TextButton( | ||||||
|               onPressed: |             onPressed: | ||||||
|                   sharedUsersList.value.isEmpty ? null : _createSharedAlbum, |                 sharedUsersList.value.isEmpty ? null : _createSharedAlbum, | ||||||
|               child: const Text( |             child: const Text( | ||||||
|                 "share_create_album", |               "share_create_album", | ||||||
|                 style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold), |               style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold), | ||||||
|               ).tr()) |             ).tr(), | ||||||
|  |           ) | ||||||
|         ], |         ], | ||||||
|       ), |       ), | ||||||
|       body: suggestedShareUsers.when( |       body: suggestedShareUsers.when( | ||||||
|   | |||||||
| @@ -5,10 +5,10 @@ import 'package:flutter_hooks/flutter_hooks.dart'; | |||||||
| import 'package:hive/hive.dart'; | import 'package:hive/hive.dart'; | ||||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||||
| import 'package:immich_mobile/constants/hive_box.dart'; | import 'package:immich_mobile/constants/hive_box.dart'; | ||||||
| import 'package:immich_mobile/modules/sharing/models/shared_album.model.dart'; |  | ||||||
| import 'package:immich_mobile/modules/sharing/providers/shared_album.provider.dart'; | import 'package:immich_mobile/modules/sharing/providers/shared_album.provider.dart'; | ||||||
| import 'package:immich_mobile/modules/sharing/ui/sharing_sliver_appbar.dart'; | import 'package:immich_mobile/modules/sharing/ui/sharing_sliver_appbar.dart'; | ||||||
| import 'package:immich_mobile/routing/router.dart'; | import 'package:immich_mobile/routing/router.dart'; | ||||||
|  | import 'package:openapi/api.dart'; | ||||||
| import 'package:transparent_image/transparent_image.dart'; | import 'package:transparent_image/transparent_image.dart'; | ||||||
|  |  | ||||||
| class SharingPage extends HookConsumerWidget { | class SharingPage extends HookConsumerWidget { | ||||||
| @@ -18,13 +18,16 @@ class SharingPage extends HookConsumerWidget { | |||||||
|   Widget build(BuildContext context, WidgetRef ref) { |   Widget build(BuildContext context, WidgetRef ref) { | ||||||
|     var box = Hive.box(userInfoBox); |     var box = Hive.box(userInfoBox); | ||||||
|     var thumbnailRequestUrl = '${box.get(serverEndpointKey)}/asset/thumbnail'; |     var thumbnailRequestUrl = '${box.get(serverEndpointKey)}/asset/thumbnail'; | ||||||
|     final List<SharedAlbum> sharedAlbums = ref.watch(sharedAlbumProvider); |     final List<AlbumResponseDto> sharedAlbums = ref.watch(sharedAlbumProvider); | ||||||
|  |  | ||||||
|     useEffect(() { |     useEffect( | ||||||
|       ref.read(sharedAlbumProvider.notifier).getAllSharedAlbums(); |       () { | ||||||
|  |         ref.read(sharedAlbumProvider.notifier).getAllSharedAlbums(); | ||||||
|  |  | ||||||
|       return null; |         return null; | ||||||
|     }, []); |       }, | ||||||
|  |       [], | ||||||
|  |     ); | ||||||
|  |  | ||||||
|     _buildAlbumList() { |     _buildAlbumList() { | ||||||
|       return SliverList( |       return SliverList( | ||||||
| @@ -60,9 +63,10 @@ class SharingPage extends HookConsumerWidget { | |||||||
|                 maxLines: 1, |                 maxLines: 1, | ||||||
|                 overflow: TextOverflow.ellipsis, |                 overflow: TextOverflow.ellipsis, | ||||||
|                 style: TextStyle( |                 style: TextStyle( | ||||||
|                     fontSize: 16, |                   fontSize: 16, | ||||||
|                     fontWeight: FontWeight.bold, |                   fontWeight: FontWeight.bold, | ||||||
|                     color: Colors.grey.shade800), |                   color: Colors.grey.shade800, | ||||||
|  |                 ), | ||||||
|               ), |               ), | ||||||
|               onTap: () { |               onTap: () { | ||||||
|                 AutoRouter.of(context) |                 AutoRouter.of(context) | ||||||
| @@ -133,9 +137,9 @@ class SharingPage extends HookConsumerWidget { | |||||||
|         slivers: [ |         slivers: [ | ||||||
|           const SharingSliverAppBar(), |           const SharingSliverAppBar(), | ||||||
|           SliverPadding( |           SliverPadding( | ||||||
|             padding: EdgeInsets.symmetric(horizontal: 12, vertical: 12), |             padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12), | ||||||
|             sliver: SliverToBoxAdapter( |             sliver: SliverToBoxAdapter( | ||||||
|               child: Text( |               child: const Text( | ||||||
|                 "sharing_page_album", |                 "sharing_page_album", | ||||||
|                 style: TextStyle( |                 style: TextStyle( | ||||||
|                   fontWeight: FontWeight.bold, |                   fontWeight: FontWeight.bold, | ||||||
|   | |||||||
| @@ -1,21 +1,25 @@ | |||||||
| import 'dart:convert'; |  | ||||||
|  |  | ||||||
| import 'package:auto_route/auto_route.dart'; | import 'package:auto_route/auto_route.dart'; | ||||||
| import 'package:immich_mobile/shared/services/network.service.dart'; | import 'package:flutter/foundation.dart'; | ||||||
|  | import 'package:immich_mobile/routing/router.dart'; | ||||||
|  | import 'package:immich_mobile/shared/services/api.service.dart'; | ||||||
|  |  | ||||||
| class AuthGuard extends AutoRouteGuard { | class AuthGuard extends AutoRouteGuard { | ||||||
|   final NetworkService _networkService = NetworkService(); |   final ApiService _apiService; | ||||||
|  |   AuthGuard(this._apiService); | ||||||
|   @override |   @override | ||||||
|   void onNavigation(NavigationResolver resolver, StackRouter router) async { |   void onNavigation(NavigationResolver resolver, StackRouter router) async { | ||||||
|     try { |     try { | ||||||
|       var res = await _networkService.postRequest(url: 'auth/validateToken'); |       var res = await _apiService.authenticationApi.validateAccessToken(); | ||||||
|       var jsonReponse = jsonDecode(res.toString()); |  | ||||||
|       if (jsonReponse['authStatus']) { |       if (res != null && res.authStatus) { | ||||||
|         resolver.next(true); |         resolver.next(true); | ||||||
|  |       } else { | ||||||
|  |         router.replaceAll([const LoginRoute()]); | ||||||
|       } |       } | ||||||
|     } catch (e) { |     } catch (e) { | ||||||
|       router.removeUntil((route) => route.name == "LoginRoute"); |       debugPrint("Error [onNavigation] ${e.toString()}"); | ||||||
|  |       router.replaceAll([const LoginRoute()]); | ||||||
|  |       return; | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,5 +1,6 @@ | |||||||
| import 'package:auto_route/auto_route.dart'; | import 'package:auto_route/auto_route.dart'; | ||||||
| import 'package:flutter/material.dart'; | import 'package:flutter/material.dart'; | ||||||
|  | import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||||
| import 'package:immich_mobile/modules/backup/views/album_preview_page.dart'; | import 'package:immich_mobile/modules/backup/views/album_preview_page.dart'; | ||||||
| import 'package:immich_mobile/modules/backup/views/backup_album_selection_page.dart'; | import 'package:immich_mobile/modules/backup/views/backup_album_selection_page.dart'; | ||||||
| import 'package:immich_mobile/modules/backup/views/failed_backup_status_page.dart'; | import 'package:immich_mobile/modules/backup/views/failed_backup_status_page.dart'; | ||||||
| @@ -9,7 +10,6 @@ import 'package:immich_mobile/modules/home/views/home_page.dart'; | |||||||
| import 'package:immich_mobile/modules/search/views/search_page.dart'; | import 'package:immich_mobile/modules/search/views/search_page.dart'; | ||||||
| import 'package:immich_mobile/modules/search/views/search_result_page.dart'; | import 'package:immich_mobile/modules/search/views/search_result_page.dart'; | ||||||
| import 'package:immich_mobile/modules/sharing/models/asset_selection_page_result.model.dart'; | import 'package:immich_mobile/modules/sharing/models/asset_selection_page_result.model.dart'; | ||||||
| import 'package:immich_mobile/modules/sharing/models/shared_album.model.dart'; |  | ||||||
| import 'package:immich_mobile/modules/sharing/views/album_viewer_page.dart'; | import 'package:immich_mobile/modules/sharing/views/album_viewer_page.dart'; | ||||||
| import 'package:immich_mobile/modules/sharing/views/asset_selection_page.dart'; | import 'package:immich_mobile/modules/sharing/views/asset_selection_page.dart'; | ||||||
| import 'package:immich_mobile/modules/sharing/views/create_shared_album_page.dart'; | import 'package:immich_mobile/modules/sharing/views/create_shared_album_page.dart'; | ||||||
| @@ -17,12 +17,13 @@ import 'package:immich_mobile/modules/sharing/views/select_additional_user_for_s | |||||||
| import 'package:immich_mobile/modules/sharing/views/select_user_for_sharing_page.dart'; | import 'package:immich_mobile/modules/sharing/views/select_user_for_sharing_page.dart'; | ||||||
| import 'package:immich_mobile/modules/sharing/views/sharing_page.dart'; | import 'package:immich_mobile/modules/sharing/views/sharing_page.dart'; | ||||||
| import 'package:immich_mobile/routing/auth_guard.dart'; | import 'package:immich_mobile/routing/auth_guard.dart'; | ||||||
| import 'package:immich_mobile/shared/models/immich_asset.model.dart'; |  | ||||||
| import 'package:immich_mobile/modules/backup/views/backup_controller_page.dart'; | import 'package:immich_mobile/modules/backup/views/backup_controller_page.dart'; | ||||||
| import 'package:immich_mobile/modules/asset_viewer/views/image_viewer_page.dart'; | import 'package:immich_mobile/modules/asset_viewer/views/image_viewer_page.dart'; | ||||||
|  | import 'package:immich_mobile/shared/services/api.service.dart'; | ||||||
| import 'package:immich_mobile/shared/views/splash_screen.dart'; | import 'package:immich_mobile/shared/views/splash_screen.dart'; | ||||||
| import 'package:immich_mobile/shared/views/tab_controller_page.dart'; | import 'package:immich_mobile/shared/views/tab_controller_page.dart'; | ||||||
| import 'package:immich_mobile/modules/asset_viewer/views/video_viewer_page.dart'; | import 'package:immich_mobile/modules/asset_viewer/views/video_viewer_page.dart'; | ||||||
|  | import 'package:openapi/api.dart'; | ||||||
| import 'package:photo_manager/photo_manager.dart'; | import 'package:photo_manager/photo_manager.dart'; | ||||||
|  |  | ||||||
| part 'router.gr.dart'; | part 'router.gr.dart'; | ||||||
| @@ -74,5 +75,9 @@ part 'router.gr.dart'; | |||||||
|   ], |   ], | ||||||
| ) | ) | ||||||
| class AppRouter extends _$AppRouter { | class AppRouter extends _$AppRouter { | ||||||
|   AppRouter() : super(authGuard: AuthGuard()); |   final ApiService _apiService; | ||||||
|  |   AppRouter(this._apiService) : super(authGuard: AuthGuard(_apiService)); | ||||||
| } | } | ||||||
|  |  | ||||||
|  | final appRouterProvider = | ||||||
|  |     Provider((ref) => AppRouter(ref.watch(apiServiceProvider))); | ||||||
|   | |||||||
| @@ -234,7 +234,7 @@ class ImageViewerRoute extends PageRouteInfo<ImageViewerRouteArgs> { | |||||||
|       required String imageUrl, |       required String imageUrl, | ||||||
|       required String heroTag, |       required String heroTag, | ||||||
|       required String thumbnailUrl, |       required String thumbnailUrl, | ||||||
|       required ImmichAsset asset}) |       required AssetResponseDto asset}) | ||||||
|       : super(ImageViewerRoute.name, |       : super(ImageViewerRoute.name, | ||||||
|             path: '/image-viewer-page', |             path: '/image-viewer-page', | ||||||
|             args: ImageViewerRouteArgs( |             args: ImageViewerRouteArgs( | ||||||
| @@ -263,7 +263,7 @@ class ImageViewerRouteArgs { | |||||||
|  |  | ||||||
|   final String thumbnailUrl; |   final String thumbnailUrl; | ||||||
|  |  | ||||||
|   final ImmichAsset asset; |   final AssetResponseDto asset; | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   String toString() { |   String toString() { | ||||||
| @@ -275,7 +275,7 @@ class ImageViewerRouteArgs { | |||||||
| /// [VideoViewerPage] | /// [VideoViewerPage] | ||||||
| class VideoViewerRoute extends PageRouteInfo<VideoViewerRouteArgs> { | class VideoViewerRoute extends PageRouteInfo<VideoViewerRouteArgs> { | ||||||
|   VideoViewerRoute( |   VideoViewerRoute( | ||||||
|       {Key? key, required String videoUrl, required ImmichAsset asset}) |       {Key? key, required String videoUrl, required AssetResponseDto asset}) | ||||||
|       : super(VideoViewerRoute.name, |       : super(VideoViewerRoute.name, | ||||||
|             path: '/video-viewer-page', |             path: '/video-viewer-page', | ||||||
|             args: VideoViewerRouteArgs( |             args: VideoViewerRouteArgs( | ||||||
| @@ -292,7 +292,7 @@ class VideoViewerRouteArgs { | |||||||
|  |  | ||||||
|   final String videoUrl; |   final String videoUrl; | ||||||
|  |  | ||||||
|   final ImmichAsset asset; |   final AssetResponseDto asset; | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   String toString() { |   String toString() { | ||||||
| @@ -390,7 +390,7 @@ class AlbumViewerRouteArgs { | |||||||
| class SelectAdditionalUserForSharingRoute | class SelectAdditionalUserForSharingRoute | ||||||
|     extends PageRouteInfo<SelectAdditionalUserForSharingRouteArgs> { |     extends PageRouteInfo<SelectAdditionalUserForSharingRouteArgs> { | ||||||
|   SelectAdditionalUserForSharingRoute( |   SelectAdditionalUserForSharingRoute( | ||||||
|       {Key? key, required SharedAlbum albumInfo}) |       {Key? key, required AlbumResponseDto albumInfo}) | ||||||
|       : super(SelectAdditionalUserForSharingRoute.name, |       : super(SelectAdditionalUserForSharingRoute.name, | ||||||
|             path: '/select-additional-user-for-sharing-page', |             path: '/select-additional-user-for-sharing-page', | ||||||
|             args: SelectAdditionalUserForSharingRouteArgs( |             args: SelectAdditionalUserForSharingRouteArgs( | ||||||
| @@ -405,7 +405,7 @@ class SelectAdditionalUserForSharingRouteArgs { | |||||||
|  |  | ||||||
|   final Key? key; |   final Key? key; | ||||||
|  |  | ||||||
|   final SharedAlbum albumInfo; |   final AlbumResponseDto albumInfo; | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   String toString() { |   String toString() { | ||||||
|   | |||||||
| @@ -23,7 +23,9 @@ class TabNavigationObserver extends AutoRouterObserver { | |||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   Future<void> didChangeTabRoute( |   Future<void> didChangeTabRoute( | ||||||
|       TabPageRoute route, TabPageRoute previousRoute) async { |     TabPageRoute route, | ||||||
|  |     TabPageRoute previousRoute, | ||||||
|  |   ) async { | ||||||
|     // Perform tasks on re-visit to SearchRoute |     // Perform tasks on re-visit to SearchRoute | ||||||
|     if (route.name == 'SearchRoute') { |     if (route.name == 'SearchRoute') { | ||||||
|       // Refresh Location State |       // Refresh Location State | ||||||
|   | |||||||
| @@ -1,100 +0,0 @@ | |||||||
| import 'dart:convert'; |  | ||||||
|  |  | ||||||
| class DeviceInfoRemote { |  | ||||||
|   final int id; |  | ||||||
|   final String userId; |  | ||||||
|   final String deviceId; |  | ||||||
|   final String deviceType; |  | ||||||
|   final String notificationToken; |  | ||||||
|   final String createdAt; |  | ||||||
|   final bool isAutoBackup; |  | ||||||
|  |  | ||||||
|   DeviceInfoRemote({ |  | ||||||
|     required this.id, |  | ||||||
|     required this.userId, |  | ||||||
|     required this.deviceId, |  | ||||||
|     required this.deviceType, |  | ||||||
|     required this.notificationToken, |  | ||||||
|     required this.createdAt, |  | ||||||
|     required this.isAutoBackup, |  | ||||||
|   }); |  | ||||||
|  |  | ||||||
|   DeviceInfoRemote copyWith({ |  | ||||||
|     int? id, |  | ||||||
|     String? userId, |  | ||||||
|     String? deviceId, |  | ||||||
|     String? deviceType, |  | ||||||
|     String? notificationToken, |  | ||||||
|     String? createdAt, |  | ||||||
|     bool? isAutoBackup, |  | ||||||
|   }) { |  | ||||||
|     return DeviceInfoRemote( |  | ||||||
|       id: id ?? this.id, |  | ||||||
|       userId: userId ?? this.userId, |  | ||||||
|       deviceId: deviceId ?? this.deviceId, |  | ||||||
|       deviceType: deviceType ?? this.deviceType, |  | ||||||
|       notificationToken: notificationToken ?? this.notificationToken, |  | ||||||
|       createdAt: createdAt ?? this.createdAt, |  | ||||||
|       isAutoBackup: isAutoBackup ?? this.isAutoBackup, |  | ||||||
|     ); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   Map<String, dynamic> toMap() { |  | ||||||
|     return { |  | ||||||
|       'id': id, |  | ||||||
|       'userId': userId, |  | ||||||
|       'deviceId': deviceId, |  | ||||||
|       'deviceType': deviceType, |  | ||||||
|       'notificationToken': notificationToken, |  | ||||||
|       'createdAt': createdAt, |  | ||||||
|       'isAutoBackup': isAutoBackup, |  | ||||||
|     }; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   factory DeviceInfoRemote.fromMap(Map<String, dynamic> map) { |  | ||||||
|     return DeviceInfoRemote( |  | ||||||
|       id: map['id']?.toInt() ?? 0, |  | ||||||
|       userId: map['userId'] ?? '', |  | ||||||
|       deviceId: map['deviceId'] ?? '', |  | ||||||
|       deviceType: map['deviceType'] ?? '', |  | ||||||
|       notificationToken: map['notificationToken'] ?? '', |  | ||||||
|       createdAt: map['createdAt'] ?? '', |  | ||||||
|       isAutoBackup: map['isAutoBackup'] ?? false, |  | ||||||
|     ); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   String toJson() => json.encode(toMap()); |  | ||||||
|  |  | ||||||
|   factory DeviceInfoRemote.fromJson(String source) => |  | ||||||
|       DeviceInfoRemote.fromMap(json.decode(source)); |  | ||||||
|  |  | ||||||
|   @override |  | ||||||
|   String toString() { |  | ||||||
|     return 'DeviceInfo(id: $id, userId: $userId, deviceId: $deviceId, deviceType: $deviceType, notificationToken: $notificationToken, createdAt: $createdAt, isAutoBackup: $isAutoBackup)'; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   @override |  | ||||||
|   bool operator ==(Object other) { |  | ||||||
|     if (identical(this, other)) return true; |  | ||||||
|  |  | ||||||
|     return other is DeviceInfoRemote && |  | ||||||
|         other.id == id && |  | ||||||
|         other.userId == userId && |  | ||||||
|         other.deviceId == deviceId && |  | ||||||
|         other.deviceType == deviceType && |  | ||||||
|         other.notificationToken == notificationToken && |  | ||||||
|         other.createdAt == createdAt && |  | ||||||
|         other.isAutoBackup == isAutoBackup; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   @override |  | ||||||
|   int get hashCode { |  | ||||||
|     return id.hashCode ^ |  | ||||||
|         userId.hashCode ^ |  | ||||||
|         deviceId.hashCode ^ |  | ||||||
|         deviceType.hashCode ^ |  | ||||||
|         notificationToken.hashCode ^ |  | ||||||
|         createdAt.hashCode ^ |  | ||||||
|         isAutoBackup.hashCode; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| @@ -1,212 +0,0 @@ | |||||||
| import 'dart:convert'; |  | ||||||
|  |  | ||||||
| class ImmichExif { |  | ||||||
|   final int? id; |  | ||||||
|   final String? assetId; |  | ||||||
|   final String? make; |  | ||||||
|   final String? model; |  | ||||||
|   final String? imageName; |  | ||||||
|   final int? exifImageWidth; |  | ||||||
|   final int? exifImageHeight; |  | ||||||
|   final int? fileSizeInByte; |  | ||||||
|   final String? orientation; |  | ||||||
|   final String? dateTimeOriginal; |  | ||||||
|   final String? modifyDate; |  | ||||||
|   final String? lensModel; |  | ||||||
|   final double? fNumber; |  | ||||||
|   final double? focalLength; |  | ||||||
|   final int? iso; |  | ||||||
|   final double? exposureTime; |  | ||||||
|   final double? latitude; |  | ||||||
|   final double? longitude; |  | ||||||
|   final String? city; |  | ||||||
|   final String? state; |  | ||||||
|   final String? country; |  | ||||||
|  |  | ||||||
|   ImmichExif({ |  | ||||||
|     this.id, |  | ||||||
|     this.assetId, |  | ||||||
|     this.make, |  | ||||||
|     this.model, |  | ||||||
|     this.imageName, |  | ||||||
|     this.exifImageWidth, |  | ||||||
|     this.exifImageHeight, |  | ||||||
|     this.fileSizeInByte, |  | ||||||
|     this.orientation, |  | ||||||
|     this.dateTimeOriginal, |  | ||||||
|     this.modifyDate, |  | ||||||
|     this.lensModel, |  | ||||||
|     this.fNumber, |  | ||||||
|     this.focalLength, |  | ||||||
|     this.iso, |  | ||||||
|     this.exposureTime, |  | ||||||
|     this.latitude, |  | ||||||
|     this.longitude, |  | ||||||
|     this.city, |  | ||||||
|     this.state, |  | ||||||
|     this.country, |  | ||||||
|   }); |  | ||||||
|  |  | ||||||
|   ImmichExif copyWith({ |  | ||||||
|     int? id, |  | ||||||
|     String? assetId, |  | ||||||
|     String? make, |  | ||||||
|     String? model, |  | ||||||
|     String? imageName, |  | ||||||
|     int? exifImageWidth, |  | ||||||
|     int? exifImageHeight, |  | ||||||
|     int? fileSizeInByte, |  | ||||||
|     String? orientation, |  | ||||||
|     String? dateTimeOriginal, |  | ||||||
|     String? modifyDate, |  | ||||||
|     String? lensModel, |  | ||||||
|     double? fNumber, |  | ||||||
|     double? focalLength, |  | ||||||
|     int? iso, |  | ||||||
|     double? exposureTime, |  | ||||||
|     double? latitude, |  | ||||||
|     double? longitude, |  | ||||||
|     String? city, |  | ||||||
|     String? state, |  | ||||||
|     String? country, |  | ||||||
|   }) { |  | ||||||
|     return ImmichExif( |  | ||||||
|       id: id ?? this.id, |  | ||||||
|       assetId: assetId ?? this.assetId, |  | ||||||
|       make: make ?? this.make, |  | ||||||
|       model: model ?? this.model, |  | ||||||
|       imageName: imageName ?? this.imageName, |  | ||||||
|       exifImageWidth: exifImageWidth ?? this.exifImageWidth, |  | ||||||
|       exifImageHeight: exifImageHeight ?? this.exifImageHeight, |  | ||||||
|       fileSizeInByte: fileSizeInByte ?? this.fileSizeInByte, |  | ||||||
|       orientation: orientation ?? this.orientation, |  | ||||||
|       dateTimeOriginal: dateTimeOriginal ?? this.dateTimeOriginal, |  | ||||||
|       modifyDate: modifyDate ?? this.modifyDate, |  | ||||||
|       lensModel: lensModel ?? this.lensModel, |  | ||||||
|       fNumber: fNumber ?? this.fNumber, |  | ||||||
|       focalLength: focalLength ?? this.focalLength, |  | ||||||
|       iso: iso ?? this.iso, |  | ||||||
|       exposureTime: exposureTime ?? this.exposureTime, |  | ||||||
|       latitude: latitude ?? this.latitude, |  | ||||||
|       longitude: longitude ?? this.longitude, |  | ||||||
|       city: city ?? this.city, |  | ||||||
|       state: state ?? this.state, |  | ||||||
|       country: country ?? this.country, |  | ||||||
|     ); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   Map<String, dynamic> toMap() { |  | ||||||
|     return { |  | ||||||
|       'id': id, |  | ||||||
|       'assetId': assetId, |  | ||||||
|       'make': make, |  | ||||||
|       'model': model, |  | ||||||
|       'imageName': imageName, |  | ||||||
|       'exifImageWidth': exifImageWidth, |  | ||||||
|       'exifImageHeight': exifImageHeight, |  | ||||||
|       'fileSizeInByte': fileSizeInByte, |  | ||||||
|       'orientation': orientation, |  | ||||||
|       'dateTimeOriginal': dateTimeOriginal, |  | ||||||
|       'modifyDate': modifyDate, |  | ||||||
|       'lensModel': lensModel, |  | ||||||
|       'fNumber': fNumber, |  | ||||||
|       'focalLength': focalLength, |  | ||||||
|       'iso': iso, |  | ||||||
|       'exposureTime': exposureTime, |  | ||||||
|       'latitude': latitude, |  | ||||||
|       'longitude': longitude, |  | ||||||
|       'city': city, |  | ||||||
|       'state': state, |  | ||||||
|       'country': country, |  | ||||||
|     }; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   factory ImmichExif.fromMap(Map<String, dynamic> map) { |  | ||||||
|     return ImmichExif( |  | ||||||
|       id: map['id']?.toInt(), |  | ||||||
|       assetId: map['assetId'], |  | ||||||
|       make: map['make'], |  | ||||||
|       model: map['model'], |  | ||||||
|       imageName: map['imageName'], |  | ||||||
|       exifImageWidth: map['exifImageWidth']?.toInt(), |  | ||||||
|       exifImageHeight: map['exifImageHeight']?.toInt(), |  | ||||||
|       fileSizeInByte: map['fileSizeInByte']?.toInt(), |  | ||||||
|       orientation: map['orientation'], |  | ||||||
|       dateTimeOriginal: map['dateTimeOriginal'], |  | ||||||
|       modifyDate: map['modifyDate'], |  | ||||||
|       lensModel: map['lensModel'], |  | ||||||
|       fNumber: map['fNumber']?.toDouble(), |  | ||||||
|       focalLength: map['focalLength']?.toDouble(), |  | ||||||
|       iso: map['iso']?.toInt(), |  | ||||||
|       exposureTime: map['exposureTime']?.toDouble(), |  | ||||||
|       latitude: map['latitude']?.toDouble(), |  | ||||||
|       longitude: map['longitude']?.toDouble(), |  | ||||||
|       city: map['city'], |  | ||||||
|       state: map['state'], |  | ||||||
|       country: map['country'], |  | ||||||
|     ); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   String toJson() => json.encode(toMap()); |  | ||||||
|  |  | ||||||
|   factory ImmichExif.fromJson(String source) => |  | ||||||
|       ImmichExif.fromMap(json.decode(source)); |  | ||||||
|  |  | ||||||
|   @override |  | ||||||
|   String toString() { |  | ||||||
|     return 'ImmichExif(id: $id, assetId: $assetId, make: $make, model: $model, imageName: $imageName, exifImageWidth: $exifImageWidth, exifImageHeight: $exifImageHeight, fileSizeInByte: $fileSizeInByte, orientation: $orientation, dateTimeOriginal: $dateTimeOriginal, modifyDate: $modifyDate, lensModel: $lensModel, fNumber: $fNumber, focalLength: $focalLength, iso: $iso, exposureTime: $exposureTime, latitude: $latitude, longitude: $longitude, city: $city, state: $state, country: $country)'; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   @override |  | ||||||
|   bool operator ==(Object other) { |  | ||||||
|     if (identical(this, other)) return true; |  | ||||||
|  |  | ||||||
|     return other is ImmichExif && |  | ||||||
|         other.id == id && |  | ||||||
|         other.assetId == assetId && |  | ||||||
|         other.make == make && |  | ||||||
|         other.model == model && |  | ||||||
|         other.imageName == imageName && |  | ||||||
|         other.exifImageWidth == exifImageWidth && |  | ||||||
|         other.exifImageHeight == exifImageHeight && |  | ||||||
|         other.fileSizeInByte == fileSizeInByte && |  | ||||||
|         other.orientation == orientation && |  | ||||||
|         other.dateTimeOriginal == dateTimeOriginal && |  | ||||||
|         other.modifyDate == modifyDate && |  | ||||||
|         other.lensModel == lensModel && |  | ||||||
|         other.fNumber == fNumber && |  | ||||||
|         other.focalLength == focalLength && |  | ||||||
|         other.iso == iso && |  | ||||||
|         other.exposureTime == exposureTime && |  | ||||||
|         other.latitude == latitude && |  | ||||||
|         other.longitude == longitude && |  | ||||||
|         other.city == city && |  | ||||||
|         other.state == state && |  | ||||||
|         other.country == country; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   @override |  | ||||||
|   int get hashCode { |  | ||||||
|     return id.hashCode ^ |  | ||||||
|         assetId.hashCode ^ |  | ||||||
|         make.hashCode ^ |  | ||||||
|         model.hashCode ^ |  | ||||||
|         imageName.hashCode ^ |  | ||||||
|         exifImageWidth.hashCode ^ |  | ||||||
|         exifImageHeight.hashCode ^ |  | ||||||
|         fileSizeInByte.hashCode ^ |  | ||||||
|         orientation.hashCode ^ |  | ||||||
|         dateTimeOriginal.hashCode ^ |  | ||||||
|         modifyDate.hashCode ^ |  | ||||||
|         lensModel.hashCode ^ |  | ||||||
|         fNumber.hashCode ^ |  | ||||||
|         focalLength.hashCode ^ |  | ||||||
|         iso.hashCode ^ |  | ||||||
|         exposureTime.hashCode ^ |  | ||||||
|         latitude.hashCode ^ |  | ||||||
|         longitude.hashCode ^ |  | ||||||
|         city.hashCode ^ |  | ||||||
|         state.hashCode ^ |  | ||||||
|         country.hashCode; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| @@ -1,110 +0,0 @@ | |||||||
| import 'dart:convert'; |  | ||||||
|  |  | ||||||
| import 'package:equatable/equatable.dart'; |  | ||||||
|  |  | ||||||
| class ImmichAsset extends Equatable { |  | ||||||
|   final String id; |  | ||||||
|   final String deviceAssetId; |  | ||||||
|   final String userId; |  | ||||||
|   final String deviceId; |  | ||||||
|   final String type; |  | ||||||
|   final String createdAt; |  | ||||||
|   final String modifiedAt; |  | ||||||
|   final bool isFavorite; |  | ||||||
|   final String? duration; |  | ||||||
|   final String originalPath; |  | ||||||
|   final String resizePath; |  | ||||||
|  |  | ||||||
|   const ImmichAsset({ |  | ||||||
|     required this.id, |  | ||||||
|     required this.deviceAssetId, |  | ||||||
|     required this.userId, |  | ||||||
|     required this.deviceId, |  | ||||||
|     required this.type, |  | ||||||
|     required this.createdAt, |  | ||||||
|     required this.modifiedAt, |  | ||||||
|     required this.isFavorite, |  | ||||||
|     this.duration, |  | ||||||
|     required this.originalPath, |  | ||||||
|     required this.resizePath, |  | ||||||
|   }); |  | ||||||
|  |  | ||||||
|   ImmichAsset copyWith({ |  | ||||||
|     String? id, |  | ||||||
|     String? deviceAssetId, |  | ||||||
|     String? userId, |  | ||||||
|     String? deviceId, |  | ||||||
|     String? type, |  | ||||||
|     String? createdAt, |  | ||||||
|     String? modifiedAt, |  | ||||||
|     bool? isFavorite, |  | ||||||
|     String? duration, |  | ||||||
|     String? originalPath, |  | ||||||
|     String? resizePath, |  | ||||||
|   }) { |  | ||||||
|     return ImmichAsset( |  | ||||||
|       id: id ?? this.id, |  | ||||||
|       deviceAssetId: deviceAssetId ?? this.deviceAssetId, |  | ||||||
|       userId: userId ?? this.userId, |  | ||||||
|       deviceId: deviceId ?? this.deviceId, |  | ||||||
|       type: type ?? this.type, |  | ||||||
|       createdAt: createdAt ?? this.createdAt, |  | ||||||
|       modifiedAt: modifiedAt ?? this.modifiedAt, |  | ||||||
|       isFavorite: isFavorite ?? this.isFavorite, |  | ||||||
|       duration: duration ?? this.duration, |  | ||||||
|       originalPath: originalPath ?? this.originalPath, |  | ||||||
|       resizePath: resizePath ?? this.resizePath, |  | ||||||
|     ); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   Map<String, dynamic> toMap() { |  | ||||||
|     final result = <String, dynamic>{}; |  | ||||||
|  |  | ||||||
|     result.addAll({'id': id}); |  | ||||||
|     result.addAll({'deviceAssetId': deviceAssetId}); |  | ||||||
|     result.addAll({'userId': userId}); |  | ||||||
|     result.addAll({'deviceId': deviceId}); |  | ||||||
|     result.addAll({'type': type}); |  | ||||||
|     result.addAll({'createdAt': createdAt}); |  | ||||||
|     result.addAll({'modifiedAt': modifiedAt}); |  | ||||||
|     result.addAll({'isFavorite': isFavorite}); |  | ||||||
|     if (duration != null) { |  | ||||||
|       result.addAll({'duration': duration}); |  | ||||||
|     } |  | ||||||
|     result.addAll({'originalPath': originalPath}); |  | ||||||
|     result.addAll({'resizePath': resizePath}); |  | ||||||
|  |  | ||||||
|     return result; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   factory ImmichAsset.fromMap(Map<String, dynamic> map) { |  | ||||||
|     return ImmichAsset( |  | ||||||
|       id: map['id'] ?? '', |  | ||||||
|       deviceAssetId: map['deviceAssetId'] ?? '', |  | ||||||
|       userId: map['userId'] ?? '', |  | ||||||
|       deviceId: map['deviceId'] ?? '', |  | ||||||
|       type: map['type'] ?? '', |  | ||||||
|       createdAt: map['createdAt'] ?? '', |  | ||||||
|       modifiedAt: map['modifiedAt'] ?? '', |  | ||||||
|       isFavorite: map['isFavorite'] ?? false, |  | ||||||
|       duration: map['duration'], |  | ||||||
|       originalPath: map['originalPath'] ?? '', |  | ||||||
|       resizePath: map['resizePath'] ?? '', |  | ||||||
|     ); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   String toJson() => json.encode(toMap()); |  | ||||||
|  |  | ||||||
|   factory ImmichAsset.fromJson(String source) => |  | ||||||
|       ImmichAsset.fromMap(json.decode(source)); |  | ||||||
|  |  | ||||||
|   @override |  | ||||||
|   String toString() { |  | ||||||
|     return 'ImmichAsset(id: $id, deviceAssetId: $deviceAssetId, userId: $userId, deviceId: $deviceId, type: $type, createdAt: $createdAt, modifiedAt: $modifiedAt, isFavorite: $isFavorite, duration: $duration, originalPath: $originalPath, resizePath: $resizePath)'; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   @override |  | ||||||
|   List<Object> get props { |  | ||||||
|     return [id]; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| @@ -1,135 +0,0 @@ | |||||||
| import 'dart:convert'; |  | ||||||
|  |  | ||||||
| import 'package:immich_mobile/shared/models/exif.model.dart'; |  | ||||||
|  |  | ||||||
| class ImmichAssetWithExif { |  | ||||||
|   final String id; |  | ||||||
|   final String deviceAssetId; |  | ||||||
|   final String userId; |  | ||||||
|   final String deviceId; |  | ||||||
|   final String type; |  | ||||||
|   final String createdAt; |  | ||||||
|   final String modifiedAt; |  | ||||||
|   final String originalPath; |  | ||||||
|   final bool isFavorite; |  | ||||||
|   final String? duration; |  | ||||||
|   final ImmichExif? exifInfo; |  | ||||||
|  |  | ||||||
|   ImmichAssetWithExif({ |  | ||||||
|     required this.id, |  | ||||||
|     required this.deviceAssetId, |  | ||||||
|     required this.userId, |  | ||||||
|     required this.deviceId, |  | ||||||
|     required this.type, |  | ||||||
|     required this.createdAt, |  | ||||||
|     required this.modifiedAt, |  | ||||||
|     required this.originalPath, |  | ||||||
|     required this.isFavorite, |  | ||||||
|     this.duration, |  | ||||||
|     this.exifInfo, |  | ||||||
|   }); |  | ||||||
|  |  | ||||||
|   ImmichAssetWithExif copyWith({ |  | ||||||
|     String? id, |  | ||||||
|     String? deviceAssetId, |  | ||||||
|     String? userId, |  | ||||||
|     String? deviceId, |  | ||||||
|     String? type, |  | ||||||
|     String? createdAt, |  | ||||||
|     String? modifiedAt, |  | ||||||
|     String? originalPath, |  | ||||||
|     bool? isFavorite, |  | ||||||
|     String? duration, |  | ||||||
|     ImmichExif? exifInfo, |  | ||||||
|   }) { |  | ||||||
|     return ImmichAssetWithExif( |  | ||||||
|       id: id ?? this.id, |  | ||||||
|       deviceAssetId: deviceAssetId ?? this.deviceAssetId, |  | ||||||
|       userId: userId ?? this.userId, |  | ||||||
|       deviceId: deviceId ?? this.deviceId, |  | ||||||
|       type: type ?? this.type, |  | ||||||
|       createdAt: createdAt ?? this.createdAt, |  | ||||||
|       modifiedAt: modifiedAt ?? this.modifiedAt, |  | ||||||
|       originalPath: originalPath ?? this.originalPath, |  | ||||||
|       isFavorite: isFavorite ?? this.isFavorite, |  | ||||||
|       duration: duration ?? this.duration, |  | ||||||
|       exifInfo: exifInfo ?? this.exifInfo, |  | ||||||
|     ); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   Map<String, dynamic> toMap() { |  | ||||||
|     return { |  | ||||||
|       'id': id, |  | ||||||
|       'deviceAssetId': deviceAssetId, |  | ||||||
|       'userId': userId, |  | ||||||
|       'deviceId': deviceId, |  | ||||||
|       'type': type, |  | ||||||
|       'createdAt': createdAt, |  | ||||||
|       'modifiedAt': modifiedAt, |  | ||||||
|       'originalPath': originalPath, |  | ||||||
|       'isFavorite': isFavorite, |  | ||||||
|       'duration': duration, |  | ||||||
|       'exifInfo': exifInfo?.toMap(), |  | ||||||
|     }; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   factory ImmichAssetWithExif.fromMap(Map<String, dynamic> map) { |  | ||||||
|     return ImmichAssetWithExif( |  | ||||||
|       id: map['id'] ?? '', |  | ||||||
|       deviceAssetId: map['deviceAssetId'] ?? '', |  | ||||||
|       userId: map['userId'] ?? '', |  | ||||||
|       deviceId: map['deviceId'] ?? '', |  | ||||||
|       type: map['type'] ?? '', |  | ||||||
|       createdAt: map['createdAt'] ?? '', |  | ||||||
|       modifiedAt: map['modifiedAt'] ?? '', |  | ||||||
|       originalPath: map['originalPath'] ?? '', |  | ||||||
|       isFavorite: map['isFavorite'] ?? false, |  | ||||||
|       duration: map['duration'], |  | ||||||
|       exifInfo: |  | ||||||
|           map['exifInfo'] != null ? ImmichExif.fromMap(map['exifInfo']) : null, |  | ||||||
|     ); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   String toJson() => json.encode(toMap()); |  | ||||||
|  |  | ||||||
|   factory ImmichAssetWithExif.fromJson(String source) => |  | ||||||
|       ImmichAssetWithExif.fromMap(json.decode(source)); |  | ||||||
|  |  | ||||||
|   @override |  | ||||||
|   String toString() { |  | ||||||
|     return 'ImmichAssetWithExif(id: $id, deviceAssetId: $deviceAssetId, userId: $userId, deviceId: $deviceId, type: $type, createdAt: $createdAt, modifiedAt: $modifiedAt, originalPath: $originalPath, isFavorite: $isFavorite, duration: $duration, exifInfo: $exifInfo)'; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   @override |  | ||||||
|   bool operator ==(Object other) { |  | ||||||
|     if (identical(this, other)) return true; |  | ||||||
|  |  | ||||||
|     return other is ImmichAssetWithExif && |  | ||||||
|         other.id == id && |  | ||||||
|         other.deviceAssetId == deviceAssetId && |  | ||||||
|         other.userId == userId && |  | ||||||
|         other.deviceId == deviceId && |  | ||||||
|         other.type == type && |  | ||||||
|         other.createdAt == createdAt && |  | ||||||
|         other.modifiedAt == modifiedAt && |  | ||||||
|         other.originalPath == originalPath && |  | ||||||
|         other.isFavorite == isFavorite && |  | ||||||
|         other.duration == duration && |  | ||||||
|         other.exifInfo == exifInfo; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   @override |  | ||||||
|   int get hashCode { |  | ||||||
|     return id.hashCode ^ |  | ||||||
|         deviceAssetId.hashCode ^ |  | ||||||
|         userId.hashCode ^ |  | ||||||
|         deviceId.hashCode ^ |  | ||||||
|         type.hashCode ^ |  | ||||||
|         createdAt.hashCode ^ |  | ||||||
|         modifiedAt.hashCode ^ |  | ||||||
|         originalPath.hashCode ^ |  | ||||||
|         isFavorite.hashCode ^ |  | ||||||
|         duration.hashCode ^ |  | ||||||
|         exifInfo.hashCode; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| @@ -1,55 +0,0 @@ | |||||||
| import 'dart:convert'; |  | ||||||
|  |  | ||||||
| class MapboxInfo { |  | ||||||
|   final bool isEnable; |  | ||||||
|   final String mapboxSecret; |  | ||||||
|   MapboxInfo({ |  | ||||||
|     required this.isEnable, |  | ||||||
|     required this.mapboxSecret, |  | ||||||
|   }); |  | ||||||
|  |  | ||||||
|   MapboxInfo copyWith({ |  | ||||||
|     bool? isEnable, |  | ||||||
|     String? mapboxSecret, |  | ||||||
|   }) { |  | ||||||
|     return MapboxInfo( |  | ||||||
|       isEnable: isEnable ?? this.isEnable, |  | ||||||
|       mapboxSecret: mapboxSecret ?? this.mapboxSecret, |  | ||||||
|     ); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   Map<String, dynamic> toMap() { |  | ||||||
|     return { |  | ||||||
|       'isEnable': isEnable, |  | ||||||
|       'mapboxSecret': mapboxSecret, |  | ||||||
|     }; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   factory MapboxInfo.fromMap(Map<String, dynamic> map) { |  | ||||||
|     return MapboxInfo( |  | ||||||
|       isEnable: map['isEnable'] ?? false, |  | ||||||
|       mapboxSecret: map['mapboxSecret'] ?? '', |  | ||||||
|     ); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   String toJson() => json.encode(toMap()); |  | ||||||
|  |  | ||||||
|   factory MapboxInfo.fromJson(String source) => |  | ||||||
|       MapboxInfo.fromMap(json.decode(source)); |  | ||||||
|  |  | ||||||
|   @override |  | ||||||
|   String toString() => |  | ||||||
|       'MapboxInfo(isEnable: $isEnable, mapboxSecret: $mapboxSecret)'; |  | ||||||
|  |  | ||||||
|   @override |  | ||||||
|   bool operator ==(Object other) { |  | ||||||
|     if (identical(this, other)) return true; |  | ||||||
|  |  | ||||||
|     return other is MapboxInfo && |  | ||||||
|         other.isEnable == isEnable && |  | ||||||
|         other.mapboxSecret == mapboxSecret; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   @override |  | ||||||
|   int get hashCode => isEnable.hashCode ^ mapboxSecret.hashCode; |  | ||||||
| } |  | ||||||
| @@ -1,99 +0,0 @@ | |||||||
| import 'dart:convert'; |  | ||||||
|  |  | ||||||
| class ServerInfo { |  | ||||||
|   final String diskSize; |  | ||||||
|   final String diskUse; |  | ||||||
|   final String diskAvailable; |  | ||||||
|   final int diskSizeRaw; |  | ||||||
|   final int diskUseRaw; |  | ||||||
|   final int diskAvailableRaw; |  | ||||||
|   final double diskUsagePercentage; |  | ||||||
|   ServerInfo({ |  | ||||||
|     required this.diskSize, |  | ||||||
|     required this.diskUse, |  | ||||||
|     required this.diskAvailable, |  | ||||||
|     required this.diskSizeRaw, |  | ||||||
|     required this.diskUseRaw, |  | ||||||
|     required this.diskAvailableRaw, |  | ||||||
|     required this.diskUsagePercentage, |  | ||||||
|   }); |  | ||||||
|  |  | ||||||
|   ServerInfo copyWith({ |  | ||||||
|     String? diskSize, |  | ||||||
|     String? diskUse, |  | ||||||
|     String? diskAvailable, |  | ||||||
|     int? diskSizeRaw, |  | ||||||
|     int? diskUseRaw, |  | ||||||
|     int? diskAvailableRaw, |  | ||||||
|     double? diskUsagePercentage, |  | ||||||
|   }) { |  | ||||||
|     return ServerInfo( |  | ||||||
|       diskSize: diskSize ?? this.diskSize, |  | ||||||
|       diskUse: diskUse ?? this.diskUse, |  | ||||||
|       diskAvailable: diskAvailable ?? this.diskAvailable, |  | ||||||
|       diskSizeRaw: diskSizeRaw ?? this.diskSizeRaw, |  | ||||||
|       diskUseRaw: diskUseRaw ?? this.diskUseRaw, |  | ||||||
|       diskAvailableRaw: diskAvailableRaw ?? this.diskAvailableRaw, |  | ||||||
|       diskUsagePercentage: diskUsagePercentage ?? this.diskUsagePercentage, |  | ||||||
|     ); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   Map<String, dynamic> toMap() { |  | ||||||
|     return { |  | ||||||
|       'diskSize': diskSize, |  | ||||||
|       'diskUse': diskUse, |  | ||||||
|       'diskAvailable': diskAvailable, |  | ||||||
|       'diskSizeRaw': diskSizeRaw, |  | ||||||
|       'diskUseRaw': diskUseRaw, |  | ||||||
|       'diskAvailableRaw': diskAvailableRaw, |  | ||||||
|       'diskUsagePercentage': diskUsagePercentage, |  | ||||||
|     }; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   factory ServerInfo.fromMap(Map<String, dynamic> map) { |  | ||||||
|     return ServerInfo( |  | ||||||
|       diskSize: map['diskSize'] ?? '', |  | ||||||
|       diskUse: map['diskUse'] ?? '', |  | ||||||
|       diskAvailable: map['diskAvailable'] ?? '', |  | ||||||
|       diskSizeRaw: map['diskSizeRaw']?.toInt() ?? 0, |  | ||||||
|       diskUseRaw: map['diskUseRaw']?.toInt() ?? 0, |  | ||||||
|       diskAvailableRaw: map['diskAvailableRaw']?.toInt() ?? 0, |  | ||||||
|       diskUsagePercentage: map['diskUsagePercentage']?.toDouble() ?? 0.0, |  | ||||||
|     ); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   String toJson() => json.encode(toMap()); |  | ||||||
|  |  | ||||||
|   factory ServerInfo.fromJson(String source) => |  | ||||||
|       ServerInfo.fromMap(json.decode(source)); |  | ||||||
|  |  | ||||||
|   @override |  | ||||||
|   String toString() { |  | ||||||
|     return 'ServerInfo(diskSize: $diskSize, diskUse: $diskUse, diskAvailable: $diskAvailable, diskSizeRaw: $diskSizeRaw, diskUseRaw: $diskUseRaw, diskAvailableRaw: $diskAvailableRaw, diskUsagePercentage: $diskUsagePercentage)'; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   @override |  | ||||||
|   bool operator ==(Object other) { |  | ||||||
|     if (identical(this, other)) return true; |  | ||||||
|  |  | ||||||
|     return other is ServerInfo && |  | ||||||
|         other.diskSize == diskSize && |  | ||||||
|         other.diskUse == diskUse && |  | ||||||
|         other.diskAvailable == diskAvailable && |  | ||||||
|         other.diskSizeRaw == diskSizeRaw && |  | ||||||
|         other.diskUseRaw == diskUseRaw && |  | ||||||
|         other.diskAvailableRaw == diskAvailableRaw && |  | ||||||
|         other.diskUsagePercentage == diskUsagePercentage; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   @override |  | ||||||
|   int get hashCode { |  | ||||||
|     return diskSize.hashCode ^ |  | ||||||
|         diskUse.hashCode ^ |  | ||||||
|         diskAvailable.hashCode ^ |  | ||||||
|         diskSizeRaw.hashCode ^ |  | ||||||
|         diskUseRaw.hashCode ^ |  | ||||||
|         diskAvailableRaw.hashCode ^ |  | ||||||
|         diskUsagePercentage.hashCode; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| @@ -1,29 +1,22 @@ | |||||||
| import 'dart:convert'; | import 'package:openapi/api.dart'; | ||||||
|  |  | ||||||
| import 'package:immich_mobile/shared/models/mapbox_info.model.dart'; |  | ||||||
| import 'package:immich_mobile/shared/models/server_version.model.dart'; |  | ||||||
|  |  | ||||||
| class ServerInfoState { | class ServerInfoState { | ||||||
|   final MapboxInfo mapboxInfo; |   final ServerVersionReponseDto serverVersion; | ||||||
|   final ServerVersion serverVersion; |  | ||||||
|   final bool isVersionMismatch; |   final bool isVersionMismatch; | ||||||
|   final String versionMismatchErrorMessage; |   final String versionMismatchErrorMessage; | ||||||
|  |  | ||||||
|   ServerInfoState({ |   ServerInfoState({ | ||||||
|     required this.mapboxInfo, |  | ||||||
|     required this.serverVersion, |     required this.serverVersion, | ||||||
|     required this.isVersionMismatch, |     required this.isVersionMismatch, | ||||||
|     required this.versionMismatchErrorMessage, |     required this.versionMismatchErrorMessage, | ||||||
|   }); |   }); | ||||||
|  |  | ||||||
|   ServerInfoState copyWith({ |   ServerInfoState copyWith({ | ||||||
|     MapboxInfo? mapboxInfo, |     ServerVersionReponseDto? serverVersion, | ||||||
|     ServerVersion? serverVersion, |  | ||||||
|     bool? isVersionMismatch, |     bool? isVersionMismatch, | ||||||
|     String? versionMismatchErrorMessage, |     String? versionMismatchErrorMessage, | ||||||
|   }) { |   }) { | ||||||
|     return ServerInfoState( |     return ServerInfoState( | ||||||
|       mapboxInfo: mapboxInfo ?? this.mapboxInfo, |  | ||||||
|       serverVersion: serverVersion ?? this.serverVersion, |       serverVersion: serverVersion ?? this.serverVersion, | ||||||
|       isVersionMismatch: isVersionMismatch ?? this.isVersionMismatch, |       isVersionMismatch: isVersionMismatch ?? this.isVersionMismatch, | ||||||
|       versionMismatchErrorMessage: |       versionMismatchErrorMessage: | ||||||
| @@ -31,32 +24,9 @@ class ServerInfoState { | |||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   Map<String, dynamic> toMap() { |  | ||||||
|     return { |  | ||||||
|       'mapboxInfo': mapboxInfo.toMap(), |  | ||||||
|       'serverVersion': serverVersion.toMap(), |  | ||||||
|       'isVersionMismatch': isVersionMismatch, |  | ||||||
|       'versionMismatchErrorMessage': versionMismatchErrorMessage, |  | ||||||
|     }; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   factory ServerInfoState.fromMap(Map<String, dynamic> map) { |  | ||||||
|     return ServerInfoState( |  | ||||||
|       mapboxInfo: MapboxInfo.fromMap(map['mapboxInfo']), |  | ||||||
|       serverVersion: ServerVersion.fromMap(map['serverVersion']), |  | ||||||
|       isVersionMismatch: map['isVersionMismatch'] ?? false, |  | ||||||
|       versionMismatchErrorMessage: map['versionMismatchErrorMessage'] ?? '', |  | ||||||
|     ); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   String toJson() => json.encode(toMap()); |  | ||||||
|  |  | ||||||
|   factory ServerInfoState.fromJson(String source) => |  | ||||||
|       ServerInfoState.fromMap(json.decode(source)); |  | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   String toString() { |   String toString() { | ||||||
|     return 'ServerInfoState(mapboxInfo: $mapboxInfo, serverVersion: $serverVersion, isVersionMismatch: $isVersionMismatch, versionMismatchErrorMessage: $versionMismatchErrorMessage)'; |     return 'ServerInfoState( serverVersion: $serverVersion, isVersionMismatch: $isVersionMismatch, versionMismatchErrorMessage: $versionMismatchErrorMessage)'; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
| @@ -64,7 +34,6 @@ class ServerInfoState { | |||||||
|     if (identical(this, other)) return true; |     if (identical(this, other)) return true; | ||||||
|  |  | ||||||
|     return other is ServerInfoState && |     return other is ServerInfoState && | ||||||
|         other.mapboxInfo == mapboxInfo && |  | ||||||
|         other.serverVersion == serverVersion && |         other.serverVersion == serverVersion && | ||||||
|         other.isVersionMismatch == isVersionMismatch && |         other.isVersionMismatch == isVersionMismatch && | ||||||
|         other.versionMismatchErrorMessage == versionMismatchErrorMessage; |         other.versionMismatchErrorMessage == versionMismatchErrorMessage; | ||||||
| @@ -72,8 +41,7 @@ class ServerInfoState { | |||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   int get hashCode { |   int get hashCode { | ||||||
|     return mapboxInfo.hashCode ^ |     return serverVersion.hashCode ^ | ||||||
|         serverVersion.hashCode ^ |  | ||||||
|         isVersionMismatch.hashCode ^ |         isVersionMismatch.hashCode ^ | ||||||
|         versionMismatchErrorMessage.hashCode; |         versionMismatchErrorMessage.hashCode; | ||||||
|   } |   } | ||||||
|   | |||||||
| @@ -1,73 +0,0 @@ | |||||||
| import 'dart:convert'; |  | ||||||
|  |  | ||||||
| class ServerVersion { |  | ||||||
|   final int major; |  | ||||||
|   final int minor; |  | ||||||
|   final int patch; |  | ||||||
|   final int build; |  | ||||||
|  |  | ||||||
|   ServerVersion({ |  | ||||||
|     required this.major, |  | ||||||
|     required this.minor, |  | ||||||
|     required this.patch, |  | ||||||
|     required this.build, |  | ||||||
|   }); |  | ||||||
|  |  | ||||||
|   ServerVersion copyWith({ |  | ||||||
|     int? major, |  | ||||||
|     int? minor, |  | ||||||
|     int? patch, |  | ||||||
|     int? build, |  | ||||||
|   }) { |  | ||||||
|     return ServerVersion( |  | ||||||
|       major: major ?? this.major, |  | ||||||
|       minor: minor ?? this.minor, |  | ||||||
|       patch: patch ?? this.patch, |  | ||||||
|       build: build ?? this.build, |  | ||||||
|     ); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   Map<String, dynamic> toMap() { |  | ||||||
|     return { |  | ||||||
|       'major': major, |  | ||||||
|       'minor': minor, |  | ||||||
|       'patch': patch, |  | ||||||
|       'build': build, |  | ||||||
|     }; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   factory ServerVersion.fromMap(Map<String, dynamic> map) { |  | ||||||
|     return ServerVersion( |  | ||||||
|       major: map['major']?.toInt() ?? 0, |  | ||||||
|       minor: map['minor']?.toInt() ?? 0, |  | ||||||
|       patch: map['patch']?.toInt() ?? 0, |  | ||||||
|       build: map['build']?.toInt() ?? 0, |  | ||||||
|     ); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   String toJson() => json.encode(toMap()); |  | ||||||
|  |  | ||||||
|   factory ServerVersion.fromJson(String source) => |  | ||||||
|       ServerVersion.fromMap(json.decode(source)); |  | ||||||
|  |  | ||||||
|   @override |  | ||||||
|   String toString() { |  | ||||||
|     return 'ServerVersion(major: $major, minor: $minor, patch: $patch, build: $build)'; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   @override |  | ||||||
|   bool operator ==(Object other) { |  | ||||||
|     if (identical(this, other)) return true; |  | ||||||
|  |  | ||||||
|     return other is ServerVersion && |  | ||||||
|         other.major == major && |  | ||||||
|         other.minor == minor && |  | ||||||
|         other.patch == patch && |  | ||||||
|         other.build == build; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   @override |  | ||||||
|   int get hashCode { |  | ||||||
|     return major.hashCode ^ minor.hashCode ^ patch.hashCode ^ build.hashCode; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| @@ -1,76 +0,0 @@ | |||||||
| import 'dart:convert'; |  | ||||||
|  |  | ||||||
| class User { |  | ||||||
|   final String id; |  | ||||||
|   final String email; |  | ||||||
|   final String createdAt; |  | ||||||
|   final String firstName; |  | ||||||
|   final String lastName; |  | ||||||
|  |  | ||||||
|   User({ |  | ||||||
|     required this.id, |  | ||||||
|     required this.email, |  | ||||||
|     required this.createdAt, |  | ||||||
|     required this.firstName, |  | ||||||
|     required this.lastName, |  | ||||||
|   }); |  | ||||||
|  |  | ||||||
|   User copyWith({ |  | ||||||
|     String? id, |  | ||||||
|     String? email, |  | ||||||
|     String? createdAt, |  | ||||||
|     String? firstName, |  | ||||||
|     String? lastName, |  | ||||||
|   }) { |  | ||||||
|     return User( |  | ||||||
|       id: id ?? this.id, |  | ||||||
|       email: email ?? this.email, |  | ||||||
|       createdAt: createdAt ?? this.createdAt, |  | ||||||
|       firstName: firstName ?? this.firstName, |  | ||||||
|       lastName: lastName ?? this.lastName, |  | ||||||
|     ); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   Map<String, dynamic> toMap() { |  | ||||||
|     final result = <String, dynamic>{}; |  | ||||||
|  |  | ||||||
|     result.addAll({'id': id}); |  | ||||||
|     result.addAll({'email': email}); |  | ||||||
|     result.addAll({'createdAt': createdAt}); |  | ||||||
|  |  | ||||||
|     return result; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   factory User.fromMap(Map<String, dynamic> map) { |  | ||||||
|     return User( |  | ||||||
|       id: map['id'] ?? '', |  | ||||||
|       email: map['email'] ?? '', |  | ||||||
|       createdAt: map['createdAt'] ?? '', |  | ||||||
|       firstName: map['firstName'] ?? '', |  | ||||||
|       lastName: map['lastName'] ?? '', |  | ||||||
|     ); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   String toJson() => json.encode(toMap()); |  | ||||||
|  |  | ||||||
|   factory User.fromJson(String source) => User.fromMap(json.decode(source)); |  | ||||||
|  |  | ||||||
|   @override |  | ||||||
|   String toString() => |  | ||||||
|       'UserInfo(id: $id, email: $email, createdAt: $createdAt)'; |  | ||||||
|  |  | ||||||
|   @override |  | ||||||
|   bool operator ==(Object other) { |  | ||||||
|     if (identical(this, other)) return true; |  | ||||||
|  |  | ||||||
|     return other is User && |  | ||||||
|         other.id == id && |  | ||||||
|         other.email == email && |  | ||||||
|         other.createdAt == createdAt && |  | ||||||
|         other.firstName == firstName && |  | ||||||
|         other.lastName == lastName; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   @override |  | ||||||
|   int get hashCode => id.hashCode ^ email.hashCode ^ createdAt.hashCode; |  | ||||||
| } |  | ||||||
| @@ -1,21 +1,20 @@ | |||||||
| import 'package:flutter/foundation.dart'; | import 'package:flutter/foundation.dart'; | ||||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||||
| import 'package:immich_mobile/modules/home/models/delete_asset_response.model.dart'; |  | ||||||
| import 'package:immich_mobile/modules/home/services/asset.service.dart'; | import 'package:immich_mobile/modules/home/services/asset.service.dart'; | ||||||
| import 'package:immich_mobile/shared/models/immich_asset.model.dart'; |  | ||||||
| import 'package:immich_mobile/shared/services/device_info.service.dart'; | import 'package:immich_mobile/shared/services/device_info.service.dart'; | ||||||
| import 'package:collection/collection.dart'; | import 'package:collection/collection.dart'; | ||||||
| import 'package:intl/intl.dart'; | import 'package:intl/intl.dart'; | ||||||
|  | import 'package:openapi/api.dart'; | ||||||
| import 'package:photo_manager/photo_manager.dart'; | import 'package:photo_manager/photo_manager.dart'; | ||||||
|  |  | ||||||
| class AssetNotifier extends StateNotifier<List<ImmichAsset>> { | class AssetNotifier extends StateNotifier<List<AssetResponseDto>> { | ||||||
|   final AssetService _assetService; |   final AssetService _assetService; | ||||||
|   final DeviceInfoService _deviceInfoService = DeviceInfoService(); |   final DeviceInfoService _deviceInfoService = DeviceInfoService(); | ||||||
|  |  | ||||||
|   AssetNotifier(this._assetService) : super([]); |   AssetNotifier(this._assetService) : super([]); | ||||||
|  |  | ||||||
|   getAllAsset() async { |   getAllAsset() async { | ||||||
|     List<ImmichAsset>? allAssets = await _assetService.getAllAsset(); |     var allAssets = await _assetService.getAllAsset(); | ||||||
|  |  | ||||||
|     if (allAssets != null) { |     if (allAssets != null) { | ||||||
|       state = allAssets; |       state = allAssets; | ||||||
| @@ -26,11 +25,11 @@ class AssetNotifier extends StateNotifier<List<ImmichAsset>> { | |||||||
|     state = []; |     state = []; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   onNewAssetUploaded(ImmichAsset newAsset) { |   onNewAssetUploaded(AssetResponseDto newAsset) { | ||||||
|     state = [...state, newAsset]; |     state = [...state, newAsset]; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   deleteAssets(Set<ImmichAsset> deleteAssets) async { |   deleteAssets(Set<AssetResponseDto> deleteAssets) async { | ||||||
|     var deviceInfo = await _deviceInfoService.getDeviceInfo(); |     var deviceInfo = await _deviceInfoService.getDeviceInfo(); | ||||||
|     var deviceId = deviceInfo["deviceId"]; |     var deviceId = deviceInfo["deviceId"]; | ||||||
|     var deleteIdList = <String>[]; |     var deleteIdList = <String>[]; | ||||||
| @@ -53,14 +52,15 @@ class AssetNotifier extends StateNotifier<List<ImmichAsset>> { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     // Delete asset on server |     // Delete asset on server | ||||||
|     List<DeleteAssetResponse>? deleteAssetResult = |     List<DeleteAssetResponseDto>? deleteAssetResult = | ||||||
|         await _assetService.deleteAssets(deleteAssets); |         await _assetService.deleteAssets(deleteAssets); | ||||||
|  |  | ||||||
|     if (deleteAssetResult == null) { |     if (deleteAssetResult == null) { | ||||||
|       return; |       return; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     for (var asset in deleteAssetResult) { |     for (var asset in deleteAssetResult) { | ||||||
|       if (asset.status == 'success') { |       if (asset.status == DeleteAssetStatus.SUCCESS) { | ||||||
|         state = |         state = | ||||||
|             state.where((immichAsset) => immichAsset.id != asset.id).toList(); |             state.where((immichAsset) => immichAsset.id != asset.id).toList(); | ||||||
|       } |       } | ||||||
| @@ -69,7 +69,7 @@ class AssetNotifier extends StateNotifier<List<ImmichAsset>> { | |||||||
| } | } | ||||||
|  |  | ||||||
| final assetProvider = | final assetProvider = | ||||||
|     StateNotifierProvider<AssetNotifier, List<ImmichAsset>>((ref) { |     StateNotifierProvider<AssetNotifier, List<AssetResponseDto>>((ref) { | ||||||
|   return AssetNotifier(ref.watch(assetServiceProvider)); |   return AssetNotifier(ref.watch(assetServiceProvider)); | ||||||
| }); | }); | ||||||
|  |  | ||||||
| @@ -77,17 +77,25 @@ final assetGroupByDateTimeProvider = StateProvider((ref) { | |||||||
|   var assets = ref.watch(assetProvider); |   var assets = ref.watch(assetProvider); | ||||||
|  |  | ||||||
|   assets.sortByCompare<DateTime>( |   assets.sortByCompare<DateTime>( | ||||||
|       (e) => DateTime.parse(e.createdAt), (a, b) => b.compareTo(a)); |     (e) => DateTime.parse(e.createdAt), | ||||||
|   return assets.groupListsBy((element) => |     (a, b) => b.compareTo(a), | ||||||
|       DateFormat('y-MM-dd').format(DateTime.parse(element.createdAt))); |   ); | ||||||
|  |   return assets.groupListsBy( | ||||||
|  |     (element) => | ||||||
|  |         DateFormat('y-MM-dd').format(DateTime.parse(element.createdAt)), | ||||||
|  |   ); | ||||||
| }); | }); | ||||||
|  |  | ||||||
| final assetGroupByMonthYearProvider = StateProvider((ref) { | final assetGroupByMonthYearProvider = StateProvider((ref) { | ||||||
|   var assets = ref.watch(assetProvider); |   var assets = ref.watch(assetProvider); | ||||||
|  |  | ||||||
|   assets.sortByCompare<DateTime>( |   assets.sortByCompare<DateTime>( | ||||||
|       (e) => DateTime.parse(e.createdAt), (a, b) => b.compareTo(a)); |     (e) => DateTime.parse(e.createdAt), | ||||||
|  |     (a, b) => b.compareTo(a), | ||||||
|  |   ); | ||||||
|  |  | ||||||
|   return assets.groupListsBy((element) => |   return assets.groupListsBy( | ||||||
|       DateFormat('MMMM, y').format(DateTime.parse(element.createdAt))); |     (element) => | ||||||
|  |         DateFormat('MMMM, y').format(DateTime.parse(element.createdAt)), | ||||||
|  |   ); | ||||||
| }); | }); | ||||||
|   | |||||||
| @@ -56,4 +56,5 @@ class ReleaseInfoNotifier extends StateNotifier<String> { | |||||||
| } | } | ||||||
|  |  | ||||||
| final releaseInfoProvider = StateNotifierProvider<ReleaseInfoNotifier, String>( | final releaseInfoProvider = StateNotifierProvider<ReleaseInfoNotifier, String>( | ||||||
|     (ref) => ReleaseInfoNotifier()); |   (ref) => ReleaseInfoNotifier(), | ||||||
|  | ); | ||||||
|   | |||||||
| @@ -1,18 +1,20 @@ | |||||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||||
|  |  | ||||||
| import 'package:immich_mobile/shared/models/mapbox_info.model.dart'; |  | ||||||
| import 'package:immich_mobile/shared/models/server_info_state.model.dart'; | import 'package:immich_mobile/shared/models/server_info_state.model.dart'; | ||||||
| import 'package:immich_mobile/shared/models/server_version.model.dart'; |  | ||||||
| import 'package:immich_mobile/shared/services/server_info.service.dart'; | import 'package:immich_mobile/shared/services/server_info.service.dart'; | ||||||
|  | import 'package:openapi/api.dart'; | ||||||
| import 'package:package_info_plus/package_info_plus.dart'; | import 'package:package_info_plus/package_info_plus.dart'; | ||||||
|  |  | ||||||
| class ServerInfoNotifier extends StateNotifier<ServerInfoState> { | class ServerInfoNotifier extends StateNotifier<ServerInfoState> { | ||||||
|   ServerInfoNotifier(this._serverInfoService) |   ServerInfoNotifier(this._serverInfoService) | ||||||
|       : super( |       : super( | ||||||
|           ServerInfoState( |           ServerInfoState( | ||||||
|             mapboxInfo: MapboxInfo(isEnable: false, mapboxSecret: ""), |             serverVersion: ServerVersionReponseDto( | ||||||
|             serverVersion: |               major: 0, | ||||||
|                 ServerVersion(major: 0, patch: 0, minor: 0, build: 0), |               patch_: 0, | ||||||
|  |               minor: 0, | ||||||
|  |               build: 0, | ||||||
|  |             ), | ||||||
|             isVersionMismatch: false, |             isVersionMismatch: false, | ||||||
|             versionMismatchErrorMessage: "", |             versionMismatchErrorMessage: "", | ||||||
|           ), |           ), | ||||||
| @@ -21,7 +23,8 @@ class ServerInfoNotifier extends StateNotifier<ServerInfoState> { | |||||||
|   final ServerInfoService _serverInfoService; |   final ServerInfoService _serverInfoService; | ||||||
|  |  | ||||||
|   getServerVersion() async { |   getServerVersion() async { | ||||||
|     ServerVersion? serverVersion = await _serverInfoService.getServerVersion(); |     ServerVersionReponseDto? serverVersion = | ||||||
|  |         await _serverInfoService.getServerVersion(); | ||||||
|  |  | ||||||
|     if (serverVersion == null) { |     if (serverVersion == null) { | ||||||
|       state = state.copyWith( |       state = state.copyWith( | ||||||
| @@ -59,7 +62,9 @@ class ServerInfoNotifier extends StateNotifier<ServerInfoState> { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     state = state.copyWith( |     state = state.copyWith( | ||||||
|         isVersionMismatch: false, versionMismatchErrorMessage: ""); |       isVersionMismatch: false, | ||||||
|  |       versionMismatchErrorMessage: "", | ||||||
|  |     ); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   Map<String, int> _getDetailVersion(String version) { |   Map<String, int> _getDetailVersion(String version) { | ||||||
|   | |||||||
| @@ -5,8 +5,8 @@ import 'package:hive/hive.dart'; | |||||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||||
| import 'package:immich_mobile/constants/hive_box.dart'; | import 'package:immich_mobile/constants/hive_box.dart'; | ||||||
| import 'package:immich_mobile/modules/login/providers/authentication.provider.dart'; | import 'package:immich_mobile/modules/login/providers/authentication.provider.dart'; | ||||||
| import 'package:immich_mobile/shared/models/immich_asset.model.dart'; |  | ||||||
| import 'package:immich_mobile/shared/providers/asset.provider.dart'; | import 'package:immich_mobile/shared/providers/asset.provider.dart'; | ||||||
|  | import 'package:openapi/api.dart'; | ||||||
| import 'package:socket_io_client/socket_io_client.dart'; | import 'package:socket_io_client/socket_io_client.dart'; | ||||||
|  |  | ||||||
| class WebscoketState { | class WebscoketState { | ||||||
| @@ -92,8 +92,11 @@ class WebsocketNotifier extends StateNotifier<WebscoketState> { | |||||||
|  |  | ||||||
|         socket.on('on_upload_success', (data) { |         socket.on('on_upload_success', (data) { | ||||||
|           var jsonString = jsonDecode(data.toString()); |           var jsonString = jsonDecode(data.toString()); | ||||||
|           ImmichAsset newAsset = ImmichAsset.fromMap(jsonString); |           AssetResponseDto? newAsset = AssetResponseDto.fromJson(jsonString); | ||||||
|           ref.watch(assetProvider.notifier).onNewAssetUploaded(newAsset); |  | ||||||
|  |           if (newAsset != null) { | ||||||
|  |             ref.watch(assetProvider.notifier).onNewAssetUploaded(newAsset); | ||||||
|  |           } | ||||||
|         }); |         }); | ||||||
|       } catch (e) { |       } catch (e) { | ||||||
|         debugPrint("[WEBSOCKET] Catch Websocket Error - ${e.toString()}"); |         debugPrint("[WEBSOCKET] Catch Websocket Error - ${e.toString()}"); | ||||||
| @@ -119,8 +122,11 @@ class WebsocketNotifier extends StateNotifier<WebscoketState> { | |||||||
|     debugPrint("[Websocket] Start listening to event on_upload_success"); |     debugPrint("[Websocket] Start listening to event on_upload_success"); | ||||||
|     state.socket?.on('on_upload_success', (data) { |     state.socket?.on('on_upload_success', (data) { | ||||||
|       var jsonString = jsonDecode(data.toString()); |       var jsonString = jsonDecode(data.toString()); | ||||||
|       ImmichAsset newAsset = ImmichAsset.fromMap(jsonString); |       AssetResponseDto? newAsset = AssetResponseDto.fromJson(jsonString); | ||||||
|       ref.watch(assetProvider.notifier).onNewAssetUploaded(newAsset); |  | ||||||
|  |       if (newAsset != null) { | ||||||
|  |         ref.watch(assetProvider.notifier).onNewAssetUploaded(newAsset); | ||||||
|  |       } | ||||||
|     }); |     }); | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
							
								
								
									
										30
									
								
								mobile/lib/shared/services/api.service.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								mobile/lib/shared/services/api.service.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,30 @@ | |||||||
|  | import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||||
|  | import 'package:openapi/api.dart'; | ||||||
|  |  | ||||||
|  | final apiServiceProvider = Provider((ref) => ApiService()); | ||||||
|  |  | ||||||
|  | class ApiService { | ||||||
|  |   late ApiClient _apiClient; | ||||||
|  |  | ||||||
|  |   late UserApi userApi; | ||||||
|  |   late AuthenticationApi authenticationApi; | ||||||
|  |   late AlbumApi albumApi; | ||||||
|  |   late AssetApi assetApi; | ||||||
|  |   late ServerInfoApi serverInfoApi; | ||||||
|  |   late DeviceInfoApi deviceInfoApi; | ||||||
|  |  | ||||||
|  |   setEndpoint(String endpoint) { | ||||||
|  |     _apiClient = ApiClient(basePath: endpoint); | ||||||
|  |  | ||||||
|  |     userApi = UserApi(_apiClient); | ||||||
|  |     authenticationApi = AuthenticationApi(_apiClient); | ||||||
|  |     albumApi = AlbumApi(_apiClient); | ||||||
|  |     assetApi = AssetApi(_apiClient); | ||||||
|  |     serverInfoApi = ServerInfoApi(_apiClient); | ||||||
|  |     deviceInfoApi = DeviceInfoApi(_apiClient); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   setAccessToken(String accessToken) { | ||||||
|  |     _apiClient.addDefaultHeader('Authorization', 'bearer $accessToken'); | ||||||
|  |   } | ||||||
|  | } | ||||||
| @@ -2,6 +2,7 @@ import 'package:flutter_udid/flutter_udid.dart'; | |||||||
| import 'dart:io' show Platform; | import 'dart:io' show Platform; | ||||||
|  |  | ||||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||||
|  | import 'package:openapi/api.dart'; | ||||||
|  |  | ||||||
| final deviceInfoServiceProvider = Provider((_) => DeviceInfoService()); | final deviceInfoServiceProvider = Provider((_) => DeviceInfoService()); | ||||||
|  |  | ||||||
| @@ -9,12 +10,12 @@ class DeviceInfoService { | |||||||
|   Future<Map<String, dynamic>> getDeviceInfo() async { |   Future<Map<String, dynamic>> getDeviceInfo() async { | ||||||
|     // Get device info |     // Get device info | ||||||
|     var deviceId = await FlutterUdid.consistentUdid; |     var deviceId = await FlutterUdid.consistentUdid; | ||||||
|     var deviceType = ""; |     var deviceType = DeviceTypeEnum.ANDROID; | ||||||
|  |  | ||||||
|     if (Platform.isAndroid) { |     if (Platform.isAndroid) { | ||||||
|       deviceType = "ANDROID"; |       deviceType = DeviceTypeEnum.ANDROID; | ||||||
|     } else if (Platform.isIOS) { |     } else if (Platform.isIOS) { | ||||||
|       deviceType = "IOS"; |       deviceType = DeviceTypeEnum.IOS; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     return {"deviceId": deviceId, "deviceType": deviceType}; |     return {"deviceId": deviceId, "deviceType": deviceType}; | ||||||
|   | |||||||
| @@ -1,21 +0,0 @@ | |||||||
| import 'package:hive/hive.dart'; |  | ||||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; |  | ||||||
| import 'package:immich_mobile/constants/hive_box.dart'; |  | ||||||
|  |  | ||||||
| final localStorageServiceProvider = Provider((_) => LocalStorageService()); |  | ||||||
|  |  | ||||||
| class LocalStorageService { |  | ||||||
|   late Box _box; |  | ||||||
|  |  | ||||||
|   LocalStorageService() { |  | ||||||
|     _box = Hive.box(userInfoBox); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   T get<T>(String key) { |  | ||||||
|     return _box.get(key); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   put<T>(String key, T value) { |  | ||||||
|     return _box.put(key, value); |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| @@ -33,10 +33,11 @@ class NetworkService { | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   Future<dynamic> getRequest( |   Future<dynamic> getRequest({ | ||||||
|       {required String url, |     required String url, | ||||||
|       bool isByteResponse = false, |     bool isByteResponse = false, | ||||||
|       bool isStreamReponse = false}) async { |     bool isStreamReponse = false, | ||||||
|  |   }) async { | ||||||
|     try { |     try { | ||||||
|       var savedEndpoint = Hive.box(userInfoBox).get(serverEndpointKey); |       var savedEndpoint = Hive.box(userInfoBox).get(serverEndpointKey); | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,33 +1,33 @@ | |||||||
| import 'package:dio/dio.dart'; |  | ||||||
| import 'package:flutter/material.dart'; | import 'package:flutter/material.dart'; | ||||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||||
| import 'package:immich_mobile/shared/models/server_info.model.dart'; | import 'package:immich_mobile/shared/services/api.service.dart'; | ||||||
| import 'package:immich_mobile/shared/models/server_version.model.dart'; | import 'package:openapi/api.dart'; | ||||||
| import 'package:immich_mobile/shared/services/network.service.dart'; |  | ||||||
|  |  | ||||||
| final serverInfoServiceProvider = | final serverInfoServiceProvider = Provider( | ||||||
|     Provider((ref) => ServerInfoService(ref.watch(networkServiceProvider))); |   (ref) => ServerInfoService( | ||||||
|  |     ref.watch(apiServiceProvider), | ||||||
|  |   ), | ||||||
|  | ); | ||||||
|  |  | ||||||
| class ServerInfoService { | class ServerInfoService { | ||||||
|   final NetworkService _networkService; |   final ApiService _apiService; | ||||||
|   ServerInfoService(this._networkService); |   ServerInfoService(this._apiService); | ||||||
|  |  | ||||||
|   Future<ServerInfo> getServerInfo() async { |   Future<ServerInfoResponseDto?> getServerInfo() async { | ||||||
|     Response response = await _networkService.getRequest(url: 'server-info'); |     try { | ||||||
|  |       return await _apiService.serverInfoApi.getServerInfo(); | ||||||
|     return ServerInfo.fromJson(response.toString()); |     } catch (e) { | ||||||
|  |       debugPrint("Error [getServerInfo] ${e.toString()}"); | ||||||
|  |       return null; | ||||||
|  |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   Future<ServerVersion?> getServerVersion() async { |   Future<ServerVersionReponseDto?> getServerVersion() async { | ||||||
|     try { |     try { | ||||||
|       Response response = |       return await _apiService.serverInfoApi.getServerVersion(); | ||||||
|           await _networkService.getRequest(url: 'server-info/version'); |  | ||||||
|  |  | ||||||
|       return ServerVersion.fromJson(response.toString()); |  | ||||||
|     } catch (e) { |     } catch (e) { | ||||||
|       debugPrint("Error getting server info"); |       debugPrint("Error getting server info"); | ||||||
|  |       return null; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     return null; |  | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,70 +1,49 @@ | |||||||
| import 'dart:convert'; |  | ||||||
|  |  | ||||||
| import 'package:dio/dio.dart'; |  | ||||||
| import 'package:flutter/material.dart'; | import 'package:flutter/material.dart'; | ||||||
| import 'package:hive/hive.dart'; |  | ||||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||||
|  | import 'package:http/http.dart'; | ||||||
| import 'package:http_parser/http_parser.dart'; | import 'package:http_parser/http_parser.dart'; | ||||||
| import 'package:image_picker/image_picker.dart'; | import 'package:image_picker/image_picker.dart'; | ||||||
| import 'package:immich_mobile/constants/hive_box.dart'; | import 'package:immich_mobile/shared/services/api.service.dart'; | ||||||
| import 'package:immich_mobile/shared/models/upload_profile_image_repsonse.model.dart'; |  | ||||||
| import 'package:immich_mobile/shared/models/user.model.dart'; |  | ||||||
| import 'package:immich_mobile/shared/services/network.service.dart'; |  | ||||||
| import 'package:immich_mobile/utils/dio_http_interceptor.dart'; |  | ||||||
| import 'package:immich_mobile/utils/files_helper.dart'; | import 'package:immich_mobile/utils/files_helper.dart'; | ||||||
|  | import 'package:openapi/api.dart'; | ||||||
|  |  | ||||||
| final userServiceProvider = | final userServiceProvider = Provider( | ||||||
|     Provider((ref) => UserService(ref.watch(networkServiceProvider))); |   (ref) => UserService( | ||||||
|  |     ref.watch(apiServiceProvider), | ||||||
|  |   ), | ||||||
|  | ); | ||||||
|  |  | ||||||
| class UserService { | class UserService { | ||||||
|   final NetworkService _networkService; |   final ApiService _apiService; | ||||||
|   UserService(this._networkService); |  | ||||||
|  |  | ||||||
|   Future<List<User>> getAllUsersInfo() async { |   UserService(this._apiService); | ||||||
|  |  | ||||||
|  |   Future<List<UserResponseDto>?> getAllUsersInfo({required bool isAll}) async { | ||||||
|     try { |     try { | ||||||
|       var res = await _networkService.getRequest(url: 'user'); |       return await _apiService.userApi.getAllUsers(isAll); | ||||||
|       List<dynamic> decodedData = jsonDecode(res.toString()); |  | ||||||
|       List<User> result = List.from(decodedData.map((e) => User.fromMap(e))); |  | ||||||
|  |  | ||||||
|       return result; |  | ||||||
|     } catch (e) { |     } catch (e) { | ||||||
|       debugPrint("Error getAllUsersInfo  ${e.toString()}"); |       debugPrint("Error [getAllUsersInfo]  ${e.toString()}"); | ||||||
|  |       return null; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     return []; |  | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   Future<UploadProfileImageResponse?> uploadProfileImage(XFile image) async { |   Future<CreateProfileImageResponseDto?> uploadProfileImage(XFile image) async { | ||||||
|     var dio = Dio(); |  | ||||||
|     dio.interceptors.add(AuthenticatedRequestInterceptor()); |  | ||||||
|     String savedEndpoint = Hive.box(userInfoBox).get(serverEndpointKey); |  | ||||||
|     var mimeType = FileHelper.getMimeType(image.path); |  | ||||||
|  |  | ||||||
|     final imageData = MultipartFile.fromBytes( |  | ||||||
|       await image.readAsBytes(), |  | ||||||
|       filename: image.name, |  | ||||||
|       contentType: MediaType( |  | ||||||
|         mimeType["type"], |  | ||||||
|         mimeType["subType"], |  | ||||||
|       ), |  | ||||||
|     ); |  | ||||||
|  |  | ||||||
|     final formData = FormData.fromMap({'file': imageData}); |  | ||||||
|  |  | ||||||
|     try { |     try { | ||||||
|       Response res = await dio.post( |       var mimeType = FileHelper.getMimeType(image.path); | ||||||
|         '$savedEndpoint/user/profile-image', |  | ||||||
|         data: formData, |       return await _apiService.userApi.createProfileImage( | ||||||
|  |         MultipartFile.fromBytes( | ||||||
|  |           'file', | ||||||
|  |           await image.readAsBytes(), | ||||||
|  |           filename: image.name, | ||||||
|  |           contentType: MediaType( | ||||||
|  |             mimeType["type"], | ||||||
|  |             mimeType["subType"], | ||||||
|  |           ), | ||||||
|  |         ), | ||||||
|       ); |       ); | ||||||
|  |  | ||||||
|       var payload = UploadProfileImageResponse.fromJson(res.toString()); |  | ||||||
|  |  | ||||||
|       return payload; |  | ||||||
|     } on DioError catch (e) { |  | ||||||
|       debugPrint("Error uploading file: ${e.response}"); |  | ||||||
|       return null; |  | ||||||
|     } catch (e) { |     } catch (e) { | ||||||
|       debugPrint("Error uploading file: $e"); |       debugPrint("Error [uploadProfileImage] ${e.toString()}"); | ||||||
|       return null; |       return null; | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|   | |||||||
| @@ -22,7 +22,10 @@ class ImmichSliverPersistentAppBarDelegate | |||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   Widget build( |   Widget build( | ||||||
|       BuildContext context, double shrinkOffset, bool overlapsContent) { |     BuildContext context, | ||||||
|  |     double shrinkOffset, | ||||||
|  |     bool overlapsContent, | ||||||
|  |   ) { | ||||||
|     return SizedBox.expand(child: child); |     return SizedBox.expand(child: child); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   | |||||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user