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 | ||||
| ``` | ||||
|  | ||||
| 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 | ||||
|  | ||||
| @@ -225,6 +225,15 @@ make dev # required Makefile installed on the system. | ||||
|  | ||||
| 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 | ||||
|  | ||||
| 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 | ||||
|   # `// ignore_for_file: name_of_lint` syntax on the line or in the file | ||||
|   # producing the lint. | ||||
|  | ||||
|   rules: | ||||
|     # avoid_print: false  # Uncomment to disable the `avoid_print` rule | ||||
|     # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule | ||||
|     use_build_context_synchronously: false | ||||
|     require_trailing_commas: true | ||||
|     unrelated_type_equality_checks: true | ||||
|  | ||||
| # Additional information about this file can be found at | ||||
| # 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_password_hint": "password", | ||||
|   "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", | ||||
|   "profile_drawer_client_server_up_to_date": "Client and Server are up-to-date", | ||||
|   "profile_drawer_sign_out": "Sign Out", | ||||
| @@ -81,6 +82,7 @@ | ||||
|   "search_result_page_new_search_hint": "New Search", | ||||
|   "select_additional_user_for_sharing_page_suggestions": "Suggestions", | ||||
|   "select_user_for_sharing_page_err_album": "Failed to create album", | ||||
|   "select_user_for_sharing_page_share_suggestions": "Suggestions", | ||||
|   "share_add": "Add", | ||||
|   "share_add_photos": "Add photos", | ||||
|   "share_add_title": "Add a title", | ||||
|   | ||||
| @@ -46,12 +46,15 @@ void main() async { | ||||
|     Locale('de', 'DE') | ||||
|   ]; | ||||
|  | ||||
|   runApp(EasyLocalization( | ||||
|   runApp( | ||||
|     EasyLocalization( | ||||
|       supportedLocales: locales, | ||||
|       path: 'assets/i18n', | ||||
|       useFallbackTranslations: true, | ||||
|       fallbackLocale: locales.first, | ||||
|       child: const ProviderScope(child: ImmichApp()))); | ||||
|       child: const ProviderScope(child: ImmichApp()), | ||||
|     ), | ||||
|   ); | ||||
| } | ||||
|  | ||||
| class ImmichApp extends ConsumerStatefulWidget { | ||||
| @@ -111,6 +114,7 @@ class ImmichAppState extends ConsumerState<ImmichApp> | ||||
|   @override | ||||
|   initState() { | ||||
|     super.initState(); | ||||
|  | ||||
|     initApp().then((_) => debugPrint("App Init Completed")); | ||||
|   } | ||||
|  | ||||
| @@ -120,10 +124,9 @@ class ImmichAppState extends ConsumerState<ImmichApp> | ||||
|     super.dispose(); | ||||
|   } | ||||
|  | ||||
|   final _immichRouter = AppRouter(); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     var router = ref.watch(appRouterProvider); | ||||
|     ref.watch(releaseInfoProvider.notifier).checkGithubReleaseInfo(); | ||||
|  | ||||
|     return MaterialApp( | ||||
| @@ -142,7 +145,8 @@ class ImmichAppState extends ConsumerState<ImmichApp> | ||||
|               primarySwatch: Colors.indigo, | ||||
|               fontFamily: 'WorkSans', | ||||
|               snackBarTheme: const SnackBarThemeData( | ||||
|                   contentTextStyle: TextStyle(fontFamily: 'WorkSans')), | ||||
|                 contentTextStyle: TextStyle(fontFamily: 'WorkSans'), | ||||
|               ), | ||||
|               scaffoldBackgroundColor: immichBackgroundColor, | ||||
|               appBarTheme: const AppBarTheme( | ||||
|                 backgroundColor: immichBackgroundColor, | ||||
| @@ -152,9 +156,10 @@ class ImmichAppState extends ConsumerState<ImmichApp> | ||||
|                 systemOverlayStyle: SystemUiOverlayStyle.dark, | ||||
|               ), | ||||
|             ), | ||||
|             routeInformationParser: _immichRouter.defaultRouteParser(), | ||||
|             routerDelegate: _immichRouter.delegate( | ||||
|                 navigatorObservers: () => [TabNavigationObserver(ref: ref)]), | ||||
|             routeInformationParser: router.defaultRouteParser(), | ||||
|             routerDelegate: router.delegate( | ||||
|               navigatorObservers: () => [TabNavigationObserver(ref: ref)], | ||||
|             ), | ||||
|           ), | ||||
|           const ImmichLoadingOverlay(), | ||||
|           const VersionAnnouncementOverlay(), | ||||
|   | ||||
| @@ -3,17 +3,20 @@ import 'package:fluttertoast/fluttertoast.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/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:openapi/api.dart'; | ||||
|  | ||||
| class ImageViewerStateNotifier extends StateNotifier<ImageViewerPageState> { | ||||
|   final ImageViewerService _imageViewerService = ImageViewerService(); | ||||
|   final ImageViewerService _imageViewerService; | ||||
|  | ||||
|   ImageViewerStateNotifier() | ||||
|       : super(ImageViewerPageState( | ||||
|             downloadAssetStatus: DownloadAssetStatus.idle)); | ||||
|   ImageViewerStateNotifier(this._imageViewerService) | ||||
|       : super( | ||||
|           ImageViewerPageState( | ||||
|             downloadAssetStatus: DownloadAssetStatus.idle, | ||||
|           ), | ||||
|         ); | ||||
|  | ||||
|   void downloadAsset(ImmichAsset asset, BuildContext context) async { | ||||
|   void downloadAsset(AssetResponseDto asset, BuildContext context) async { | ||||
|     state = state.copyWith(downloadAssetStatus: DownloadAssetStatus.loading); | ||||
|  | ||||
|     bool isSuccess = await _imageViewerService.downloadAssetToDevice(asset); | ||||
| @@ -43,4 +46,5 @@ class ImageViewerStateNotifier extends StateNotifier<ImageViewerPageState> { | ||||
|  | ||||
| final imageViewerStateProvider = | ||||
|     StateNotifierProvider<ImageViewerStateNotifier, ImageViewerPageState>( | ||||
|         ((ref) => ImageViewerStateNotifier())); | ||||
|   ((ref) => ImageViewerStateNotifier(ref.watch(imageViewerServiceProvider))), | ||||
| ); | ||||
|   | ||||
| @@ -1,33 +1,35 @@ | ||||
| import 'dart:io'; | ||||
|  | ||||
| import 'package:flutter/foundation.dart'; | ||||
| import 'package:hive_flutter/hive_flutter.dart'; | ||||
| import 'package:immich_mobile/constants/hive_box.dart'; | ||||
| import 'package:immich_mobile/shared/models/immich_asset.model.dart'; | ||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| import 'package:immich_mobile/shared/services/api.service.dart'; | ||||
| import 'package:openapi/api.dart'; | ||||
| import 'package:path/path.dart' as p; | ||||
| import 'package:http/http.dart' as http; | ||||
|  | ||||
| import 'package:photo_manager/photo_manager.dart'; | ||||
| import 'package:path_provider/path_provider.dart'; | ||||
|  | ||||
| final imageViewerServiceProvider = | ||||
|     Provider((ref) => ImageViewerService(ref.watch(apiServiceProvider))); | ||||
|  | ||||
| class ImageViewerService { | ||||
|   Future<bool> downloadAssetToDevice(ImmichAsset asset) async { | ||||
|   final ApiService _apiService; | ||||
|   ImageViewerService(this._apiService); | ||||
|  | ||||
|   Future<bool> downloadAssetToDevice(AssetResponseDto asset) async { | ||||
|     try { | ||||
|       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( | ||||
|         filePath, | ||||
|         headers: { | ||||
|           "Authorization": "Bearer ${Hive.box(userInfoBox).get(accessTokenKey)}" | ||||
|         }, | ||||
|       var res = await _apiService.assetApi.downloadFileWithHttpInfo( | ||||
|         asset.deviceAssetId, | ||||
|         asset.deviceId, | ||||
|         isThumb: false, | ||||
|         isWeb: false, | ||||
|       ); | ||||
|  | ||||
|       final AssetEntity? entity; | ||||
|  | ||||
|       if (asset.type == 'IMAGE') { | ||||
|       if (asset.type == AssetTypeEnum.IMAGE) { | ||||
|         entity = await PhotoManager.editor.saveImage( | ||||
|           res.bodyBytes, | ||||
|           title: p.basename(asset.originalPath), | ||||
|   | ||||
| @@ -2,13 +2,12 @@ import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:flutter_map/flutter_map.dart'; | ||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| import 'package:immich_mobile/shared/models/immich_asset_with_exif.model.dart'; | ||||
| import 'package:intl/intl.dart'; | ||||
| import 'package:openapi/api.dart'; | ||||
| import 'package:path/path.dart' as p; | ||||
| import 'package:latlong2/latlong.dart'; | ||||
|  | ||||
| class ExifBottomSheet extends ConsumerWidget { | ||||
|   final ImmichAssetWithExif assetDetail; | ||||
|   final AssetResponseDto assetDetail; | ||||
|  | ||||
|   const ExifBottomSheet({Key? key, required this.assetDetail}) | ||||
|       : super(key: key); | ||||
| @@ -26,8 +25,10 @@ class ExifBottomSheet extends ConsumerWidget { | ||||
|           ), | ||||
|           child: FlutterMap( | ||||
|             options: MapOptions( | ||||
|               center: LatLng(assetDetail.exifInfo!.latitude!, | ||||
|                   assetDetail.exifInfo!.longitude!), | ||||
|               center: LatLng( | ||||
|                 assetDetail.exifInfo?.latitude?.toDouble() ?? 0, | ||||
|                 assetDetail.exifInfo?.longitude?.toDouble() ?? 0, | ||||
|               ), | ||||
|               zoom: 16.0, | ||||
|             ), | ||||
|             layers: [ | ||||
| @@ -46,10 +47,13 @@ class ExifBottomSheet extends ConsumerWidget { | ||||
|                 markers: [ | ||||
|                   Marker( | ||||
|                     anchorPos: AnchorPos.align(AnchorAlign.top), | ||||
|                     point: LatLng(assetDetail.exifInfo!.latitude!, | ||||
|                         assetDetail.exifInfo!.longitude!), | ||||
|                     point: LatLng( | ||||
|                       assetDetail.exifInfo?.latitude?.toDouble() ?? 0, | ||||
|                       assetDetail.exifInfo?.longitude?.toDouble() ?? 0, | ||||
|                     ), | ||||
|                     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( | ||||
|         "${assetDetail.exifInfo!.city}, ${assetDetail.exifInfo!.state}", | ||||
|         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) | ||||
|             Text( | ||||
|               DateFormat('date_format'.tr()).format( | ||||
|                 DateTime.parse(assetDetail.exifInfo!.dateTimeOriginal!), | ||||
|                 assetDetail.exifInfo!.dateTimeOriginal!, | ||||
|               ), | ||||
|               style: TextStyle( | ||||
|                 color: Colors.grey[400], | ||||
| @@ -151,7 +158,8 @@ class ExifBottomSheet extends ConsumerWidget { | ||||
|                     ), | ||||
|                     subtitle: assetDetail.exifInfo?.exifImageHeight != null | ||||
|                         ? Text( | ||||
|                             "${assetDetail.exifInfo?.exifImageHeight} x ${assetDetail.exifInfo?.exifImageWidth}  ${assetDetail.exifInfo?.fileSizeInByte!}B ") | ||||
|                             "${assetDetail.exifInfo?.exifImageHeight} x ${assetDetail.exifInfo?.exifImageWidth}  ${assetDetail.exifInfo?.fileSizeInByte!}B ", | ||||
|                           ) | ||||
|                         : null, | ||||
|                   ), | ||||
|                   if (assetDetail.exifInfo?.make != null) | ||||
| @@ -166,7 +174,8 @@ class ExifBottomSheet extends ConsumerWidget { | ||||
|                         style: const TextStyle(fontWeight: FontWeight.bold), | ||||
|                       ), | ||||
|                       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} ", | ||||
|                       ), | ||||
|                     ), | ||||
|                 ], | ||||
|               ), | ||||
|   | ||||
| @@ -22,11 +22,15 @@ class _RemotePhotoViewState extends State<RemotePhotoView> { | ||||
|       maxScale: allowMoving ? 1.0 : PhotoViewComputedScale.contained, | ||||
|       enablePanAlways: true, | ||||
|       scaleStateChangedCallback: _scaleStateChanged, | ||||
|         onScaleEnd: _onScaleListener); | ||||
|       onScaleEnd: _onScaleListener, | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   void _onScaleListener(BuildContext context, ScaleEndDetails details, | ||||
|       PhotoViewControllerValue controllerValue) { | ||||
|   void _onScaleListener( | ||||
|     BuildContext context, | ||||
|     ScaleEndDetails details, | ||||
|     PhotoViewControllerValue controllerValue, | ||||
|   ) { | ||||
|     // Disable swipe events when zoomed in | ||||
|     if (_zoomedIn) return; | ||||
|  | ||||
| @@ -42,12 +46,17 @@ class _RemotePhotoViewState extends State<RemotePhotoView> { | ||||
|   } | ||||
|  | ||||
|   CachedNetworkImageProvider _authorizedImageProvider(String url) { | ||||
|     return CachedNetworkImageProvider(url, | ||||
|         headers: {"Authorization": widget.authToken}, cacheKey: url); | ||||
|     return CachedNetworkImageProvider( | ||||
|       url, | ||||
|       headers: {"Authorization": widget.authToken}, | ||||
|       cacheKey: url, | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   void _performStateTransition( | ||||
|       _RemoteImageStatus newStatus, CachedNetworkImageProvider provider) { | ||||
|     _RemoteImageStatus newStatus, | ||||
|     CachedNetworkImageProvider provider, | ||||
|   ) { | ||||
|     // Transition to same status is forbidden | ||||
|     if (_status == newStatus) return; | ||||
|     // Transition full -> thumbnail is forbidden | ||||
| @@ -67,19 +76,22 @@ class _RemotePhotoViewState extends State<RemotePhotoView> { | ||||
|         _authorizedImageProvider(widget.thumbnailUrl); | ||||
|     _imageProvider = thumbnailProvider; | ||||
|  | ||||
|     thumbnailProvider | ||||
|         .resolve(const ImageConfiguration()) | ||||
|         .addListener(ImageStreamListener((ImageInfo imageInfo, _) { | ||||
|       _performStateTransition(_RemoteImageStatus.thumbnail, thumbnailProvider); | ||||
|     })); | ||||
|     thumbnailProvider.resolve(const ImageConfiguration()).addListener( | ||||
|       ImageStreamListener((ImageInfo imageInfo, _) { | ||||
|         _performStateTransition( | ||||
|           _RemoteImageStatus.thumbnail, | ||||
|           thumbnailProvider, | ||||
|         ); | ||||
|       }), | ||||
|     ); | ||||
|  | ||||
|     CachedNetworkImageProvider fullProvider = | ||||
|         _authorizedImageProvider(widget.imageUrl); | ||||
|     fullProvider | ||||
|         .resolve(const ImageConfiguration()) | ||||
|         .addListener(ImageStreamListener((ImageInfo imageInfo, _) { | ||||
|     fullProvider.resolve(const ImageConfiguration()).addListener( | ||||
|       ImageStreamListener((ImageInfo imageInfo, _) { | ||||
|         _performStateTransition(_RemoteImageStatus.full, fullProvider); | ||||
|     })); | ||||
|       }), | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
| @@ -90,14 +102,14 @@ class _RemotePhotoViewState extends State<RemotePhotoView> { | ||||
| } | ||||
|  | ||||
| class RemotePhotoView extends StatefulWidget { | ||||
|   const RemotePhotoView( | ||||
|       {Key? key, | ||||
|   const RemotePhotoView({ | ||||
|     Key? key, | ||||
|     required this.thumbnailUrl, | ||||
|     required this.imageUrl, | ||||
|     required this.authToken, | ||||
|     required this.onSwipeDown, | ||||
|       required this.onSwipeUp}) | ||||
|       : super(key: key); | ||||
|     required this.onSwipeUp, | ||||
|   }) : super(key: key); | ||||
|  | ||||
|   final String thumbnailUrl; | ||||
|   final String imageUrl; | ||||
|   | ||||
| @@ -3,17 +3,17 @@ import 'dart:developer'; | ||||
| import 'package:auto_route/auto_route.dart'; | ||||
| import 'package:flutter/material.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 { | ||||
|   const TopControlAppBar( | ||||
|       {Key? key, | ||||
|   const TopControlAppBar({ | ||||
|     Key? key, | ||||
|     required this.asset, | ||||
|     required this.onMoreInfoPressed, | ||||
|       required this.onDownloadPressed}) | ||||
|       : super(key: key); | ||||
|     required this.onDownloadPressed, | ||||
|   }) : super(key: key); | ||||
|  | ||||
|   final ImmichAsset asset; | ||||
|   final AssetResponseDto asset; | ||||
|   final Function onMoreInfoPressed; | ||||
|   final Function onDownloadPressed; | ||||
|  | ||||
| @@ -59,7 +59,8 @@ class TopControlAppBar extends ConsumerWidget with PreferredSizeWidget { | ||||
|           onPressed: () { | ||||
|             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/top_control_app_bar.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/models/immich_asset_with_exif.model.dart'; | ||||
| import 'package:openapi/api.dart'; | ||||
|  | ||||
| // ignore: must_be_immutable | ||||
| class ImageViewerPage extends HookConsumerWidget { | ||||
|   final String imageUrl; | ||||
|   final String heroTag; | ||||
|   final String thumbnailUrl; | ||||
|   final ImmichAsset asset; | ||||
|   final AssetResponseDto asset; | ||||
|  | ||||
|   ImmichAssetWithExif? assetDetail; | ||||
|   AssetResponseDto? assetDetail; | ||||
|  | ||||
|   ImageViewerPage({ | ||||
|     Key? key, | ||||
| @@ -54,10 +53,13 @@ class ImageViewerPage extends HookConsumerWidget { | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     useEffect(() { | ||||
|     useEffect( | ||||
|       () { | ||||
|         getAssetExif(); | ||||
|         return null; | ||||
|     }, []); | ||||
|       }, | ||||
|       [], | ||||
|     ); | ||||
|  | ||||
|     return Scaffold( | ||||
|       backgroundColor: Colors.black, | ||||
| @@ -82,7 +84,8 @@ class ImageViewerPage extends HookConsumerWidget { | ||||
|                   authToken: "Bearer ${box.get(accessTokenKey)}", | ||||
|                   onSwipeDown: () => AutoRouter.of(context).pop(), | ||||
|                   onSwipeUp: () => showInfo(), | ||||
|                   )), | ||||
|                 ), | ||||
|               ), | ||||
|             ), | ||||
|             if (downloadAssetStatus == DownloadAssetStatus.loading) | ||||
|               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/top_control_app_bar.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/models/immich_asset_with_exif.model.dart'; | ||||
| import 'package:openapi/api.dart'; | ||||
| import 'package:video_player/video_player.dart'; | ||||
|  | ||||
| // ignore: must_be_immutable | ||||
| class VideoViewerPage extends HookConsumerWidget { | ||||
|   final String videoUrl; | ||||
|   final ImmichAsset asset; | ||||
|   ImmichAssetWithExif? assetDetail; | ||||
|   final AssetResponseDto asset; | ||||
|   AssetResponseDto? assetDetail; | ||||
|  | ||||
|   VideoViewerPage({Key? key, required this.videoUrl, required this.asset}) | ||||
|       : super(key: key); | ||||
| @@ -49,10 +48,13 @@ class VideoViewerPage extends HookConsumerWidget { | ||||
|           await ref.watch(assetServiceProvider).getAssetById(asset.id); | ||||
|     } | ||||
|  | ||||
|     useEffect(() { | ||||
|     useEffect( | ||||
|       () { | ||||
|         getAssetExif(); | ||||
|         return null; | ||||
|     }, []); | ||||
|       }, | ||||
|       [], | ||||
|     ); | ||||
|  | ||||
|     return Scaffold( | ||||
|       backgroundColor: Colors.black, | ||||
| @@ -116,8 +118,10 @@ class _VideoThumbnailPlayerState extends State<VideoThumbnailPlayer> { | ||||
|  | ||||
|   Future<void> initializePlayer() async { | ||||
|     try { | ||||
|       videoPlayerController = VideoPlayerController.network(widget.url, | ||||
|           httpHeaders: {"Authorization": "Bearer ${widget.jwtToken}"}); | ||||
|       videoPlayerController = VideoPlayerController.network( | ||||
|         widget.url, | ||||
|         httpHeaders: {"Authorization": "Bearer ${widget.jwtToken}"}, | ||||
|       ); | ||||
|  | ||||
|       await videoPlayerController.initialize(); | ||||
|       _createChewieController(); | ||||
|   | ||||
| @@ -1,10 +1,10 @@ | ||||
| import 'package:cancellation_token_http/http.dart'; | ||||
| import 'package:collection/collection.dart'; | ||||
| import 'package:openapi/api.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/current_upload_asset.model.dart'; | ||||
| import 'package:immich_mobile/shared/models/server_info.model.dart'; | ||||
|  | ||||
| enum BackUpProgressEnum { idle, inProgress, done } | ||||
|  | ||||
| @@ -14,7 +14,7 @@ class BackUpState { | ||||
|   final List<String> allAssetsInDatabase; | ||||
|   final double progressInPercentage; | ||||
|   final CancellationToken cancelToken; | ||||
|   final ServerInfo serverInfo; | ||||
|   final ServerInfoResponseDto serverInfo; | ||||
|  | ||||
|   /// All available albums on the device | ||||
|   final List<AvailableAlbum> availableAlbums; | ||||
| @@ -49,7 +49,7 @@ class BackUpState { | ||||
|     List<String>? allAssetsInDatabase, | ||||
|     double? progressInPercentage, | ||||
|     CancellationToken? cancelToken, | ||||
|     ServerInfo? serverInfo, | ||||
|     ServerInfoResponseDto? serverInfo, | ||||
|     List<AvailableAlbum>? availableAlbums, | ||||
|     Set<AssetPathEntity>? selectedBackupAlbums, | ||||
|     Set<AssetPathEntity>? excludedBackupAlbums, | ||||
| @@ -93,8 +93,10 @@ class BackUpState { | ||||
|         collectionEquals(other.selectedBackupAlbums, selectedBackupAlbums) && | ||||
|         collectionEquals(other.excludedBackupAlbums, excludedBackupAlbums) && | ||||
|         collectionEquals(other.allUniqueAssets, allUniqueAssets) && | ||||
|         collectionEquals(other.selectedAlbumsBackupAssetsIds, | ||||
|             selectedAlbumsBackupAssetsIds) && | ||||
|         collectionEquals( | ||||
|           other.selectedAlbumsBackupAssetsIds, | ||||
|           selectedAlbumsBackupAssetsIds, | ||||
|         ) && | ||||
|         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/login/models/authentication_state.model.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:openapi/api.dart'; | ||||
| import 'package:photo_manager/photo_manager.dart'; | ||||
|  | ||||
| class BackupNotifier extends StateNotifier<BackUpState> { | ||||
| @@ -28,12 +28,12 @@ class BackupNotifier extends StateNotifier<BackUpState> { | ||||
|             allAssetsInDatabase: const [], | ||||
|             progressInPercentage: 0, | ||||
|             cancelToken: CancellationToken(), | ||||
|             serverInfo: ServerInfo( | ||||
|             serverInfo: ServerInfoResponseDto( | ||||
|               diskAvailable: "0", | ||||
|               diskAvailableRaw: 0, | ||||
|               diskSize: "0", | ||||
|               diskSizeRaw: 0, | ||||
|               diskUsagePercentage: 0.0, | ||||
|               diskUsagePercentage: 0, | ||||
|               diskUse: "0", | ||||
|               diskUseRaw: 0, | ||||
|             ), | ||||
| @@ -113,7 +113,9 @@ class BackupNotifier extends StateNotifier<BackUpState> { | ||||
|     // Get all albums on the device | ||||
|     List<AvailableAlbum> availableAlbums = []; | ||||
|     List<AssetPathEntity> albums = await PhotoManager.getAssetPathList( | ||||
|         hasAll: true, type: RequestType.common); | ||||
|       hasAll: true, | ||||
|       type: RequestType.common, | ||||
|     ); | ||||
|  | ||||
|     for (AssetPathEntity album in albums) { | ||||
|       AvailableAlbum availableAlbum = AvailableAlbum(albumEntity: album); | ||||
| @@ -156,7 +158,10 @@ class BackupNotifier extends StateNotifier<BackUpState> { | ||||
|  | ||||
|       // Get album that contains all assets | ||||
|       var list = await PhotoManager.getAssetPathList( | ||||
|           hasAll: true, onlyAll: true, type: RequestType.common); | ||||
|         hasAll: true, | ||||
|         onlyAll: true, | ||||
|         type: RequestType.common, | ||||
|       ); | ||||
|       AssetPathEntity albumHasAllAssets = list.first; | ||||
|  | ||||
|       backupAlbumInfoBox.put( | ||||
| @@ -175,13 +180,15 @@ class BackupNotifier extends StateNotifier<BackUpState> { | ||||
|       for (var selectedAlbumId in backupAlbumInfo!.selectedAlbumIds) { | ||||
|         var albumAsset = await AssetPathEntity.fromId(selectedAlbumId); | ||||
|         state = state.copyWith( | ||||
|             selectedBackupAlbums: {...state.selectedBackupAlbums, albumAsset}); | ||||
|           selectedBackupAlbums: {...state.selectedBackupAlbums, albumAsset}, | ||||
|         ); | ||||
|       } | ||||
|  | ||||
|       for (var excludedAlbumId in backupAlbumInfo.excludedAlbumsIds) { | ||||
|         var albumAsset = await AssetPathEntity.fromId(excludedAlbumId); | ||||
|         state = state.copyWith( | ||||
|             excludedBackupAlbums: {...state.excludedBackupAlbums, albumAsset}); | ||||
|           excludedBackupAlbums: {...state.excludedBackupAlbums, albumAsset}, | ||||
|         ); | ||||
|       } | ||||
|     } catch (e) { | ||||
|       debugPrint("[ERROR] Failed to generate album from id $e"); | ||||
| @@ -211,8 +218,11 @@ class BackupNotifier extends StateNotifier<BackUpState> { | ||||
|  | ||||
|     Set<AssetEntity> allUniqueAssets = | ||||
|         assetsFromSelectedAlbums.difference(assetsFromExcludedAlbums); | ||||
|     List<String> allAssetsInDatabase = | ||||
|         await _backupService.getDeviceBackupAsset(); | ||||
|     var allAssetsInDatabase = await _backupService.getDeviceBackupAsset(); | ||||
|  | ||||
|     if (allAssetsInDatabase == null) { | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     // Find asset that were backup from selected albums | ||||
|     Set<String> selectedAlbumsBackupAssets = | ||||
| @@ -328,23 +338,27 @@ class BackupNotifier extends StateNotifier<BackUpState> { | ||||
|   void cancelBackup() { | ||||
|     state.cancelToken.cancel(); | ||||
|     state = state.copyWith( | ||||
|         backupProgress: BackUpProgressEnum.idle, progressInPercentage: 0.0); | ||||
|       backupProgress: BackUpProgressEnum.idle, | ||||
|       progressInPercentage: 0.0, | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   void _onAssetUploaded(String deviceAssetId, String deviceId) { | ||||
|     state = state.copyWith(selectedAlbumsBackupAssetsIds: { | ||||
|     state = state.copyWith( | ||||
|       selectedAlbumsBackupAssetsIds: { | ||||
|         ...state.selectedAlbumsBackupAssetsIds, | ||||
|         deviceAssetId | ||||
|     }, allAssetsInDatabase: [ | ||||
|       ...state.allAssetsInDatabase, | ||||
|       deviceAssetId | ||||
|     ]); | ||||
|       }, | ||||
|       allAssetsInDatabase: [...state.allAssetsInDatabase, deviceAssetId], | ||||
|     ); | ||||
|  | ||||
|     if (state.allUniqueAssets.length - | ||||
|             state.selectedAlbumsBackupAssetsIds.length == | ||||
|         0) { | ||||
|       state = state.copyWith( | ||||
|           backupProgress: BackUpProgressEnum.done, progressInPercentage: 0.0); | ||||
|         backupProgress: BackUpProgressEnum.done, | ||||
|         progressInPercentage: 0.0, | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     _updateServerInfo(); | ||||
| @@ -352,25 +366,20 @@ class BackupNotifier extends StateNotifier<BackUpState> { | ||||
|  | ||||
|   void _onUploadProgress(int sent, int total) { | ||||
|     state = state.copyWith( | ||||
|         progressInPercentage: (sent.toDouble() / total.toDouble() * 100)); | ||||
|       progressInPercentage: (sent.toDouble() / total.toDouble() * 100), | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   Future<void> _updateServerInfo() async { | ||||
|     var serverInfo = await _serverInfoService.getServerInfo(); | ||||
|  | ||||
|     // Update server info | ||||
|     if (serverInfo != null) { | ||||
|       state = state.copyWith( | ||||
|       serverInfo: ServerInfo( | ||||
|         diskSize: serverInfo.diskSize, | ||||
|         diskUse: serverInfo.diskUse, | ||||
|         diskAvailable: serverInfo.diskAvailable, | ||||
|         diskSizeRaw: serverInfo.diskSizeRaw, | ||||
|         diskUseRaw: serverInfo.diskUseRaw, | ||||
|         diskAvailableRaw: serverInfo.diskAvailableRaw, | ||||
|         diskUsagePercentage: serverInfo.diskUsagePercentage, | ||||
|       ), | ||||
|         serverInfo: serverInfo, | ||||
|       ); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   void resumeBackup() { | ||||
|     // Check if user is login | ||||
|   | ||||
| @@ -2,59 +2,38 @@ import 'dart:async'; | ||||
| import 'dart:convert'; | ||||
| import 'dart:io'; | ||||
|  | ||||
| import 'package:dio/dio.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:hive/hive.dart'; | ||||
| import 'package:hooks_riverpod/hooks_riverpod.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/error_upload_asset.model.dart'; | ||||
| import 'package:immich_mobile/shared/services/network.service.dart'; | ||||
| import 'package:immich_mobile/shared/models/device_info.model.dart'; | ||||
| import 'package:immich_mobile/shared/services/api.service.dart'; | ||||
| import 'package:immich_mobile/utils/files_helper.dart'; | ||||
| import 'package:openapi/api.dart'; | ||||
| import 'package:photo_manager/photo_manager.dart'; | ||||
| import 'package:http_parser/http_parser.dart'; | ||||
| import 'package:path/path.dart' as p; | ||||
| import 'package:cancellation_token_http/http.dart' as http; | ||||
|  | ||||
| final backupServiceProvider = | ||||
|     Provider((ref) => BackupService(ref.watch(networkServiceProvider))); | ||||
| final backupServiceProvider = Provider( | ||||
|   (ref) => BackupService( | ||||
|     ref.watch(apiServiceProvider), | ||||
|   ), | ||||
| ); | ||||
|  | ||||
| class BackupService { | ||||
|   final NetworkService _networkService; | ||||
|   final ApiService _apiService; | ||||
|   BackupService(this._apiService); | ||||
|  | ||||
|   BackupService(this._networkService); | ||||
|  | ||||
|   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 { | ||||
|   Future<List<String>?> getDeviceBackupAsset() async { | ||||
|     String deviceId = Hive.box(userInfoBox).get(deviceIdKey); | ||||
|  | ||||
|     try { | ||||
|       Response response = | ||||
|           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; | ||||
|       } | ||||
|       return await _apiService.assetApi.getUserAssetsByDeviceId(deviceId); | ||||
|     } catch (e) { | ||||
|       return false; | ||||
|       debugPrint('Error [getDeviceBackupAsset] ${e.toString()}'); | ||||
|       return null; | ||||
|     } | ||||
|   } | ||||
|  | ||||
| @@ -99,9 +78,11 @@ class BackupService { | ||||
|           var box = Hive.box(userInfoBox); | ||||
|  | ||||
|           var req = MultipartRequest( | ||||
|               'POST', Uri.parse('$savedEndpoint/asset/upload'), | ||||
|             'POST', | ||||
|             Uri.parse('$savedEndpoint/asset/upload'), | ||||
|             onProgress: ((bytes, totalBytes) => | ||||
|                   uploadProgressCb(bytes, totalBytes))); | ||||
|                 uploadProgressCb(bytes, totalBytes)), | ||||
|           ); | ||||
|           req.headers["Authorization"] = "Bearer ${box.get(accessTokenKey)}"; | ||||
|  | ||||
|           req.fields['deviceAssetId'] = entity.id; | ||||
| @@ -133,16 +114,19 @@ class BackupService { | ||||
|             var error = jsonDecode(data); | ||||
|  | ||||
|             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( | ||||
|               ErrorUploadAsset( | ||||
|                 asset: entity, | ||||
|                 id: entity.id, | ||||
|                 createdAt: entity.createDateTime, | ||||
|                 fileName: originalFileName, | ||||
|                 fileType: _getAssetType(entity.type), | ||||
|                 errorMessage: error['error'], | ||||
|             )); | ||||
|               ), | ||||
|             ); | ||||
|             continue; | ||||
|           } | ||||
|         } | ||||
| @@ -160,8 +144,6 @@ class BackupService { | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   void sendBackupRequest(AssetEntity entity) {} | ||||
|  | ||||
|   String _getAssetType(AssetType assetType) { | ||||
|     switch (assetType) { | ||||
|       case AssetType.audio: | ||||
| @@ -175,15 +157,29 @@ class BackupService { | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   Future<DeviceInfoRemote> setAutoBackup( | ||||
|       bool status, String deviceId, String deviceType) async { | ||||
|     var res = await _networkService.patchRequest(url: 'device-info', data: { | ||||
|       "isAutoBackup": status, | ||||
|       "deviceId": deviceId, | ||||
|       "deviceType": deviceType, | ||||
|     }); | ||||
|   Future<DeviceInfoResponseDto> setAutoBackup( | ||||
|     bool status, | ||||
|     String deviceId, | ||||
|     DeviceTypeEnum deviceType, | ||||
|   ) async { | ||||
|     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); | ||||
|  | ||||
|     ColorFilter selectedFilter = ColorFilter.mode( | ||||
|         Theme.of(context).primaryColor.withAlpha(100), BlendMode.darken); | ||||
|       Theme.of(context).primaryColor.withAlpha(100), | ||||
|       BlendMode.darken, | ||||
|     ); | ||||
|     ColorFilter excludedFilter = | ||||
|         ColorFilter.mode(Colors.red.withAlpha(75), BlendMode.darken); | ||||
|     ColorFilter unselectedFilter = | ||||
| @@ -40,7 +42,10 @@ class AlbumInfoCard extends HookConsumerWidget { | ||||
|           label: const Text( | ||||
|             "album_info_card_backup_album_included", | ||||
|             style: TextStyle( | ||||
|                 fontSize: 10, color: Colors.white, fontWeight: FontWeight.bold), | ||||
|               fontSize: 10, | ||||
|               color: Colors.white, | ||||
|               fontWeight: FontWeight.bold, | ||||
|             ), | ||||
|           ).tr(), | ||||
|           backgroundColor: Theme.of(context).primaryColor, | ||||
|         ); | ||||
| @@ -51,7 +56,10 @@ class AlbumInfoCard extends HookConsumerWidget { | ||||
|           label: const Text( | ||||
|             "album_info_card_backup_album_excluded", | ||||
|             style: TextStyle( | ||||
|                 fontSize: 10, color: Colors.white, fontWeight: FontWeight.bold), | ||||
|               fontSize: 10, | ||||
|               color: Colors.white, | ||||
|               fontWeight: FontWeight.bold, | ||||
|             ), | ||||
|           ).tr(), | ||||
|           backgroundColor: Colors.red[300], | ||||
|         ); | ||||
| @@ -139,14 +147,15 @@ class AlbumInfoCard extends HookConsumerWidget { | ||||
|                   decoration: BoxDecoration( | ||||
|                     borderRadius: const BorderRadius.only( | ||||
|                       topLeft: Radius.circular(12), | ||||
|                         topRight: Radius.circular(12)), | ||||
|                       topRight: Radius.circular(12), | ||||
|                     ), | ||||
|                     image: DecorationImage( | ||||
|                       colorFilter: _buildImageFilter(), | ||||
|                       image: imageData != null | ||||
|                           ? MemoryImage(imageData!) | ||||
|                           : const AssetImage( | ||||
|                                   'assets/immich-logo-no-outline.png') | ||||
|                               as ImageProvider, | ||||
|                               'assets/immich-logo-no-outline.png', | ||||
|                             ) as ImageProvider, | ||||
|                       fit: BoxFit.cover, | ||||
|                     ), | ||||
|                   ), | ||||
| @@ -176,7 +185,8 @@ class AlbumInfoCard extends HookConsumerWidget { | ||||
|                             style: TextStyle( | ||||
|                               fontSize: 14, | ||||
|                               color: Theme.of(context).primaryColor, | ||||
|                                 fontWeight: FontWeight.bold), | ||||
|                               fontWeight: FontWeight.bold, | ||||
|                             ), | ||||
|                           ), | ||||
|                           Padding( | ||||
|                             padding: const EdgeInsets.only(top: 2.0), | ||||
| @@ -186,7 +196,9 @@ class AlbumInfoCard extends HookConsumerWidget { | ||||
|                                       ? " (${'backup_all'.tr()})" | ||||
|                                       : ""), | ||||
|                               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 subtitle; | ||||
|   final String info; | ||||
|   const BackupInfoCard( | ||||
|       {Key? key, | ||||
|   const BackupInfoCard({ | ||||
|     Key? key, | ||||
|     required this.title, | ||||
|     required this.subtitle, | ||||
|       required this.info}) | ||||
|       : super(key: key); | ||||
|     required this.info, | ||||
|   }) : super(key: key); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|   | ||||
| @@ -20,10 +20,13 @@ class AlbumPreviewPage extends HookConsumerWidget { | ||||
|           await album.getAssetListRange(start: 0, end: album.assetCount); | ||||
|     } | ||||
|  | ||||
|     useEffect(() { | ||||
|     useEffect( | ||||
|       () { | ||||
|         _getAssetsInAlbum(); | ||||
|         return null; | ||||
|     }, []); | ||||
|       }, | ||||
|       [], | ||||
|     ); | ||||
|  | ||||
|     return Scaffold( | ||||
|       appBar: AppBar( | ||||
| @@ -41,7 +44,8 @@ class AlbumPreviewPage extends HookConsumerWidget { | ||||
|                 style: TextStyle( | ||||
|                   fontSize: 10, | ||||
|                   color: Colors.grey[600], | ||||
|                     fontWeight: FontWeight.bold), | ||||
|                   fontWeight: FontWeight.bold, | ||||
|                 ), | ||||
|               ), | ||||
|             ), | ||||
|           ], | ||||
| @@ -59,9 +63,11 @@ class AlbumPreviewPage extends HookConsumerWidget { | ||||
|         ), | ||||
|         itemCount: assets.value.length, | ||||
|         itemBuilder: (context, index) { | ||||
|           Future<Uint8List?> thumbData = assets.value[index] | ||||
|               .thumbnailDataWithSize(const ThumbnailSize(200, 200), | ||||
|                   quality: 50); | ||||
|           Future<Uint8List?> thumbData = | ||||
|               assets.value[index].thumbnailDataWithSize( | ||||
|             const ThumbnailSize(200, 200), | ||||
|             quality: 50, | ||||
|           ); | ||||
|  | ||||
|           return FutureBuilder<Uint8List?>( | ||||
|             future: thumbData, | ||||
|   | ||||
| @@ -17,10 +17,13 @@ class BackupAlbumSelectionPage extends HookConsumerWidget { | ||||
|     final selectedBackupAlbums = ref.watch(backupProvider).selectedBackupAlbums; | ||||
|     final excludedBackupAlbums = ref.watch(backupProvider).excludedBackupAlbums; | ||||
|  | ||||
|     useEffect(() { | ||||
|     useEffect( | ||||
|       () { | ||||
|         ref.read(backupProvider.notifier).getBackupInfo(); | ||||
|         return null; | ||||
|     }, []); | ||||
|       }, | ||||
|       [], | ||||
|     ); | ||||
|  | ||||
|     _buildAlbumSelectionList() { | ||||
|       if (availableAlbums.isEmpty) { | ||||
| @@ -43,7 +46,8 @@ class BackupAlbumSelectionPage extends HookConsumerWidget { | ||||
|                   : const EdgeInsets.all(0), | ||||
|               child: AlbumInfoCard( | ||||
|                 imageData: thumbnailData, | ||||
|                   albumInfo: availableAlbums[index].albumEntity), | ||||
|                 albumInfo: availableAlbums[index].albumEntity, | ||||
|               ), | ||||
|             ); | ||||
|           }), | ||||
|         ), | ||||
| @@ -73,13 +77,15 @@ class BackupAlbumSelectionPage extends HookConsumerWidget { | ||||
|             child: Chip( | ||||
|               visualDensity: VisualDensity.compact, | ||||
|               shape: RoundedRectangleBorder( | ||||
|                   borderRadius: BorderRadius.circular(5)), | ||||
|                 borderRadius: BorderRadius.circular(5), | ||||
|               ), | ||||
|               label: Text( | ||||
|                 album.name, | ||||
|                 style: const TextStyle( | ||||
|                   fontSize: 10, | ||||
|                   color: Colors.white, | ||||
|                     fontWeight: FontWeight.bold), | ||||
|                   fontWeight: FontWeight.bold, | ||||
|                 ), | ||||
|               ), | ||||
|               backgroundColor: Theme.of(context).primaryColor, | ||||
|               deleteIconColor: Colors.white, | ||||
| @@ -109,13 +115,15 @@ class BackupAlbumSelectionPage extends HookConsumerWidget { | ||||
|             child: Chip( | ||||
|               visualDensity: VisualDensity.compact, | ||||
|               shape: RoundedRectangleBorder( | ||||
|                   borderRadius: BorderRadius.circular(5)), | ||||
|                 borderRadius: BorderRadius.circular(5), | ||||
|               ), | ||||
|               label: Text( | ||||
|                 album.name, | ||||
|                 style: const TextStyle( | ||||
|                   fontSize: 10, | ||||
|                   color: Colors.white, | ||||
|                     fontWeight: FontWeight.bold), | ||||
|                   fontWeight: FontWeight.bold, | ||||
|                 ), | ||||
|               ), | ||||
|               backgroundColor: Colors.red[300], | ||||
|               deleteIconColor: Colors.white, | ||||
| @@ -187,7 +195,8 @@ class BackupAlbumSelectionPage extends HookConsumerWidget { | ||||
|                       style: TextStyle( | ||||
|                         fontWeight: FontWeight.bold, | ||||
|                         fontSize: 14, | ||||
|                           color: Colors.grey[700]), | ||||
|                         color: Colors.grey[700], | ||||
|                       ), | ||||
|                     ).tr(), | ||||
|                     trailing: Text( | ||||
|                       ref | ||||
| @@ -234,7 +243,8 @@ class BackupAlbumSelectionPage extends HookConsumerWidget { | ||||
|                   builder: (BuildContext context) { | ||||
|                     return AlertDialog( | ||||
|                       shape: RoundedRectangleBorder( | ||||
|                           borderRadius: BorderRadius.circular(12)), | ||||
|                         borderRadius: BorderRadius.circular(12), | ||||
|                       ), | ||||
|                       elevation: 5, | ||||
|                       title: Text( | ||||
|                         'backup_album_selection_page_selection_info', | ||||
| @@ -250,7 +260,9 @@ class BackupAlbumSelectionPage extends HookConsumerWidget { | ||||
|                             Text( | ||||
|                               'backup_album_selection_page_assets_scatter', | ||||
|                               style: TextStyle( | ||||
|                                   fontSize: 14, color: Colors.grey[700]), | ||||
|                                 fontSize: 14, | ||||
|                                 color: Colors.grey[700], | ||||
|                               ), | ||||
|                             ).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/shared/providers/websocket.provider.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'; | ||||
|  | ||||
| class BackupControllerPage extends HookConsumerWidget { | ||||
| @@ -27,7 +26,8 @@ class BackupControllerPage extends HookConsumerWidget { | ||||
|         ? false | ||||
|         : true; | ||||
|  | ||||
|     useEffect(() { | ||||
|     useEffect( | ||||
|       () { | ||||
|         if (backupState.backupProgress != BackUpProgressEnum.inProgress) { | ||||
|           ref.watch(backupProvider.notifier).getBackupInfo(); | ||||
|         } | ||||
| @@ -36,7 +36,9 @@ class BackupControllerPage extends HookConsumerWidget { | ||||
|             .watch(websocketProvider.notifier) | ||||
|             .stopListenToEvent('on_upload_success'); | ||||
|         return null; | ||||
|     }, []); | ||||
|       }, | ||||
|       [], | ||||
|     ); | ||||
|  | ||||
|     Widget _buildStorageInformation() { | ||||
|       return ListTile( | ||||
| @@ -71,7 +73,8 @@ class BackupControllerPage extends HookConsumerWidget { | ||||
|                   args: [ | ||||
|                     backupState.serverInfo.diskUse, | ||||
|                     backupState.serverInfo.diskSize | ||||
|                     ]), | ||||
|                   ], | ||||
|                 ), | ||||
|               ), | ||||
|             ], | ||||
|           ), | ||||
| @@ -129,8 +132,10 @@ class BackupControllerPage extends HookConsumerWidget { | ||||
|                           .setAutoBackup(true); | ||||
|                     } | ||||
|                   }, | ||||
|                   child: Text(backupBtnText, | ||||
|                       style: const TextStyle(fontWeight: FontWeight.bold)), | ||||
|                   child: Text( | ||||
|                     backupBtnText, | ||||
|                     style: const TextStyle(fontWeight: FontWeight.bold), | ||||
|                   ), | ||||
|                 ), | ||||
|               ) | ||||
|             ], | ||||
| @@ -159,7 +164,8 @@ class BackupControllerPage extends HookConsumerWidget { | ||||
|             style: TextStyle( | ||||
|               color: Theme.of(context).primaryColor, | ||||
|               fontSize: 12, | ||||
|                 fontWeight: FontWeight.bold), | ||||
|               fontWeight: FontWeight.bold, | ||||
|             ), | ||||
|           ), | ||||
|         ); | ||||
|       } else { | ||||
| @@ -170,7 +176,8 @@ class BackupControllerPage extends HookConsumerWidget { | ||||
|             style: TextStyle( | ||||
|               color: Theme.of(context).primaryColor, | ||||
|               fontSize: 12, | ||||
|                 fontWeight: FontWeight.bold), | ||||
|               fontWeight: FontWeight.bold, | ||||
|             ), | ||||
|           ), | ||||
|         ); | ||||
|       } | ||||
| @@ -192,7 +199,8 @@ class BackupControllerPage extends HookConsumerWidget { | ||||
|             style: TextStyle( | ||||
|               color: Colors.red[300], | ||||
|               fontSize: 12, | ||||
|                 fontWeight: FontWeight.bold), | ||||
|               fontWeight: FontWeight.bold, | ||||
|             ), | ||||
|           ), | ||||
|         ); | ||||
|       } else { | ||||
| @@ -213,9 +221,10 @@ class BackupControllerPage extends HookConsumerWidget { | ||||
|         borderOnForeground: false, | ||||
|         child: ListTile( | ||||
|           minVerticalPadding: 15, | ||||
|           title: const Text("backup_controller_page_albums", | ||||
|                   style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20)) | ||||
|               .tr(), | ||||
|           title: const Text( | ||||
|             "backup_controller_page_albums", | ||||
|             style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20), | ||||
|           ).tr(), | ||||
|           subtitle: Padding( | ||||
|             padding: const EdgeInsets.only(top: 8.0), | ||||
|             child: Column( | ||||
| @@ -284,9 +293,9 @@ class BackupControllerPage extends HookConsumerWidget { | ||||
|                     fontWeight: FontWeight.bold, | ||||
|                     fontSize: 11, | ||||
|                   ), | ||||
|                 ).tr(args: [ | ||||
|                   ref.watch(errorBackupListProvider).length.toString() | ||||
|                 ]), | ||||
|                 ).tr( | ||||
|                   args: [ref.watch(errorBackupListProvider).length.toString()], | ||||
|                 ), | ||||
|                 backgroundColor: Colors.white, | ||||
|                 onPressed: () { | ||||
|                   AutoRouter.of(context).push(const FailedBackupStatusRoute()); | ||||
| @@ -331,12 +340,16 @@ class BackupControllerPage extends HookConsumerWidget { | ||||
|                           child: const Text( | ||||
|                             'backup_controller_page_filename', | ||||
|                             style: TextStyle( | ||||
|                                 fontWeight: FontWeight.bold, fontSize: 10.0), | ||||
|                           ).tr(args: [ | ||||
|                               fontWeight: FontWeight.bold, | ||||
|                               fontSize: 10.0, | ||||
|                             ), | ||||
|                           ).tr( | ||||
|                             args: [ | ||||
|                               backupState.currentUploadAsset.fileName, | ||||
|                               backupState.currentUploadAsset.fileType | ||||
|                                   .toLowerCase() | ||||
|                           ]), | ||||
|                             ], | ||||
|                           ), | ||||
|                         ), | ||||
|                       ), | ||||
|                     ], | ||||
| @@ -352,16 +365,20 @@ class BackupControllerPage extends HookConsumerWidget { | ||||
|                           padding: const EdgeInsets.all(6.0), | ||||
|                           child: const Text( | ||||
|                             "backup_controller_page_created", | ||||
|                             style: const TextStyle( | ||||
|                                 fontWeight: FontWeight.bold, fontSize: 10.0), | ||||
|                           ).tr(args: [ | ||||
|                             style: TextStyle( | ||||
|                               fontWeight: FontWeight.bold, | ||||
|                               fontSize: 10.0, | ||||
|                             ), | ||||
|                           ).tr( | ||||
|                             args: [ | ||||
|                               DateFormat.yMMMMd('en_US').format( | ||||
|                                 DateTime.parse( | ||||
|                                   backupState.currentUploadAsset.createdAt | ||||
|                                       .toString(), | ||||
|                                 ), | ||||
|                               ) | ||||
|                           ]), | ||||
|                             ], | ||||
|                           ), | ||||
|                         ), | ||||
|                       ), | ||||
|                     ], | ||||
| @@ -413,7 +430,8 @@ class BackupControllerPage extends HookConsumerWidget { | ||||
|           splashRadius: 24, | ||||
|           icon: const Icon( | ||||
|             Icons.arrow_back_ios_rounded, | ||||
|             )), | ||||
|           ), | ||||
|         ), | ||||
|       ), | ||||
|       body: Padding( | ||||
|         padding: const EdgeInsets.only(left: 16.0, right: 16, bottom: 32), | ||||
|   | ||||
| @@ -25,7 +25,8 @@ class FailedBackupStatusPage extends HookConsumerWidget { | ||||
|           splashRadius: 24, | ||||
|           icon: const Icon( | ||||
|             Icons.arrow_back_ios_rounded, | ||||
|             )), | ||||
|           ), | ||||
|         ), | ||||
|       ), | ||||
|       body: ListView.builder( | ||||
|         shrinkWrap: true, | ||||
| @@ -94,7 +95,8 @@ class FailedBackupStatusPage extends HookConsumerWidget { | ||||
|                                 style: TextStyle( | ||||
|                                   fontSize: 12, | ||||
|                                   fontWeight: FontWeight.w600, | ||||
|                                     color: Colors.grey[700]), | ||||
|                                   color: Colors.grey[700], | ||||
|                                 ), | ||||
|                               ), | ||||
|                               Icon( | ||||
|                                 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:immich_mobile/shared/models/immich_asset.model.dart'; | ||||
| import 'package:openapi/api.dart'; | ||||
|  | ||||
| class ImmichAssetGroupByDate { | ||||
|   final String date; | ||||
|   List<ImmichAsset> assets; | ||||
|   List<AssetResponseDto> assets; | ||||
|   ImmichAssetGroupByDate({ | ||||
|     required this.date, | ||||
|     required this.assets, | ||||
| @@ -13,7 +11,7 @@ class ImmichAssetGroupByDate { | ||||
|  | ||||
|   ImmichAssetGroupByDate copyWith({ | ||||
|     String? date, | ||||
|     List<ImmichAsset>? assets, | ||||
|     List<AssetResponseDto>? assets, | ||||
|   }) { | ||||
|     return ImmichAssetGroupByDate( | ||||
|       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 | ||||
|   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 | ||||
|   String toString() => | ||||
|       'GetAllAssetResponse(count: $count, data: $data, nextPageKey: $nextPageKey)'; | ||||
|   | ||||
| @@ -1,12 +1,10 @@ | ||||
| import 'dart:convert'; | ||||
|  | ||||
| import 'package:collection/collection.dart'; | ||||
|  | ||||
| import 'package:immich_mobile/shared/models/immich_asset.model.dart'; | ||||
| import 'package:openapi/api.dart'; | ||||
|  | ||||
| class HomePageState { | ||||
|   final bool isMultiSelectEnable; | ||||
|   final Set<ImmichAsset> selectedItems; | ||||
|   final Set<AssetResponseDto> selectedItems; | ||||
|   final Set<String> selectedDateGroup; | ||||
|   HomePageState({ | ||||
|     required this.isMultiSelectEnable, | ||||
| @@ -16,7 +14,7 @@ class HomePageState { | ||||
|  | ||||
|   HomePageState copyWith({ | ||||
|     bool? isMultiSelectEnable, | ||||
|     Set<ImmichAsset>? selectedItems, | ||||
|     Set<AssetResponseDto>? selectedItems, | ||||
|     Set<String>? selectedDateGroup, | ||||
|   }) { | ||||
|     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 | ||||
|   String toString() => | ||||
|       'HomePageState(isMultiSelectEnable: $isMultiSelectEnable, selectedItems: $selectedItems, selectedDateGroup: $selectedDateGroup)'; | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| import 'package:hooks_riverpod/hooks_riverpod.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> { | ||||
|   HomePageStateNotifier() | ||||
| @@ -14,7 +14,8 @@ class HomePageStateNotifier extends StateNotifier<HomePageState> { | ||||
|  | ||||
|   void addSelectedDateGroup(String dateGroupTitle) { | ||||
|     state = state.copyWith( | ||||
|         selectedDateGroup: {...state.selectedDateGroup, dateGroupTitle}); | ||||
|       selectedDateGroup: {...state.selectedDateGroup, dateGroupTitle}, | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   void removeSelectedDateGroup(String dateGroupTitle) { | ||||
| @@ -25,36 +26,39 @@ class HomePageStateNotifier extends StateNotifier<HomePageState> { | ||||
|     state = state.copyWith(selectedDateGroup: currentDateGroup); | ||||
|   } | ||||
|  | ||||
|   void enableMultiSelect(Set<ImmichAsset> selectedItems) { | ||||
|   void enableMultiSelect(Set<AssetResponseDto> selectedItems) { | ||||
|     state = | ||||
|         state.copyWith(isMultiSelectEnable: true, selectedItems: selectedItems); | ||||
|   } | ||||
|  | ||||
|   void disableMultiSelect() { | ||||
|     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}); | ||||
|   } | ||||
|  | ||||
|   void addMultipleSelectedItems(List<ImmichAsset> assets) { | ||||
|   void addMultipleSelectedItems(List<AssetResponseDto> assets) { | ||||
|     state = state.copyWith(selectedItems: {...state.selectedItems, ...assets}); | ||||
|   } | ||||
|  | ||||
|   void removeSingleSelectedItem(ImmichAsset asset) { | ||||
|     Set<ImmichAsset> currentList = state.selectedItems; | ||||
|   void removeSingleSelectedItem(AssetResponseDto asset) { | ||||
|     Set<AssetResponseDto> currentList = state.selectedItems; | ||||
|  | ||||
|     currentList.removeWhere((e) => e.id == asset.id); | ||||
|  | ||||
|     state = state.copyWith(selectedItems: currentList); | ||||
|   } | ||||
|  | ||||
|   void removeMultipleSelectedItem(List<ImmichAsset> assets) { | ||||
|     Set<ImmichAsset> currentList = state.selectedItems; | ||||
|   void removeMultipleSelectedItem(List<AssetResponseDto> assets) { | ||||
|     Set<AssetResponseDto> currentList = state.selectedItems; | ||||
|  | ||||
|     for (ImmichAsset asset in assets) { | ||||
|     for (AssetResponseDto asset in assets) { | ||||
|       currentList.removeWhere((e) => e.id == asset.id); | ||||
|     } | ||||
|  | ||||
| @@ -64,4 +68,5 @@ class HomePageStateNotifier extends StateNotifier<HomePageState> { | ||||
|  | ||||
| final homePageStateProvider = | ||||
|     StateNotifierProvider<HomePageStateNotifier, HomePageState>( | ||||
|         ((ref) => HomePageStateNotifier())); | ||||
|   ((ref) => HomePageStateNotifier()), | ||||
| ); | ||||
|   | ||||
| @@ -73,10 +73,12 @@ class UploadProfileImageState { | ||||
| class UploadProfileImageNotifier | ||||
|     extends StateNotifier<UploadProfileImageState> { | ||||
|   UploadProfileImageNotifier(this._userSErvice) | ||||
|       : super(UploadProfileImageState( | ||||
|       : super( | ||||
|           UploadProfileImageState( | ||||
|             profileImagePath: '', | ||||
|             status: UploadProfileStatus.idle, | ||||
|         )); | ||||
|           ), | ||||
|         ); | ||||
|  | ||||
|   final UserService _userSErvice; | ||||
|  | ||||
| @@ -89,7 +91,8 @@ class UploadProfileImageNotifier | ||||
|       debugPrint("Succesfully upload profile image"); | ||||
|       state = state.copyWith( | ||||
|         status: UploadProfileStatus.success, | ||||
|           profileImagePath: res.profileImagePath); | ||||
|         profileImagePath: res.profileImagePath, | ||||
|       ); | ||||
|       return true; | ||||
|     } | ||||
|  | ||||
| @@ -100,4 +103,5 @@ class UploadProfileImageNotifier | ||||
|  | ||||
| final uploadProfileImageProvider = | ||||
|     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:hooks_riverpod/hooks_riverpod.dart'; | ||||
| import 'package:immich_mobile/modules/home/models/delete_asset_response.model.dart'; | ||||
| import 'package:immich_mobile/modules/home/models/get_all_asset_response.model.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'; | ||||
| import 'package:immich_mobile/shared/services/api.service.dart'; | ||||
| import 'package:openapi/api.dart'; | ||||
|  | ||||
| final assetServiceProvider = | ||||
|     Provider((ref) => AssetService(ref.watch(networkServiceProvider))); | ||||
| final assetServiceProvider = Provider( | ||||
|   (ref) => AssetService( | ||||
|     ref.watch(apiServiceProvider), | ||||
|   ), | ||||
| ); | ||||
|  | ||||
| class AssetService { | ||||
|   final NetworkService _networkService; | ||||
|   AssetService(this._networkService); | ||||
|   final ApiService _apiService; | ||||
|  | ||||
|   Future<List<ImmichAsset>?> getAllAsset() async { | ||||
|     var res = await _networkService.getRequest(url: "asset/"); | ||||
|   AssetService(this._apiService); | ||||
|  | ||||
|   Future<List<AssetResponseDto>?> getAllAsset() async { | ||||
|     try { | ||||
|       List<dynamic> decodedData = jsonDecode(res.toString()); | ||||
|  | ||||
|       List<ImmichAsset> result = | ||||
|           List.from(decodedData.map((a) => ImmichAsset.fromMap(a))); | ||||
|       return result; | ||||
|       return await _apiService.assetApi.getAllAssets(); | ||||
|     } catch (e) { | ||||
|       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()}"); | ||||
|       debugPrint("Error [getAllAsset]  ${e.toString()}"); | ||||
|       return null; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   Future<List<DeleteAssetResponse>?> deleteAssets( | ||||
|       Set<ImmichAsset> deleteAssets) async { | ||||
|   Future<AssetResponseDto?> getAssetById(String assetId) async { | ||||
|     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) { | ||||
|         payload.add(asset.id); | ||||
|       } | ||||
|  | ||||
|       var res = await _networkService | ||||
|           .deleteRequest(url: "asset/", data: {"ids": payload}); | ||||
|  | ||||
|       List<dynamic> decodedData = jsonDecode(res.toString()); | ||||
|  | ||||
|       List<DeleteAssetResponse> result = | ||||
|           List.from(decodedData.map((a) => DeleteAssetResponse.fromMap(a))); | ||||
|  | ||||
|       return result; | ||||
|       return await _apiService.assetApi | ||||
|           .deleteAsset(DeleteAssetDto(ids: payload)); | ||||
|     } catch (e) { | ||||
|       debugPrint("Error getAllAsset  ${e.toString()}"); | ||||
|       return null; | ||||
|   | ||||
| @@ -15,7 +15,9 @@ class ControlBottomAppBar extends StatelessWidget { | ||||
|         height: MediaQuery.of(context).size.height * 0.15, | ||||
|         decoration: BoxDecoration( | ||||
|           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), | ||||
|         ), | ||||
|         child: Column( | ||||
| @@ -48,12 +50,12 @@ class ControlBottomAppBar extends StatelessWidget { | ||||
| } | ||||
|  | ||||
| class ControlBoxButton extends StatelessWidget { | ||||
|   const ControlBoxButton( | ||||
|       {Key? key, | ||||
|   const ControlBoxButton({ | ||||
|     Key? key, | ||||
|     required this.label, | ||||
|     required this.iconData, | ||||
|       required this.onPressed}) | ||||
|       : super(key: key); | ||||
|     required this.onPressed, | ||||
|   }) : super(key: key); | ||||
|  | ||||
|   final String label; | ||||
|   final IconData iconData; | ||||
|   | ||||
| @@ -2,8 +2,7 @@ import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| import 'package:immich_mobile/modules/home/providers/home_page_state.provider.dart'; | ||||
| import 'package:immich_mobile/shared/models/immich_asset.model.dart'; | ||||
| import 'package:intl/intl.dart'; | ||||
| import 'package:openapi/api.dart'; | ||||
|  | ||||
| class DailyTitleText extends ConsumerWidget { | ||||
|   const DailyTitleText({ | ||||
| @@ -13,14 +12,15 @@ class DailyTitleText extends ConsumerWidget { | ||||
|   }) : super(key: key); | ||||
|  | ||||
|   final String isoDate; | ||||
|   final List<ImmichAsset> assetGroup; | ||||
|   final List<AssetResponseDto> assetGroup; | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context, WidgetRef ref) { | ||||
|     var currentYear = DateTime.now().year; | ||||
|     var groupYear = DateTime.parse(isoDate).year; | ||||
|     var formatDateTemplate = | ||||
|         currentYear == groupYear ? "daily_title_text_date".tr() : "daily_title_text_date_year".tr(); | ||||
|     var formatDateTemplate = currentYear == groupYear | ||||
|         ? "daily_title_text_date".tr() | ||||
|         : "daily_title_text_date_year".tr(); | ||||
|     var dateText = | ||||
|         DateFormat(formatDateTemplate).format(DateTime.parse(isoDate)); | ||||
|     var isMultiSelectEnable = | ||||
| @@ -74,7 +74,11 @@ class DailyTitleText extends ConsumerWidget { | ||||
|     return SliverToBoxAdapter( | ||||
|       child: Padding( | ||||
|         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( | ||||
|           children: [ | ||||
|             Text( | ||||
|   | ||||
| @@ -36,8 +36,11 @@ class DisableMultiSelectButton extends ConsumerWidget { | ||||
|                 label: Text( | ||||
|                   '$selectedItemCount', | ||||
|                   style: const TextStyle( | ||||
|                         fontWeight: FontWeight.w600, fontSize: 18), | ||||
|                   )), | ||||
|                     fontWeight: FontWeight.w600, | ||||
|                     fontSize: 18, | ||||
|                   ), | ||||
|                 ), | ||||
|               ), | ||||
|             ), | ||||
|           ), | ||||
|         ), | ||||
|   | ||||
| @@ -118,20 +118,24 @@ class DraggableScrollbar extends StatefulWidget { | ||||
|     this.labelConstraints, | ||||
|   })  : assert(child.scrollDirection == Axis.vertical), | ||||
|         scrollThumbBuilder = _thumbSemicircleBuilder( | ||||
|             heightScrollThumb * 0.6, scrollThumbKey, alwaysVisibleScrollThumb), | ||||
|           heightScrollThumb * 0.6, | ||||
|           scrollThumbKey, | ||||
|           alwaysVisibleScrollThumb, | ||||
|         ), | ||||
|         super(key: key); | ||||
|  | ||||
|   @override | ||||
|   DraggableScrollbarState createState() => DraggableScrollbarState(); | ||||
|  | ||||
|   static buildScrollThumbAndLabel( | ||||
|       {required Widget scrollThumb, | ||||
|   static buildScrollThumbAndLabel({ | ||||
|     required Widget scrollThumb, | ||||
|     required Color backgroundColor, | ||||
|     required Animation<double>? thumbAnimation, | ||||
|     required Animation<double>? labelAnimation, | ||||
|     required Text? labelText, | ||||
|     required BoxConstraints? labelConstraints, | ||||
|       required bool alwaysVisibleScrollThumb}) { | ||||
|     required bool alwaysVisibleScrollThumb, | ||||
|   }) { | ||||
|     var scrollThumbAndLabel = labelText == null | ||||
|         ? scrollThumb | ||||
|         : Row( | ||||
| @@ -158,7 +162,10 @@ class DraggableScrollbar extends StatefulWidget { | ||||
|   } | ||||
|  | ||||
|   static ScrollThumbBuilder _thumbSemicircleBuilder( | ||||
|       double width, Key? scrollThumbKey, bool alwaysVisibleScrollThumb) { | ||||
|     double width, | ||||
|     Key? scrollThumbKey, | ||||
|     bool alwaysVisibleScrollThumb, | ||||
|   ) { | ||||
|     return ( | ||||
|       Color backgroundColor, | ||||
|       Animation<double> thumbAnimation, | ||||
| @@ -198,7 +205,9 @@ class DraggableScrollbar extends StatefulWidget { | ||||
|   } | ||||
|  | ||||
|   static ScrollThumbBuilder _thumbArrowBuilder( | ||||
|       Key? scrollThumbKey, bool alwaysVisibleScrollThumb) { | ||||
|     Key? scrollThumbKey, | ||||
|     bool alwaysVisibleScrollThumb, | ||||
|   ) { | ||||
|     return ( | ||||
|       Color backgroundColor, | ||||
|       Animation<double> thumbAnimation, | ||||
| @@ -234,7 +243,9 @@ class DraggableScrollbar extends StatefulWidget { | ||||
|   } | ||||
|  | ||||
|   static ScrollThumbBuilder _thumbRRectBuilder( | ||||
|       Key? scrollThumbKey, bool alwaysVisibleScrollThumb) { | ||||
|     Key? scrollThumbKey, | ||||
|     bool alwaysVisibleScrollThumb, | ||||
|   ) { | ||||
|     return ( | ||||
|       Color backgroundColor, | ||||
|       Animation<double> thumbAnimation, | ||||
| @@ -403,11 +414,13 @@ class DraggableScrollbarState extends State<DraggableScrollbar> | ||||
|                       labelConstraints: widget.labelConstraints, | ||||
|                     ), | ||||
|                   ), | ||||
|             )), | ||||
|                 ), | ||||
|               ), | ||||
|             ], | ||||
|           ), | ||||
|         ); | ||||
|     }); | ||||
|       }, | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   //scroll bar has received notification that it's view was scrolled | ||||
| @@ -498,7 +511,10 @@ class DraggableScrollbarState extends State<DraggableScrollbar> | ||||
|         } | ||||
|  | ||||
|         double viewDelta = getScrollViewDelta( | ||||
|             details.delta.dy, barMaxScrollExtent, viewMaxScrollExtent); | ||||
|           details.delta.dy, | ||||
|           barMaxScrollExtent, | ||||
|           viewMaxScrollExtent, | ||||
|         ); | ||||
|  | ||||
|         _viewOffset = widget.controller.position.pixels + viewDelta; | ||||
|         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 + 1.0); | ||||
|     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.close(); | ||||
|  | ||||
| @@ -589,7 +607,9 @@ class ArrowClipper extends CustomClipper<Path> { | ||||
|     path.lineTo(startPointX, startPointY); | ||||
|     path.lineTo(startPointX, startPointY - 1.0); | ||||
|     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.close(); | ||||
|  | ||||
|   | ||||
| @@ -1,10 +1,10 @@ | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:hooks_riverpod/hooks_riverpod.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 { | ||||
|   final List<ImmichAsset> assetGroup; | ||||
|   final List<AssetResponseDto> assetGroup; | ||||
|  | ||||
|   const ImageGrid({Key? key, required this.assetGroup}) : super(key: key); | ||||
|  | ||||
| @@ -25,7 +25,7 @@ class ImageGrid extends ConsumerWidget { | ||||
|             child: Stack( | ||||
|               children: [ | ||||
|                 ThumbnailImage(asset: assetGroup[index]), | ||||
|                 if (assetType != 'IMAGE') | ||||
|                 if (assetType != AssetTypeEnum.IMAGE) | ||||
|                   Positioned( | ||||
|                     top: 5, | ||||
|                     right: 5, | ||||
|   | ||||
| @@ -31,7 +31,8 @@ class ImmichSliverAppBar extends ConsumerWidget { | ||||
|       pinned: false, | ||||
|       snap: false, | ||||
|       shape: const RoundedRectangleBorder( | ||||
|           borderRadius: BorderRadius.all(Radius.circular(5))), | ||||
|         borderRadius: BorderRadius.all(Radius.circular(5)), | ||||
|       ), | ||||
|       leading: Builder( | ||||
|         builder: (BuildContext context) { | ||||
|           return Stack( | ||||
| @@ -99,7 +100,8 @@ class ImmichSliverAppBar extends ConsumerWidget { | ||||
|                   child: CircularProgressIndicator( | ||||
|                     strokeWidth: 1, | ||||
|                     valueColor: AlwaysStoppedAnimation<Color>( | ||||
|                         Theme.of(context).primaryColor), | ||||
|                       Theme.of(context).primaryColor, | ||||
|                     ), | ||||
|                   ), | ||||
|                 ), | ||||
|               ), | ||||
| @@ -117,7 +119,8 @@ class ImmichSliverAppBar extends ConsumerWidget { | ||||
|                         Icons.cloud_off_rounded, | ||||
|                         size: 8, | ||||
|                       ), | ||||
|                       child: const Icon(Icons.backup_rounded)), | ||||
|                       child: const Icon(Icons.backup_rounded), | ||||
|                     ), | ||||
|               onPressed: () async { | ||||
|                 var onPop = await AutoRouter.of(context) | ||||
|                     .push(const BackupControllerRoute()); | ||||
|   | ||||
| @@ -1,6 +1,5 @@ | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:intl/intl.dart'; | ||||
|  | ||||
| class MonthlyTitleText extends StatelessWidget { | ||||
|   const MonthlyTitleText({ | ||||
| @@ -12,7 +11,8 @@ class MonthlyTitleText extends StatelessWidget { | ||||
|  | ||||
|   @override | ||||
|   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( | ||||
|       child: Padding( | ||||
|   | ||||
| @@ -55,7 +55,8 @@ class ProfileDrawer extends HookConsumerWidget { | ||||
|           return CircleAvatar( | ||||
|             radius: 35, | ||||
|             backgroundImage: NetworkImage( | ||||
|                 '$endpoint/user/profile-image/${authState.userId}?d=${dummmy++}'), | ||||
|               '$endpoint/user/profile-image/${authState.userId}?d=${dummmy++}', | ||||
|             ), | ||||
|             backgroundColor: Colors.transparent, | ||||
|           ); | ||||
|         } else { | ||||
| @@ -71,7 +72,8 @@ class ProfileDrawer extends HookConsumerWidget { | ||||
|         return CircleAvatar( | ||||
|           radius: 35, | ||||
|           backgroundImage: NetworkImage( | ||||
|               '$endpoint/user/profile-image/${authState.userId}?d=${dummmy++}'), | ||||
|             '$endpoint/user/profile-image/${authState.userId}?d=${dummmy++}', | ||||
|           ), | ||||
|           backgroundColor: Colors.transparent, | ||||
|         ); | ||||
|       } | ||||
| @@ -93,7 +95,10 @@ class ProfileDrawer extends HookConsumerWidget { | ||||
|  | ||||
|     _pickUserProfileImage() async { | ||||
|       final XFile? image = await ImagePicker().pickImage( | ||||
|           source: ImageSource.gallery, maxHeight: 1024, maxWidth: 1024); | ||||
|         source: ImageSource.gallery, | ||||
|         maxHeight: 1024, | ||||
|         maxWidth: 1024, | ||||
|       ); | ||||
|  | ||||
|       if (image != null) { | ||||
|         var success = | ||||
| @@ -101,16 +106,20 @@ class ProfileDrawer extends HookConsumerWidget { | ||||
|  | ||||
|         if (success) { | ||||
|           ref.watch(authenticationProvider.notifier).updateUserProfileImagePath( | ||||
|               ref.read(uploadProfileImageProvider).profileImagePath); | ||||
|                 ref.read(uploadProfileImageProvider).profileImagePath, | ||||
|               ); | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     useEffect(() { | ||||
|     useEffect( | ||||
|       () { | ||||
|         _getPackageInfo(); | ||||
|         _buildUserProfileImage(); | ||||
|         return null; | ||||
|     }, []); | ||||
|       }, | ||||
|       [], | ||||
|     ); | ||||
|     return Drawer( | ||||
|       child: Column( | ||||
|         mainAxisAlignment: MainAxisAlignment.spaceBetween, | ||||
| @@ -188,7 +197,8 @@ class ProfileDrawer extends HookConsumerWidget { | ||||
|                   style: TextStyle( | ||||
|                     color: Colors.black54, | ||||
|                     fontSize: 14, | ||||
|                       fontWeight: FontWeight.bold), | ||||
|                     fontWeight: FontWeight.bold, | ||||
|                   ), | ||||
|                 ).tr(), | ||||
|                 onTap: () async { | ||||
|                   bool res = | ||||
| @@ -233,7 +243,8 @@ class ProfileDrawer extends HookConsumerWidget { | ||||
|                         style: TextStyle( | ||||
|                           fontSize: 11, | ||||
|                           color: Theme.of(context).primaryColor, | ||||
|                             fontWeight: FontWeight.w600), | ||||
|                           fontWeight: FontWeight.w600, | ||||
|                         ), | ||||
|                       ), | ||||
|                     ), | ||||
|                     const Divider(), | ||||
| @@ -271,7 +282,7 @@ class ProfileDrawer extends HookConsumerWidget { | ||||
|                           ), | ||||
|                         ), | ||||
|                         Text( | ||||
|                           "${serverInfoState.serverVersion.major}.${serverInfoState.serverVersion.minor}.${serverInfoState.serverVersion.patch}", | ||||
|                           "${serverInfoState.serverVersion.major}.${serverInfoState.serverVersion.minor}.${serverInfoState.serverVersion.patch_}", | ||||
|                           style: TextStyle( | ||||
|                             fontSize: 11, | ||||
|                             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/modules/home/providers/home_page_state.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:openapi/api.dart'; | ||||
|  | ||||
| class ThumbnailImage extends HookConsumerWidget { | ||||
|   final ImmichAsset asset; | ||||
|   final AssetResponseDto asset; | ||||
|  | ||||
|   const ThumbnailImage({Key? key, required this.asset}) : super(key: key); | ||||
|  | ||||
| @@ -22,14 +22,13 @@ class ThumbnailImage extends HookConsumerWidget { | ||||
|  | ||||
|     var box = Hive.box(userInfoBox); | ||||
|     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 isMultiSelectEnable = | ||||
|         ref.watch(homePageStateProvider).isMultiSelectEnable; | ||||
|     var deviceId = ref.watch(authenticationProvider).deviceId; | ||||
|  | ||||
|     Widget _buildSelectionIcon(ImmichAsset asset) { | ||||
|     Widget _buildSelectionIcon(AssetResponseDto asset) { | ||||
|       if (selectedAsset.contains(asset)) { | ||||
|         return Icon( | ||||
|           Icons.check_circle, | ||||
| @@ -61,7 +60,7 @@ class ThumbnailImage extends HookConsumerWidget { | ||||
|               .watch(homePageStateProvider.notifier) | ||||
|               .addSingleSelectedItem(asset); | ||||
|         } else { | ||||
|           if (asset.type == 'IMAGE') { | ||||
|           if (asset.type == AssetTypeEnum.IMAGE) { | ||||
|             AutoRouter.of(context).push( | ||||
|               ImageViewerRoute( | ||||
|                 imageUrl: | ||||
| @@ -76,7 +75,8 @@ class ThumbnailImage extends HookConsumerWidget { | ||||
|               VideoViewerRoute( | ||||
|                 videoUrl: | ||||
|                     '${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( | ||||
|                 border: isMultiSelectEnable && selectedAsset.contains(asset) | ||||
|                     ? Border.all( | ||||
|                         color: Theme.of(context).primaryColorLight, width: 10) | ||||
|                         color: Theme.of(context).primaryColorLight, | ||||
|                         width: 10, | ||||
|                       ) | ||||
|                     : const Border(), | ||||
|               ), | ||||
|               child: CachedNetworkImage( | ||||
|                 cacheKey: "${asset.id}-${cacheKey.value}", | ||||
|                 width: 300, | ||||
|                 height: 300, | ||||
|                 memCacheHeight: asset.type == 'IMAGE' ? 250 : 400, | ||||
|                 memCacheHeight: asset.type == AssetTypeEnum.IMAGE ? 250 : 400, | ||||
|                 fit: BoxFit.cover, | ||||
|                 imageUrl: thumbnailRequestUrl, | ||||
|                 httpHeaders: { | ||||
| @@ -112,9 +114,11 @@ class ThumbnailImage extends HookConsumerWidget { | ||||
|                     Transform.scale( | ||||
|                   scale: 0.2, | ||||
|                   child: CircularProgressIndicator( | ||||
|                       value: downloadProgress.progress), | ||||
|                     value: downloadProgress.progress, | ||||
|                   ), | ||||
|                 ), | ||||
|                 errorWidget: (context, url, error) { | ||||
|                   debugPrint("Error getting thumbnail $url = $error"); | ||||
|                   return Icon( | ||||
|                     Icons.image_not_supported_outlined, | ||||
|                     color: Theme.of(context).primaryColor, | ||||
|   | ||||
| @@ -26,12 +26,15 @@ class HomePage extends HookConsumerWidget { | ||||
|         ref.watch(homePageStateProvider).isMultiSelectEnable; | ||||
|     var homePageState = ref.watch(homePageStateProvider); | ||||
|  | ||||
|     useEffect(() { | ||||
|     useEffect( | ||||
|       () { | ||||
|         ref.read(websocketProvider.notifier).connect(); | ||||
|         ref.read(assetProvider.notifier).getAllAsset(); | ||||
|         ref.watch(serverInfoProvider.notifier).getServerVersion(); | ||||
|         return null; | ||||
|     }, []); | ||||
|       }, | ||||
|       [], | ||||
|     ); | ||||
|  | ||||
|     void reloadAllAsset() { | ||||
|       ref.read(assetProvider.notifier).getAllAsset(); | ||||
|   | ||||
| @@ -1,10 +1,8 @@ | ||||
| import 'dart:convert'; | ||||
|  | ||||
| import 'package:immich_mobile/shared/models/device_info.model.dart'; | ||||
| import 'package:openapi/api.dart'; | ||||
|  | ||||
| class AuthenticationState { | ||||
|   final String deviceId; | ||||
|   final String deviceType; | ||||
|   final DeviceTypeEnum deviceType; | ||||
|   final String userId; | ||||
|   final String userEmail; | ||||
|   final bool isAuthenticated; | ||||
| @@ -13,8 +11,7 @@ class AuthenticationState { | ||||
|   final bool isAdmin; | ||||
|   final bool shouldChangePassword; | ||||
|   final String profileImagePath; | ||||
|   final DeviceInfoRemote deviceInfo; | ||||
|  | ||||
|   final DeviceInfoResponseDto deviceInfo; | ||||
|   AuthenticationState({ | ||||
|     required this.deviceId, | ||||
|     required this.deviceType, | ||||
| @@ -31,7 +28,7 @@ class AuthenticationState { | ||||
|  | ||||
|   AuthenticationState copyWith({ | ||||
|     String? deviceId, | ||||
|     String? deviceType, | ||||
|     DeviceTypeEnum? deviceType, | ||||
|     String? userId, | ||||
|     String? userEmail, | ||||
|     bool? isAuthenticated, | ||||
| @@ -40,7 +37,7 @@ class AuthenticationState { | ||||
|     bool? isAdmin, | ||||
|     bool? shouldChangePassword, | ||||
|     String? profileImagePath, | ||||
|     DeviceInfoRemote? deviceInfo, | ||||
|     DeviceInfoResponseDto? deviceInfo, | ||||
|   }) { | ||||
|     return AuthenticationState( | ||||
|       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 | ||||
|   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)'; | ||||
|   | ||||
| @@ -16,9 +16,10 @@ class HiveSavedLoginInfo { | ||||
|   @HiveField(3) | ||||
|   bool isSaveLogin; | ||||
|  | ||||
|   HiveSavedLoginInfo( | ||||
|       {required this.email, | ||||
|   HiveSavedLoginInfo({ | ||||
|     required this.email, | ||||
|     required this.password, | ||||
|     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:hive/hive.dart'; | ||||
| import 'package:hooks_riverpod/hooks_riverpod.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/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/shared/services/api.service.dart'; | ||||
| import 'package:immich_mobile/shared/services/device_info.service.dart'; | ||||
| import 'package:immich_mobile/shared/services/network.service.dart'; | ||||
| import 'package:immich_mobile/shared/models/device_info.model.dart'; | ||||
| import 'package:openapi/api.dart'; | ||||
|  | ||||
| class AuthenticationNotifier extends StateNotifier<AuthenticationState> { | ||||
|   AuthenticationNotifier( | ||||
|       this._deviceInfoService, this._backupService, this._networkService) | ||||
|       : super( | ||||
|     this._deviceInfoService, | ||||
|     this._backupService, | ||||
|     this._apiService, | ||||
|   ) : super( | ||||
|           AuthenticationState( | ||||
|             deviceId: "", | ||||
|             deviceType: "", | ||||
|             deviceType: DeviceTypeEnum.ANDROID, | ||||
|             userId: "", | ||||
|             userEmail: "", | ||||
|             firstName: '', | ||||
| @@ -26,12 +26,11 @@ class AuthenticationNotifier extends StateNotifier<AuthenticationState> { | ||||
|             isAdmin: false, | ||||
|             shouldChangePassword: false, | ||||
|             isAuthenticated: false, | ||||
|             deviceInfo: DeviceInfoRemote( | ||||
|             deviceInfo: DeviceInfoResponseDto( | ||||
|               id: 0, | ||||
|               userId: "", | ||||
|               deviceId: "", | ||||
|               deviceType: "", | ||||
|               notificationToken: "", | ||||
|               deviceType: DeviceTypeEnum.ANDROID, | ||||
|               createdAt: "", | ||||
|               isAutoBackup: false, | ||||
|             ), | ||||
| @@ -40,10 +39,14 @@ class AuthenticationNotifier extends StateNotifier<AuthenticationState> { | ||||
|  | ||||
|   final DeviceInfoService _deviceInfoService; | ||||
|   final BackupService _backupService; | ||||
|   final NetworkService _networkService; | ||||
|   final ApiService _apiService; | ||||
|  | ||||
|   Future<bool> login(String email, String password, String serverEndpoint, | ||||
|       bool isSavedLoginInfo) async { | ||||
|   Future<bool> login( | ||||
|     String email, | ||||
|     String password, | ||||
|     String serverEndpoint, | ||||
|     bool isSavedLoginInfo, | ||||
|   ) async { | ||||
|     // Store server endpoint to Hive and test endpoint | ||||
|     if (serverEndpoint[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); | ||||
|     } | ||||
|  | ||||
|     // Check Server URL validity | ||||
|     try { | ||||
|       bool isServerEndpointVerified = await _networkService.pingServer(); | ||||
|       if (!isServerEndpointVerified) { | ||||
|         return false; | ||||
|       } | ||||
|       _apiService.setEndpoint(Hive.box(userInfoBox).get(serverEndpointKey)); | ||||
|       await _apiService.serverInfoApi.pingServer(); | ||||
|     } catch (e) { | ||||
|       debugPrint('Invalid Server Endpoint Url $e'); | ||||
|       return false; | ||||
|     } | ||||
|  | ||||
| @@ -72,24 +75,34 @@ class AuthenticationNotifier extends StateNotifier<AuthenticationState> { | ||||
|  | ||||
|     // Make sign-in request | ||||
|     try { | ||||
|       Response res = await _networkService.postRequest( | ||||
|           url: 'auth/login', data: {'email': email, 'password': password}); | ||||
|       var loginResponse = await _apiService.authenticationApi.login( | ||||
|         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( | ||||
|         isAuthenticated: true, | ||||
|         userId: payload.userId, | ||||
|         userEmail: payload.userEmail, | ||||
|         firstName: payload.firstName, | ||||
|         lastName: payload.lastName, | ||||
|         profileImagePath: payload.profileImagePath, | ||||
|         isAdmin: payload.isAdmin, | ||||
|         shouldChangePassword: payload.shouldChangePassword, | ||||
|         userId: loginResponse.userId, | ||||
|         userEmail: loginResponse.userEmail, | ||||
|         firstName: loginResponse.firstName, | ||||
|         lastName: loginResponse.lastName, | ||||
|         profileImagePath: loginResponse.profileImagePath, | ||||
|         isAdmin: loginResponse.isAdmin, | ||||
|         shouldChangePassword: loginResponse.shouldChangePassword, | ||||
|       ); | ||||
|  | ||||
|       // Login Success - Set Access Token to API Client | ||||
|       _apiService.setAccessToken(loginResponse.accessToken); | ||||
|  | ||||
|       if (isSavedLoginInfo) { | ||||
|         // Save login info to local storage | ||||
|         Hive.box<HiveSavedLoginInfo>(hiveLoginInfoBox).put( | ||||
| @@ -98,30 +111,37 @@ class AuthenticationNotifier extends StateNotifier<AuthenticationState> { | ||||
|             email: email, | ||||
|             password: password, | ||||
|             isSaveLogin: true, | ||||
|               serverUrl: Hive.box(userInfoBox).get(serverEndpointKey)), | ||||
|             serverUrl: Hive.box(userInfoBox).get(serverEndpointKey), | ||||
|           ), | ||||
|         ); | ||||
|       } else { | ||||
|         Hive.box<HiveSavedLoginInfo>(hiveLoginInfoBox) | ||||
|             .delete(savedLoginInfoKey); | ||||
|       } | ||||
|     } catch (e) { | ||||
|       debugPrint("Error logging in $e"); | ||||
|       return false; | ||||
|     } | ||||
|  | ||||
|     // Register device info | ||||
|     try { | ||||
|       Response res = await _networkService.postRequest( | ||||
|         url: 'device-info', | ||||
|         data: { | ||||
|           'deviceId': state.deviceId, | ||||
|           'deviceType': state.deviceType, | ||||
|         }, | ||||
|       DeviceInfoResponseDto? deviceInfo = | ||||
|           await _apiService.deviceInfoApi.createDeviceInfo( | ||||
|         CreateDeviceInfoDto( | ||||
|           deviceId: state.deviceId, | ||||
|           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); | ||||
|     } catch (e) { | ||||
|       debugPrint("ERROR Register Device Info: $e"); | ||||
|       return false; | ||||
|     } | ||||
|  | ||||
|     return true; | ||||
| @@ -129,27 +149,7 @@ class AuthenticationNotifier extends StateNotifier<AuthenticationState> { | ||||
|  | ||||
|   Future<bool> logout() async { | ||||
|     Hive.box(userInfoBox).delete(accessTokenKey); | ||||
|     state = AuthenticationState( | ||||
|       deviceId: "", | ||||
|       deviceType: "", | ||||
|       userId: "", | ||||
|       userEmail: "", | ||||
|       firstName: '', | ||||
|       lastName: '', | ||||
|       profileImagePath: '', | ||||
|       shouldChangePassword: false, | ||||
|       isAuthenticated: false, | ||||
|       isAdmin: false, | ||||
|       deviceInfo: DeviceInfoRemote( | ||||
|         id: 0, | ||||
|         userId: "", | ||||
|         deviceId: "", | ||||
|         deviceType: "", | ||||
|         notificationToken: "", | ||||
|         createdAt: "", | ||||
|         isAutoBackup: false, | ||||
|       ), | ||||
|     ); | ||||
|     state = state.copyWith(isAuthenticated: false); | ||||
|  | ||||
|     return true; | ||||
|   } | ||||
| @@ -157,11 +157,13 @@ class AuthenticationNotifier extends StateNotifier<AuthenticationState> { | ||||
|   setAutoBackup(bool backupState) async { | ||||
|     var deviceInfo = await _deviceInfoService.getDeviceInfo(); | ||||
|     var deviceId = deviceInfo["deviceId"]; | ||||
|     var deviceType = deviceInfo["deviceType"]; | ||||
|  | ||||
|     DeviceInfoRemote deviceInfoRemote = | ||||
|     DeviceTypeEnum deviceType = deviceInfo["deviceType"]; | ||||
|  | ||||
|     DeviceInfoResponseDto updatedDeviceInfo = | ||||
|         await _backupService.setAutoBackup(backupState, deviceId, deviceType); | ||||
|     state = state.copyWith(deviceInfo: deviceInfoRemote); | ||||
|  | ||||
|     state = state.copyWith(deviceInfo: updatedDeviceInfo); | ||||
|   } | ||||
|  | ||||
|   updateUserProfileImagePath(String path) { | ||||
| @@ -169,19 +171,20 @@ class AuthenticationNotifier extends StateNotifier<AuthenticationState> { | ||||
|   } | ||||
|  | ||||
|   Future<bool> changePassword(String newPassword) async { | ||||
|     Response res = await _networkService.putRequest( | ||||
|       url: 'user', | ||||
|       data: { | ||||
|         'id': state.userId, | ||||
|         'password': newPassword, | ||||
|         'shouldChangePassword': false, | ||||
|       }, | ||||
|     try { | ||||
|       await _apiService.userApi.updateUser( | ||||
|         UpdateUserDto( | ||||
|           id: state.userId, | ||||
|           password: newPassword, | ||||
|           shouldChangePassword: false, | ||||
|         ), | ||||
|       ); | ||||
|  | ||||
|     if (res.statusCode == 200) { | ||||
|       state = state.copyWith(shouldChangePassword: false); | ||||
|  | ||||
|       return true; | ||||
|     } else { | ||||
|     } catch (e) { | ||||
|       debugPrint("Error changing password $e"); | ||||
|       return false; | ||||
|     } | ||||
|   } | ||||
| @@ -192,6 +195,6 @@ final authenticationProvider = | ||||
|   return AuthenticationNotifier( | ||||
|     ref.watch(deviceInfoServiceProvider), | ||||
|     ref.watch(backupServiceProvider), | ||||
|     ref.watch(networkServiceProvider), | ||||
|     ref.watch(apiServiceProvider), | ||||
|   ); | ||||
| }); | ||||
|   | ||||
| @@ -169,6 +169,7 @@ class ChangePasswordButton extends ConsumerWidget { | ||||
|       child: const Text( | ||||
|         "Change Password", | ||||
|         style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold), | ||||
|         )); | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -22,12 +22,13 @@ class LoginForm extends HookConsumerWidget { | ||||
|     final passwordController = | ||||
|         useTextEditingController.fromValue(TextEditingValue.empty); | ||||
|     final serverEndpointController = | ||||
|         useTextEditingController(text: 'login_endpoint_hint'.tr()); | ||||
|         useTextEditingController(text: 'login_form_endpoint_hint'.tr()); | ||||
|     final isSaveLoginInfo = useState<bool>(false); | ||||
|  | ||||
|     useEffect(() { | ||||
|       var loginInfo = | ||||
|           Hive.box<HiveSavedLoginInfo>(hiveLoginInfoBox).get(savedLoginInfoKey); | ||||
|     useEffect( | ||||
|       () { | ||||
|         var loginInfo = Hive.box<HiveSavedLoginInfo>(hiveLoginInfoBox) | ||||
|             .get(savedLoginInfoKey); | ||||
|  | ||||
|         if (loginInfo != null) { | ||||
|           usernameController.text = loginInfo.email; | ||||
| @@ -37,7 +38,9 @@ class LoginForm extends HookConsumerWidget { | ||||
|         } | ||||
|  | ||||
|         return null; | ||||
|     }, []); | ||||
|       }, | ||||
|       [], | ||||
|     ); | ||||
|  | ||||
|     return Center( | ||||
|       child: ConstrainedBox( | ||||
| @@ -71,14 +74,16 @@ class LoginForm extends HookConsumerWidget { | ||||
|                 dense: true, | ||||
|                 side: const BorderSide(color: Colors.grey, width: 1.5), | ||||
|                 shape: RoundedRectangleBorder( | ||||
|                     borderRadius: BorderRadius.circular(5)), | ||||
|                   borderRadius: BorderRadius.circular(5), | ||||
|                 ), | ||||
|                 enableFeedback: true, | ||||
|                 title: const Text( | ||||
|                   "login_form_save_login", | ||||
|                   style: TextStyle( | ||||
|                     fontSize: 16, | ||||
|                     fontWeight: FontWeight.bold, | ||||
|                       color: Colors.grey), | ||||
|                     color: Colors.grey, | ||||
|                   ), | ||||
|                 ).tr(), | ||||
|                 value: isSaveLoginInfo.value, | ||||
|                 onChanged: (switchValue) { | ||||
| @@ -108,7 +113,6 @@ class ServerEndpointInput extends StatelessWidget { | ||||
|       : super(key: key); | ||||
|  | ||||
|   String? _validateInput(String? url) { | ||||
|  | ||||
|     if (url?.startsWith(RegExp(r'https?://')) == true) { | ||||
|       return null; | ||||
|     } else { | ||||
| @@ -122,7 +126,7 @@ class ServerEndpointInput extends StatelessWidget { | ||||
|       controller: controller, | ||||
|       decoration: InputDecoration( | ||||
|         labelText: 'login_form_endpoint_url'.tr(), | ||||
|         border: OutlineInputBorder(), | ||||
|         border: const OutlineInputBorder(), | ||||
|         hintText: 'login_form_endpoint_hint'.tr(), | ||||
|       ), | ||||
|       validator: _validateInput, | ||||
| @@ -140,8 +144,9 @@ class EmailInput extends StatelessWidget { | ||||
|     if (email == null || email == '') return null; | ||||
|     if (email.endsWith(' ')) return 'login_form_err_trailing_whitespace'.tr(); | ||||
|     if (email.startsWith(' ')) return 'login_form_err_leading_whitespace'.tr(); | ||||
|     if (email.contains(' ') || !email.contains('@')) | ||||
|     if (email.contains(' ') || !email.contains('@')) { | ||||
|       return 'login_form_err_invalid_email'.tr(); | ||||
|     } | ||||
|     return null; | ||||
|   } | ||||
|  | ||||
| @@ -151,7 +156,7 @@ class EmailInput extends StatelessWidget { | ||||
|       controller: controller, | ||||
|       decoration: InputDecoration( | ||||
|         labelText: 'login_form_label_email'.tr(), | ||||
|         border: OutlineInputBorder(), | ||||
|         border: const OutlineInputBorder(), | ||||
|         hintText: 'login_form_email_hint'.tr(), | ||||
|       ), | ||||
|       validator: _validateInput, | ||||
| @@ -172,8 +177,9 @@ class PasswordInput extends StatelessWidget { | ||||
|       controller: controller, | ||||
|       decoration: InputDecoration( | ||||
|         labelText: 'login_form_label_password'.tr(), | ||||
|           border: OutlineInputBorder(), | ||||
|           hintText: 'login_form_password_hint'.tr()), | ||||
|         border: const OutlineInputBorder(), | ||||
|         hintText: 'login_form_password_hint'.tr(), | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
| @@ -206,10 +212,13 @@ class LoginButton extends ConsumerWidget { | ||||
|         // This will remove current cache asset state of previous user login. | ||||
|         ref.watch(assetProvider.notifier).clearAllAsset(); | ||||
|  | ||||
|           var isAuthenticated = await ref | ||||
|               .watch(authenticationProvider.notifier) | ||||
|               .login(emailController.text, passwordController.text, | ||||
|                   serverEndpointController.text, isSavedLoginInfo); | ||||
|         var isAuthenticated = | ||||
|             await ref.watch(authenticationProvider.notifier).login( | ||||
|                   emailController.text, | ||||
|                   passwordController.text, | ||||
|                   serverEndpointController.text, | ||||
|                   isSavedLoginInfo, | ||||
|                 ); | ||||
|  | ||||
|         if (isAuthenticated) { | ||||
|           // Resume backup (if enable) then navigate | ||||
| @@ -224,7 +233,7 @@ class LoginButton extends ConsumerWidget { | ||||
|         } else { | ||||
|           ImmichToast.show( | ||||
|             context: context, | ||||
|               msg: "login_failed".tr(), | ||||
|             msg: "login_form_failed_login".tr(), | ||||
|             toastType: ToastType.error, | ||||
|           ); | ||||
|         } | ||||
| @@ -232,6 +241,7 @@ class LoginButton extends ConsumerWidget { | ||||
|       child: const Text( | ||||
|         "login_form_button_text", | ||||
|         style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold), | ||||
|         ).tr()); | ||||
|       ).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 'package:collection/collection.dart'; | ||||
| import 'package:immich_mobile/shared/models/immich_asset.model.dart'; | ||||
| import 'package:openapi/api.dart'; | ||||
|  | ||||
| class SearchResultPageState { | ||||
|   final bool isLoading; | ||||
|   final bool isSuccess; | ||||
|   final bool isError; | ||||
|   final List<ImmichAsset> searchResult; | ||||
|   final List<AssetResponseDto> searchResult; | ||||
|  | ||||
|   SearchResultPageState({ | ||||
|     required this.isLoading, | ||||
| @@ -20,7 +20,7 @@ class SearchResultPageState { | ||||
|     bool? isLoading, | ||||
|     bool? isSuccess, | ||||
|     bool? isError, | ||||
|     List<ImmichAsset>? searchResult, | ||||
|     List<AssetResponseDto>? searchResult, | ||||
|   }) { | ||||
|     return SearchResultPageState( | ||||
|       isLoading: isLoading ?? this.isLoading, | ||||
| @@ -35,7 +35,7 @@ class SearchResultPageState { | ||||
|       'isLoading': isLoading, | ||||
|       'isSuccess': isSuccess, | ||||
|       '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, | ||||
|       isSuccess: map['isSuccess'] ?? false, | ||||
|       isError: map['isError'] ?? false, | ||||
|       searchResult: List<ImmichAsset>.from( | ||||
|           map['searchResult']?.map((x) => ImmichAsset.fromMap(x))), | ||||
|       searchResult: List<AssetResponseDto>.from( | ||||
|         map['searchResult']?.map((x) => AssetResponseDto.mapFromJson(x)), | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   | ||||
| @@ -1,9 +1,8 @@ | ||||
| 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/services/search.service.dart'; | ||||
| import 'package:openapi/api.dart'; | ||||
|  | ||||
| class SearchPageStateNotifier extends StateNotifier<SearchPageState> { | ||||
|   SearchPageStateNotifier(this._searchService) | ||||
| @@ -58,7 +57,7 @@ final searchPageStateProvider = | ||||
| }); | ||||
|  | ||||
| final getCuratedLocationProvider = | ||||
|     FutureProvider.autoDispose<List<CuratedLocation>>((ref) async { | ||||
|     FutureProvider.autoDispose<List<CuratedLocationsResponseDto>>((ref) async { | ||||
|   final SearchService searchService = ref.watch(searchServiceProvider); | ||||
|  | ||||
|   var curatedLocation = await searchService.getCuratedLocation(); | ||||
| @@ -66,7 +65,7 @@ final getCuratedLocationProvider = | ||||
| }); | ||||
|  | ||||
| final getCuratedObjectProvider = | ||||
|     FutureProvider.autoDispose<List<CuratedObject>>((ref) async { | ||||
|     FutureProvider.autoDispose<List<CuratedObjectsResponseDto>>((ref) async { | ||||
|   final SearchService searchService = ref.watch(searchServiceProvider); | ||||
|  | ||||
|   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/services/search.service.dart'; | ||||
| import 'package:immich_mobile/shared/models/immich_asset.model.dart'; | ||||
| import 'package:intl/intl.dart'; | ||||
| import 'package:openapi/api.dart'; | ||||
|  | ||||
| class SearchResultPageNotifier extends StateNotifier<SearchResultPageState> { | ||||
|   SearchResultPageNotifier(this._searchService) | ||||
| @@ -21,19 +21,29 @@ class SearchResultPageNotifier extends StateNotifier<SearchResultPageState> { | ||||
|  | ||||
|   void search(String searchTerm) async { | ||||
|     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) { | ||||
|       state = state.copyWith( | ||||
|         searchResult: assets, | ||||
|         isError: false, | ||||
|         isLoading: false, | ||||
|           isSuccess: true); | ||||
|         isSuccess: true, | ||||
|       ); | ||||
|     } else { | ||||
|       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; | ||||
|  | ||||
|   assets.sortByCompare<DateTime>( | ||||
|       (e) => DateTime.parse(e.createdAt), (a, b) => b.compareTo(a)); | ||||
|   return assets.groupListsBy((element) => | ||||
|       DateFormat('y-MM-dd').format(DateTime.parse(element.createdAt))); | ||||
|     (e) => DateTime.parse(e.createdAt), | ||||
|     (a, b) => b.compareTo(a), | ||||
|   ); | ||||
|   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: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/shared/models/immich_asset.model.dart'; | ||||
| import 'package:immich_mobile/shared/services/network.service.dart'; | ||||
| import 'package:immich_mobile/shared/services/api.service.dart'; | ||||
| import 'package:openapi/api.dart'; | ||||
|  | ||||
| final searchServiceProvider = | ||||
|     Provider((ref) => SearchService(ref.watch(networkServiceProvider))); | ||||
| final searchServiceProvider = Provider( | ||||
|   (ref) => SearchService( | ||||
|     ref.watch(apiServiceProvider), | ||||
|   ), | ||||
| ); | ||||
|  | ||||
| class SearchService { | ||||
|   final NetworkService _networkService; | ||||
|   SearchService(this._networkService); | ||||
|   final ApiService _apiService; | ||||
|   SearchService(this._apiService); | ||||
|  | ||||
|   Future<List<String>?> getUserSuggestedSearchTerms() async { | ||||
|     try { | ||||
|       var res = await _networkService.getRequest(url: "asset/searchTerm"); | ||||
|       List<dynamic> decodedData = jsonDecode(res.toString()); | ||||
|  | ||||
|       return List.from(decodedData); | ||||
|       return await _apiService.assetApi.getAssetSearchTerms(); | ||||
|     } catch (e) { | ||||
|       debugPrint("[ERROR] [getUserSuggestedSearchTerms] ${e.toString()}"); | ||||
|       return []; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   Future<List<ImmichAsset>?> searchAsset(String searchTerm) async { | ||||
|   Future<List<AssetResponseDto>?> searchAsset(String searchTerm) async { | ||||
|     try { | ||||
|       var res = await _networkService.postRequest( | ||||
|         url: "asset/search", | ||||
|         data: {"searchTerm": searchTerm}, | ||||
|       ); | ||||
|  | ||||
|       List<dynamic> decodedData = jsonDecode(res.toString()); | ||||
|  | ||||
|       List<ImmichAsset> result = | ||||
|           List.from(decodedData.map((a) => ImmichAsset.fromMap(a))); | ||||
|  | ||||
|       return result; | ||||
|       return await _apiService.assetApi | ||||
|           .searchAsset(SearchAssetDto(searchTerm: searchTerm)); | ||||
|     } catch (e) { | ||||
|       debugPrint("[ERROR] [searchAsset] ${e.toString()}"); | ||||
|       return null; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   Future<List<CuratedLocation>?> getCuratedLocation() async { | ||||
|   Future<List<CuratedLocationsResponseDto>?> getCuratedLocation() async { | ||||
|     try { | ||||
|       var res = await _networkService.getRequest(url: "asset/allLocation"); | ||||
|       var locations = await _apiService.assetApi.getCuratedLocations(); | ||||
|  | ||||
|       List<dynamic> decodedData = jsonDecode(res.toString()); | ||||
|  | ||||
|       List<CuratedLocation> result = | ||||
|           List.from(decodedData.map((a) => CuratedLocation.fromMap(a))); | ||||
|  | ||||
|       return result; | ||||
|       return locations; | ||||
|     } catch (e) { | ||||
|       debugPrint("[ERROR] [getCuratedLocation] ${e.toString()}"); | ||||
|       throw Error(); | ||||
|       debugPrint("Error [getCuratedLocation] ${e.toString()}"); | ||||
|       return []; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   Future<List<CuratedObject>?> getCuratedObjects() async { | ||||
|   Future<List<CuratedObjectsResponseDto>?> getCuratedObjects() async { | ||||
|     try { | ||||
|       var res = await _networkService.getRequest(url: "asset/allObjects"); | ||||
|  | ||||
|       List<dynamic> decodedData = jsonDecode(res.toString()); | ||||
|  | ||||
|       List<CuratedObject> result = | ||||
|           List.from(decodedData.map((a) => CuratedObject.fromMap(a))); | ||||
|  | ||||
|       return result; | ||||
|       return await _apiService.assetApi.getCuratedObjects(); | ||||
|     } catch (e) { | ||||
|       debugPrint("[ERROR] [CuratedObject] ${e.toString()}"); | ||||
|       throw Error(); | ||||
|       debugPrint("Error [getCuratedObjects] ${e.toString()}"); | ||||
|       throw []; | ||||
|     } | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -5,9 +5,11 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| import 'package:immich_mobile/modules/search/providers/search_page_state.provider.dart'; | ||||
|  | ||||
| class SearchBar extends HookConsumerWidget with PreferredSizeWidget { | ||||
|   SearchBar( | ||||
|       {Key? key, required this.searchFocusNode, required this.onSubmitted}) | ||||
|       : super(key: key); | ||||
|   SearchBar({ | ||||
|     Key? key, | ||||
|     required this.searchFocusNode, | ||||
|     required this.onSubmitted, | ||||
|   }) : super(key: key); | ||||
|  | ||||
|   final FocusNode searchFocusNode; | ||||
|   final Function(String) onSubmitted; | ||||
| @@ -26,7 +28,8 @@ class SearchBar extends HookConsumerWidget with PreferredSizeWidget { | ||||
|                 ref.watch(searchPageStateProvider.notifier).disableSearch(); | ||||
|                 searchTermController.clear(); | ||||
|               }, | ||||
|               icon: const Icon(Icons.arrow_back_ios_rounded)) | ||||
|               icon: const Icon(Icons.arrow_back_ios_rounded), | ||||
|             ) | ||||
|           : const Icon(Icons.search_rounded), | ||||
|       title: TextField( | ||||
|         controller: searchTermController, | ||||
| @@ -50,10 +53,10 @@ class SearchBar extends HookConsumerWidget with PreferredSizeWidget { | ||||
|         }, | ||||
|         decoration: InputDecoration( | ||||
|           hintText: 'search_bar_hint'.tr(), | ||||
|           enabledBorder: UnderlineInputBorder( | ||||
|           enabledBorder: const UnderlineInputBorder( | ||||
|             borderSide: BorderSide(color: Colors.transparent), | ||||
|           ), | ||||
|           focusedBorder: UnderlineInputBorder( | ||||
|           focusedBorder: const UnderlineInputBorder( | ||||
|             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:hive_flutter/hive_flutter.dart'; | ||||
| import 'package:immich_mobile/constants/hive_box.dart'; | ||||
| import 'package:immich_mobile/utils/capitalize_first_letter.dart'; | ||||
|  | ||||
| class ThumbnailWithInfo extends StatelessWidget { | ||||
|   const ThumbnailWithInfo( | ||||
|       {Key? key, | ||||
|   const ThumbnailWithInfo({ | ||||
|     Key? key, | ||||
|     required this.textInfo, | ||||
|     required this.imageUrl, | ||||
|       required this.onTap}) | ||||
|       : super(key: key); | ||||
|     required this.onTap, | ||||
|   }) : super(key: key); | ||||
|  | ||||
|   final String textInfo; | ||||
|   final String imageUrl; | ||||
|   | ||||
| @@ -5,8 +5,6 @@ import 'package:flutter_hooks/flutter_hooks.dart'; | ||||
| import 'package:hive_flutter/hive_flutter.dart'; | ||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| import 'package:immich_mobile/constants/hive_box.dart'; | ||||
| import 'package:immich_mobile/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/ui/search_bar.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/shared/ui/immich_loading_indicator.dart'; | ||||
| import 'package:immich_mobile/utils/capitalize_first_letter.dart'; | ||||
| import 'package:openapi/api.dart'; | ||||
|  | ||||
| // ignore: must_be_immutable | ||||
| class SearchPage extends HookConsumerWidget { | ||||
| @@ -25,15 +24,18 @@ class SearchPage extends HookConsumerWidget { | ||||
|   Widget build(BuildContext context, WidgetRef ref) { | ||||
|     var box = Hive.box(userInfoBox); | ||||
|     final isSearchEnabled = ref.watch(searchPageStateProvider).isSearchEnabled; | ||||
|     AsyncValue<List<CuratedLocation>> curatedLocation = | ||||
|     AsyncValue<List<CuratedLocationsResponseDto>> curatedLocation = | ||||
|         ref.watch(getCuratedLocationProvider); | ||||
|     AsyncValue<List<CuratedObject>> curatedObjects = | ||||
|     AsyncValue<List<CuratedObjectsResponseDto>> curatedObjects = | ||||
|         ref.watch(getCuratedObjectProvider); | ||||
|  | ||||
|     useEffect(() { | ||||
|     useEffect( | ||||
|       () { | ||||
|         searchFocusNode = FocusNode(); | ||||
|         return () => searchFocusNode.dispose(); | ||||
|     }, []); | ||||
|       }, | ||||
|       [], | ||||
|     ); | ||||
|  | ||||
|     _onSearchSubmitted(String searchTerm) async { | ||||
|       searchFocusNode.unfocus(); | ||||
| @@ -58,16 +60,16 @@ class SearchPage extends HookConsumerWidget { | ||||
|                     scrollDirection: Axis.horizontal, | ||||
|                     itemCount: curatedLocation.value?.length, | ||||
|                     itemBuilder: ((context, index) { | ||||
|                       CuratedLocation locationInfo = curatedLocations[index]; | ||||
|                       var locationInfo = curatedLocations[index]; | ||||
|                       var thumbnailRequestUrl = | ||||
|                           '${box.get(serverEndpointKey)}/asset/file?aid=${locationInfo.deviceAssetId}&did=${locationInfo.deviceId}&isThumb=true'; | ||||
|  | ||||
|                           '${box.get(serverEndpointKey)}/asset/thumbnail/${locationInfo.id}'; | ||||
|                       return ThumbnailWithInfo( | ||||
|                         imageUrl: thumbnailRequestUrl, | ||||
|                         textInfo: locationInfo.city, | ||||
|                         onTap: () { | ||||
|                           AutoRouter.of(context).push( | ||||
|                               SearchResultRoute(searchTerm: locationInfo.city)); | ||||
|                             SearchResultRoute(searchTerm: locationInfo.city), | ||||
|                           ); | ||||
|                         }, | ||||
|                       ); | ||||
|                     }), | ||||
| @@ -109,7 +111,7 @@ class SearchPage extends HookConsumerWidget { | ||||
|                     scrollDirection: Axis.horizontal, | ||||
|                     itemCount: curatedObjects.value?.length, | ||||
|                     itemBuilder: ((context, index) { | ||||
|                       CuratedObject curatedObjectInfo = objects[index]; | ||||
|                       var curatedObjectInfo = objects[index]; | ||||
|                       var thumbnailRequestUrl = | ||||
|                           '${box.get(serverEndpointKey)}/asset/file?aid=${curatedObjectInfo.deviceAssetId}&did=${curatedObjectInfo.deviceId}&isThumb=true'; | ||||
|  | ||||
| @@ -117,9 +119,12 @@ class SearchPage extends HookConsumerWidget { | ||||
|                         imageUrl: thumbnailRequestUrl, | ||||
|                         textInfo: curatedObjectInfo.object, | ||||
|                         onTap: () { | ||||
|                           AutoRouter.of(context).push(SearchResultRoute( | ||||
|                           AutoRouter.of(context).push( | ||||
|                             SearchResultRoute( | ||||
|                               searchTerm: curatedObjectInfo.object | ||||
|                                   .capitalizeFirstLetter())); | ||||
|                                   .capitalizeFirstLetter(), | ||||
|                             ), | ||||
|                           ); | ||||
|                         }, | ||||
|                       ); | ||||
|                     }), | ||||
| @@ -160,7 +165,7 @@ class SearchPage extends HookConsumerWidget { | ||||
|             ListView( | ||||
|               children: [ | ||||
|                 Padding( | ||||
|                   padding: EdgeInsets.all(16.0), | ||||
|                   padding: const EdgeInsets.all(16.0), | ||||
|                   child: const Text( | ||||
|                     "search_page_places", | ||||
|                     style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16), | ||||
| @@ -168,7 +173,7 @@ class SearchPage extends HookConsumerWidget { | ||||
|                 ), | ||||
|                 _buildPlaces(), | ||||
|                 Padding( | ||||
|                   padding: EdgeInsets.all(16.0), | ||||
|                   padding: const EdgeInsets.all(16.0), | ||||
|                   child: const Text( | ||||
|                     "search_page_things", | ||||
|                     style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16), | ||||
|   | ||||
| @@ -29,13 +29,18 @@ class SearchResultPage extends HookConsumerWidget { | ||||
|  | ||||
|     late FocusNode searchFocusNode; | ||||
|  | ||||
|     useEffect(() { | ||||
|     useEffect( | ||||
|       () { | ||||
|         searchFocusNode = FocusNode(); | ||||
|  | ||||
|       Future.delayed(Duration.zero, | ||||
|           () => ref.read(searchResultPageProvider.notifier).search(searchTerm)); | ||||
|         Future.delayed( | ||||
|           Duration.zero, | ||||
|           () => ref.read(searchResultPageProvider.notifier).search(searchTerm), | ||||
|         ); | ||||
|         return () => searchFocusNode.dispose(); | ||||
|     }, []); | ||||
|       }, | ||||
|       [], | ||||
|     ); | ||||
|  | ||||
|     _onSearchSubmitted(String newSearchTerm) { | ||||
|       debugPrint("Re-Search with $newSearchTerm"); | ||||
| @@ -69,10 +74,10 @@ class SearchResultPage extends HookConsumerWidget { | ||||
|         }, | ||||
|         decoration: InputDecoration( | ||||
|           hintText: 'search_result_page_new_search_hint'.tr(), | ||||
|           enabledBorder: UnderlineInputBorder( | ||||
|           enabledBorder: const UnderlineInputBorder( | ||||
|             borderSide: BorderSide(color: Colors.transparent), | ||||
|           ), | ||||
|           focusedBorder: UnderlineInputBorder( | ||||
|           focusedBorder: const UnderlineInputBorder( | ||||
|             borderSide: BorderSide(color: Colors.transparent), | ||||
|           ), | ||||
|         ), | ||||
| @@ -92,7 +97,8 @@ class SearchResultPage extends HookConsumerWidget { | ||||
|               style: TextStyle( | ||||
|                 color: Theme.of(context).primaryColor, | ||||
|                 fontSize: 13, | ||||
|                   fontWeight: FontWeight.bold), | ||||
|                 fontWeight: FontWeight.bold, | ||||
|               ), | ||||
|               maxLines: 1, | ||||
|             ), | ||||
|             Icon( | ||||
| @@ -118,7 +124,8 @@ class SearchResultPage extends HookConsumerWidget { | ||||
|         return Center( | ||||
|           child: SpinKitDancingSquare( | ||||
|             color: Theme.of(context).primaryColor, | ||||
|         )); | ||||
|           ), | ||||
|         ); | ||||
|       } | ||||
|  | ||||
|       if (searchResultPageState.isSuccess) { | ||||
| @@ -188,7 +195,8 @@ class SearchResultPage extends HookConsumerWidget { | ||||
|             isNewSearch.value = true; | ||||
|             searchFocusNode.requestFocus(); | ||||
|           }, | ||||
|             child: isNewSearch.value ? _buildTextField() : _buildChip()), | ||||
|           child: isNewSearch.value ? _buildTextField() : _buildChip(), | ||||
|         ), | ||||
|         centerTitle: false, | ||||
|       ), | ||||
|       body: GestureDetector( | ||||
|   | ||||
| @@ -1,12 +1,10 @@ | ||||
| import 'dart:convert'; | ||||
|  | ||||
| import 'package:collection/collection.dart'; | ||||
|  | ||||
| import 'package:immich_mobile/shared/models/immich_asset.model.dart'; | ||||
| import 'package:openapi/api.dart'; | ||||
|  | ||||
| class AssetSelectionPageResult { | ||||
|   final Set<ImmichAsset> selectedNewAsset; | ||||
|   final Set<ImmichAsset> selectedAdditionalAsset; | ||||
|   final Set<AssetResponseDto> selectedNewAsset; | ||||
|   final Set<AssetResponseDto> selectedAdditionalAsset; | ||||
|   final bool isAlbumExist; | ||||
|  | ||||
|   AssetSelectionPageResult({ | ||||
| @@ -16,8 +14,8 @@ class AssetSelectionPageResult { | ||||
|   }); | ||||
|  | ||||
|   AssetSelectionPageResult copyWith({ | ||||
|     Set<ImmichAsset>? selectedNewAsset, | ||||
|     Set<ImmichAsset>? selectedAdditionalAsset, | ||||
|     Set<AssetResponseDto>? selectedNewAsset, | ||||
|     Set<AssetResponseDto>? selectedAdditionalAsset, | ||||
|     bool? isAlbumExist, | ||||
|   }) { | ||||
|     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 | ||||
|   String toString() => | ||||
|       'AssetSelectionPageResult(selectedNewAsset: $selectedNewAsset, selectedAdditionalAsset: $selectedAdditionalAsset, isAlbumExist: $isAlbumExist)'; | ||||
|   | ||||
| @@ -1,14 +1,12 @@ | ||||
| import 'dart:convert'; | ||||
|  | ||||
| import 'package:collection/collection.dart'; | ||||
|  | ||||
| import 'package:immich_mobile/shared/models/immich_asset.model.dart'; | ||||
| import 'package:openapi/api.dart'; | ||||
|  | ||||
| class AssetSelectionState { | ||||
|   final Set<String> selectedMonths; | ||||
|   final Set<ImmichAsset> selectedNewAssetsForAlbum; | ||||
|   final Set<ImmichAsset> selectedAdditionalAssetsForAlbum; | ||||
|   final Set<ImmichAsset> selectedAssetsInAlbumViewer; | ||||
|   final Set<AssetResponseDto> selectedNewAssetsForAlbum; | ||||
|   final Set<AssetResponseDto> selectedAdditionalAssetsForAlbum; | ||||
|   final Set<AssetResponseDto> selectedAssetsInAlbumViewer; | ||||
|   final bool isMultiselectEnable; | ||||
|  | ||||
|   /// Indicate the asset selection page is navigated from existing album | ||||
| @@ -24,9 +22,9 @@ class AssetSelectionState { | ||||
|  | ||||
|   AssetSelectionState copyWith({ | ||||
|     Set<String>? selectedMonths, | ||||
|     Set<ImmichAsset>? selectedNewAssetsForAlbum, | ||||
|     Set<ImmichAsset>? selectedAdditionalAssetsForAlbum, | ||||
|     Set<ImmichAsset>? selectedAssetsInAlbumViewer, | ||||
|     Set<AssetResponseDto>? selectedNewAssetsForAlbum, | ||||
|     Set<AssetResponseDto>? selectedAdditionalAssetsForAlbum, | ||||
|     Set<AssetResponseDto>? selectedAssetsInAlbumViewer, | ||||
|     bool? isMultiselectEnable, | ||||
|     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 | ||||
|   String toString() { | ||||
|     return 'AssetSelectionState(selectedMonths: $selectedMonths, selectedNewAssetsForAlbum: $selectedNewAssetsForAlbum, selectedAdditionalAssetsForAlbum: $selectedAdditionalAssetsForAlbum, selectedAssetsInAlbumViewer: $selectedAssetsInAlbumViewer, isMultiselectEnable: $isMultiselectEnable, isAlbumExist: $isAlbumExist)'; | ||||
| @@ -99,10 +54,14 @@ class AssetSelectionState { | ||||
|     return other is AssetSelectionState && | ||||
|         setEquals(other.selectedMonths, selectedMonths) && | ||||
|         setEquals(other.selectedNewAssetsForAlbum, selectedNewAssetsForAlbum) && | ||||
|         setEquals(other.selectedAdditionalAssetsForAlbum, | ||||
|             selectedAdditionalAssetsForAlbum) && | ||||
|         setEquals( | ||||
|             other.selectedAssetsInAlbumViewer, selectedAssetsInAlbumViewer) && | ||||
|           other.selectedAdditionalAssetsForAlbum, | ||||
|           selectedAdditionalAssetsForAlbum, | ||||
|         ) && | ||||
|         setEquals( | ||||
|           other.selectedAssetsInAlbumViewer, | ||||
|           selectedAssetsInAlbumViewer, | ||||
|         ) && | ||||
|         other.isMultiselectEnable == isMultiselectEnable && | ||||
|         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>( | ||||
|     (ref) => AlbumTitleNotifier()); | ||||
|   (ref) => AlbumTitleNotifier(), | ||||
| ); | ||||
|   | ||||
| @@ -30,7 +30,10 @@ class AlbumViewerNotifier extends StateNotifier<AlbumViewerPageState> { | ||||
|   } | ||||
|  | ||||
|   Future<bool> changeAlbumTitle( | ||||
|       String albumId, String ownerId, String newAlbumTitle) async { | ||||
|     String albumId, | ||||
|     String ownerId, | ||||
|     String newAlbumTitle, | ||||
|   ) async { | ||||
|     SharedAlbumService service = ref.watch(sharedAlbumServiceProvider); | ||||
|  | ||||
|     bool isSuccess = | ||||
|   | ||||
| @@ -1,41 +1,46 @@ | ||||
| import 'package:hooks_riverpod/hooks_riverpod.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> { | ||||
|   AssetSelectionNotifier() | ||||
|       : super(AssetSelectionState( | ||||
|       : super( | ||||
|           AssetSelectionState( | ||||
|             selectedNewAssetsForAlbum: {}, | ||||
|             selectedMonths: {}, | ||||
|             selectedAdditionalAssetsForAlbum: {}, | ||||
|             selectedAssetsInAlbumViewer: {}, | ||||
|             isAlbumExist: false, | ||||
|             isMultiselectEnable: false, | ||||
|         )); | ||||
|           ), | ||||
|         ); | ||||
|  | ||||
|   void setIsAlbumExist(bool isAlbumExist) { | ||||
|     state = state.copyWith(isAlbumExist: isAlbumExist); | ||||
|   } | ||||
|  | ||||
|   void removeAssetsInMonth( | ||||
|       String removedMonth, List<ImmichAsset> assetsInMonth) { | ||||
|     Set<ImmichAsset> currentAssetList = state.selectedNewAssetsForAlbum; | ||||
|     String removedMonth, | ||||
|     List<AssetResponseDto> assetsInMonth, | ||||
|   ) { | ||||
|     Set<AssetResponseDto> currentAssetList = state.selectedNewAssetsForAlbum; | ||||
|     Set<String> currentMonthList = state.selectedMonths; | ||||
|  | ||||
|     currentMonthList | ||||
|         .removeWhere((selectedMonth) => selectedMonth == removedMonth); | ||||
|  | ||||
|     for (ImmichAsset asset in assetsInMonth) { | ||||
|     for (AssetResponseDto asset in assetsInMonth) { | ||||
|       currentAssetList.removeWhere((e) => e.id == asset.id); | ||||
|     } | ||||
|  | ||||
|     state = state.copyWith( | ||||
|       selectedNewAssetsForAlbum: currentAssetList, | ||||
|         selectedMonths: currentMonthList); | ||||
|       selectedMonths: currentMonthList, | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   void addAdditionalAssets(List<ImmichAsset> assets) { | ||||
|   void addAdditionalAssets(List<AssetResponseDto> assets) { | ||||
|     state = state.copyWith( | ||||
|       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( | ||||
|       selectedMonths: {...state.selectedMonths, month}, | ||||
|       selectedNewAssetsForAlbum: { | ||||
| @@ -54,7 +59,7 @@ class AssetSelectionNotifier extends StateNotifier<AssetSelectionState> { | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   void addNewAssets(List<ImmichAsset> assets) { | ||||
|   void addNewAssets(List<AssetResponseDto> assets) { | ||||
|     state = state.copyWith( | ||||
|       selectedNewAssetsForAlbum: { | ||||
|         ...state.selectedNewAssetsForAlbum, | ||||
| @@ -63,20 +68,20 @@ class AssetSelectionNotifier extends StateNotifier<AssetSelectionState> { | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   void removeSelectedNewAssets(List<ImmichAsset> assets) { | ||||
|     Set<ImmichAsset> currentList = state.selectedNewAssetsForAlbum; | ||||
|   void removeSelectedNewAssets(List<AssetResponseDto> assets) { | ||||
|     Set<AssetResponseDto> currentList = state.selectedNewAssetsForAlbum; | ||||
|  | ||||
|     for (ImmichAsset asset in assets) { | ||||
|     for (AssetResponseDto asset in assets) { | ||||
|       currentList.removeWhere((e) => e.id == asset.id); | ||||
|     } | ||||
|  | ||||
|     state = state.copyWith(selectedNewAssetsForAlbum: currentList); | ||||
|   } | ||||
|  | ||||
|   void removeSelectedAdditionalAssets(List<ImmichAsset> assets) { | ||||
|     Set<ImmichAsset> currentList = state.selectedAdditionalAssetsForAlbum; | ||||
|   void removeSelectedAdditionalAssets(List<AssetResponseDto> assets) { | ||||
|     Set<AssetResponseDto> currentList = state.selectedAdditionalAssetsForAlbum; | ||||
|  | ||||
|     for (ImmichAsset asset in assets) { | ||||
|     for (AssetResponseDto asset in assets) { | ||||
|       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( | ||||
|       selectedAssetsInAlbumViewer: { | ||||
|         ...state.selectedAssetsInAlbumViewer, | ||||
| @@ -113,10 +118,10 @@ class AssetSelectionNotifier extends StateNotifier<AssetSelectionState> { | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   void removeAssetsInAlbumViewer(List<ImmichAsset> assets) { | ||||
|     Set<ImmichAsset> currentList = state.selectedAssetsInAlbumViewer; | ||||
|   void removeAssetsInAlbumViewer(List<AssetResponseDto> assets) { | ||||
|     Set<AssetResponseDto> currentList = state.selectedAssetsInAlbumViewer; | ||||
|  | ||||
|     for (ImmichAsset asset in assets) { | ||||
|     for (AssetResponseDto asset in assets) { | ||||
|       currentList.removeWhere((e) => e.id == asset.id); | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -1,18 +1,20 @@ | ||||
| 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:openapi/api.dart'; | ||||
|  | ||||
| class SharedAlbumNotifier extends StateNotifier<List<SharedAlbum>> { | ||||
| class SharedAlbumNotifier extends StateNotifier<List<AlbumResponseDto>> { | ||||
|   SharedAlbumNotifier(this._sharedAlbumService) : super([]); | ||||
|  | ||||
|   final SharedAlbumService _sharedAlbumService; | ||||
|  | ||||
|   getAllSharedAlbums() async { | ||||
|     List<SharedAlbum> sharedAlbums = | ||||
|     List<AlbumResponseDto>? sharedAlbums = | ||||
|         await _sharedAlbumService.getAllSharedAlbum(); | ||||
|  | ||||
|     if (sharedAlbums != null) { | ||||
|       state = sharedAlbums; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   Future<bool> deleteAlbum(String albumId) async { | ||||
|     var res = await _sharedAlbumService.deleteAlbum(albumId); | ||||
| @@ -37,7 +39,9 @@ class SharedAlbumNotifier extends StateNotifier<List<SharedAlbum>> { | ||||
|   } | ||||
|  | ||||
|   Future<bool> removeAssetFromAlbum( | ||||
|       String albumId, List<String> assetIds) async { | ||||
|     String albumId, | ||||
|     List<String> assetIds, | ||||
|   ) async { | ||||
|     var res = await _sharedAlbumService.removeAssetFromAlbum(albumId, assetIds); | ||||
|  | ||||
|     if (res) { | ||||
| @@ -49,12 +53,12 @@ class SharedAlbumNotifier extends StateNotifier<List<SharedAlbum>> { | ||||
| } | ||||
|  | ||||
| final sharedAlbumProvider = | ||||
|     StateNotifierProvider<SharedAlbumNotifier, List<SharedAlbum>>((ref) { | ||||
|     StateNotifierProvider<SharedAlbumNotifier, List<AlbumResponseDto>>((ref) { | ||||
|   return SharedAlbumNotifier(ref.watch(sharedAlbumServiceProvider)); | ||||
| }); | ||||
|  | ||||
| final sharedAlbumDetailProvider = FutureProvider.autoDispose | ||||
|     .family<SharedAlbum, String>((ref, albumId) async { | ||||
|     .family<AlbumResponseDto?, String>((ref, albumId) async { | ||||
|   final SharedAlbumService sharedAlbumService = | ||||
|       ref.watch(sharedAlbumServiceProvider); | ||||
|  | ||||
|   | ||||
| @@ -1,10 +1,10 @@ | ||||
| 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:openapi/api.dart'; | ||||
|  | ||||
| final suggestedSharedUsersProvider = | ||||
|     FutureProvider.autoDispose<List<User>>((ref) async { | ||||
|     FutureProvider.autoDispose<List<UserResponseDto>>((ref) async { | ||||
|   UserService userService = ref.watch(userServiceProvider); | ||||
|  | ||||
|   return await userService.getAllUsersInfo(); | ||||
|   return await userService.getAllUsersInfo(isAll: false) ?? []; | ||||
| }); | ||||
|   | ||||
| @@ -1,73 +1,69 @@ | ||||
| import 'dart:async'; | ||||
| import 'dart:convert'; | ||||
|  | ||||
| import 'package:dio/dio.dart'; | ||||
| import 'package:flutter/foundation.dart'; | ||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| import 'package:immich_mobile/modules/sharing/models/shared_album.model.dart'; | ||||
| import 'package:immich_mobile/shared/models/immich_asset.model.dart'; | ||||
| import 'package:immich_mobile/shared/services/network.service.dart'; | ||||
| import 'package:immich_mobile/shared/services/api.service.dart'; | ||||
| import 'package:openapi/api.dart'; | ||||
|  | ||||
| final sharedAlbumServiceProvider = | ||||
|     Provider((ref) => SharedAlbumService(ref.watch(networkServiceProvider))); | ||||
| final sharedAlbumServiceProvider = Provider( | ||||
|   (ref) => SharedAlbumService( | ||||
|     ref.watch(apiServiceProvider), | ||||
|   ), | ||||
| ); | ||||
|  | ||||
| class SharedAlbumService { | ||||
|   final NetworkService _networkService; | ||||
|   SharedAlbumService(this._networkService); | ||||
|   final ApiService _apiService; | ||||
|   SharedAlbumService(this._apiService); | ||||
|  | ||||
|   Future<List<SharedAlbum>> getAllSharedAlbum() async { | ||||
|   Future<List<AlbumResponseDto>?> getAllSharedAlbum() async { | ||||
|     try { | ||||
|       var res = await _networkService.getRequest(url: 'album?shared=true'); | ||||
|       List<dynamic> decodedData = jsonDecode(res.toString()); | ||||
|       List<SharedAlbum> result = | ||||
|           List.from(decodedData.map((e) => SharedAlbum.fromMap(e))); | ||||
|  | ||||
|       return result; | ||||
|       return await _apiService.albumApi.getAllAlbums(shared: true); | ||||
|     } catch (e) { | ||||
|       debugPrint("Error getAllSharedAlbum  ${e.toString()}"); | ||||
|       return null; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|     return []; | ||||
|   } | ||||
|  | ||||
|   Future<bool> createSharedAlbum(String albumName, Set<ImmichAsset> assets, | ||||
|       List<String> sharedUserIds) async { | ||||
|   Future<bool> createSharedAlbum( | ||||
|     String albumName, | ||||
|     Set<AssetResponseDto> assets, | ||||
|     List<String> sharedUserIds, | ||||
|   ) async { | ||||
|     try { | ||||
|       var res = await _networkService.postRequest(url: 'album', data: { | ||||
|         "albumName": albumName, | ||||
|         "sharedWithUserIds": sharedUserIds, | ||||
|         "assetIds": assets.map((asset) => asset.id).toList(), | ||||
|       }); | ||||
|       _apiService.albumApi.createAlbum( | ||||
|         CreateAlbumDto( | ||||
|           albumName: albumName, | ||||
|           assetIds: assets.map((asset) => asset.id).toList(), | ||||
|           sharedWithUserIds: sharedUserIds, | ||||
|         ), | ||||
|       ); | ||||
|  | ||||
|       return res != null; | ||||
|       return true; | ||||
|     } catch (e) { | ||||
|       debugPrint("Error createSharedAlbum  ${e.toString()}"); | ||||
|       return false; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   Future<SharedAlbum> getAlbumDetail(String albumId) async { | ||||
|   Future<AlbumResponseDto?> getAlbumDetail(String albumId) async { | ||||
|     try { | ||||
|       var res = await _networkService.getRequest(url: 'album/$albumId'); | ||||
|       dynamic decodedData = jsonDecode(res.toString()); | ||||
|       SharedAlbum result = SharedAlbum.fromMap(decodedData); | ||||
|  | ||||
|       return result; | ||||
|       return await _apiService.albumApi.getAlbumInfo(albumId); | ||||
|     } catch (e) { | ||||
|       throw Exception('Error getAllSharedAlbum  ${e.toString()}'); | ||||
|       debugPrint('Error [getAlbumDetail] ${e.toString()}'); | ||||
|       return null; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   Future<bool> addAdditionalAssetToAlbum( | ||||
|       Set<ImmichAsset> assets, String albumId) async { | ||||
|     Set<AssetResponseDto> assets, | ||||
|     String albumId, | ||||
|   ) async { | ||||
|     try { | ||||
|       var res = | ||||
|           await _networkService.putRequest(url: 'album/$albumId/assets', data: { | ||||
|         "albumId": albumId, | ||||
|         "assetIds": assets.map((asset) => asset.id).toList(), | ||||
|       }); | ||||
|  | ||||
|       return res != null; | ||||
|       var result = await _apiService.albumApi.addAssetsToAlbum( | ||||
|         albumId, | ||||
|         AddAssetsDto(assetIds: assets.map((asset) => asset.id).toList()), | ||||
|       ); | ||||
|       return result != null; | ||||
|     } catch (e) { | ||||
|       debugPrint("Error addAdditionalAssetToAlbum  ${e.toString()}"); | ||||
|       return false; | ||||
| @@ -75,14 +71,16 @@ class SharedAlbumService { | ||||
|   } | ||||
|  | ||||
|   Future<bool> addAdditionalUserToAlbum( | ||||
|       List<String> sharedUserIds, String albumId) async { | ||||
|     List<String> sharedUserIds, | ||||
|     String albumId, | ||||
|   ) async { | ||||
|     try { | ||||
|       var res = | ||||
|           await _networkService.putRequest(url: 'album/$albumId/users', data: { | ||||
|         "sharedUserIds": sharedUserIds, | ||||
|       }); | ||||
|       var result = await _apiService.albumApi.addUsersToAlbum( | ||||
|         albumId, | ||||
|         AddUsersDto(sharedUserIds: sharedUserIds), | ||||
|       ); | ||||
|  | ||||
|       return res != null; | ||||
|       return result != null; | ||||
|     } catch (e) { | ||||
|       debugPrint("Error addAdditionalUserToAlbum  ${e.toString()}"); | ||||
|       return false; | ||||
| @@ -91,12 +89,7 @@ class SharedAlbumService { | ||||
|  | ||||
|   Future<bool> deleteAlbum(String albumId) async { | ||||
|     try { | ||||
|       Response res = await _networkService.deleteRequest(url: 'album/$albumId'); | ||||
|  | ||||
|       if (res.statusCode != 200) { | ||||
|         return false; | ||||
|       } | ||||
|  | ||||
|       await _apiService.albumApi.deleteAlbum(albumId); | ||||
|       return true; | ||||
|     } catch (e) { | ||||
|       debugPrint("Error deleteAlbum  ${e.toString()}"); | ||||
| @@ -106,12 +99,7 @@ class SharedAlbumService { | ||||
|  | ||||
|   Future<bool> leaveAlbum(String albumId) async { | ||||
|     try { | ||||
|       Response res = | ||||
|           await _networkService.deleteRequest(url: 'album/$albumId/user/me'); | ||||
|  | ||||
|       if (res.statusCode != 200) { | ||||
|         return false; | ||||
|       } | ||||
|       await _apiService.albumApi.removeUserFromAlbum(albumId, "me"); | ||||
|  | ||||
|       return true; | ||||
|     } catch (e) { | ||||
| @@ -121,16 +109,14 @@ class SharedAlbumService { | ||||
|   } | ||||
|  | ||||
|   Future<bool> removeAssetFromAlbum( | ||||
|       String albumId, List<String> assetIds) async { | ||||
|     String albumId, | ||||
|     List<String> assetIds, | ||||
|   ) async { | ||||
|     try { | ||||
|       Response res = await _networkService | ||||
|           .deleteRequest(url: 'album/$albumId/assets', data: { | ||||
|         "assetIds": assetIds, | ||||
|       }); | ||||
|  | ||||
|       if (res.statusCode != 200) { | ||||
|         return false; | ||||
|       } | ||||
|       await _apiService.albumApi.removeAssetFromAlbum( | ||||
|         albumId, | ||||
|         RemoveAssetsDto(assetIds: assetIds), | ||||
|       ); | ||||
|  | ||||
|       return true; | ||||
|     } catch (e) { | ||||
| @@ -140,17 +126,18 @@ class SharedAlbumService { | ||||
|   } | ||||
|  | ||||
|   Future<bool> changeTitleAlbum( | ||||
|       String albumId, String ownerId, String newAlbumTitle) async { | ||||
|     String albumId, | ||||
|     String ownerId, | ||||
|     String newAlbumTitle, | ||||
|   ) async { | ||||
|     try { | ||||
|       Response res = | ||||
|           await _networkService.patchRequest(url: 'album/$albumId/', data: { | ||||
|         "ownerId": ownerId, | ||||
|         "albumName": newAlbumTitle, | ||||
|       }); | ||||
|  | ||||
|       if (res.statusCode != 200) { | ||||
|         return false; | ||||
|       } | ||||
|       await _apiService.albumApi.updateAlbumInfo( | ||||
|         albumId, | ||||
|         UpdateAlbumDto( | ||||
|           ownerId: ownerId, | ||||
|           albumName: newAlbumTitle, | ||||
|         ), | ||||
|       ); | ||||
|  | ||||
|       return true; | ||||
|     } catch (e) { | ||||
|   | ||||
| @@ -5,12 +5,12 @@ class AlbumActionOutlinedButton extends StatelessWidget { | ||||
|   final String labelText; | ||||
|   final IconData iconData; | ||||
|  | ||||
|   const AlbumActionOutlinedButton( | ||||
|       {Key? key, | ||||
|   const AlbumActionOutlinedButton({ | ||||
|     Key? key, | ||||
|     this.onPressed, | ||||
|     required this.labelText, | ||||
|       required this.iconData}) | ||||
|       : super(key: key); | ||||
|     required this.iconData, | ||||
|   }) : super(key: key); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
| @@ -31,7 +31,10 @@ class AlbumActionOutlinedButton extends StatelessWidget { | ||||
|         label: Text( | ||||
|           labelText, | ||||
|           style: const TextStyle( | ||||
|               fontSize: 12, fontWeight: FontWeight.bold, color: Colors.black87), | ||||
|             fontSize: 12, | ||||
|             fontWeight: FontWeight.bold, | ||||
|             color: Colors.black87, | ||||
|           ), | ||||
|         ), | ||||
|         onPressed: onPressed, | ||||
|       ), | ||||
|   | ||||
| @@ -31,7 +31,10 @@ class AlbumTitleTextField extends ConsumerWidget { | ||||
|       }, | ||||
|       focusNode: albumTitleTextFieldFocusNode, | ||||
|       style: TextStyle( | ||||
|           fontSize: 28, color: Colors.grey[700], fontWeight: FontWeight.bold), | ||||
|         fontSize: 28, | ||||
|         color: Colors.grey[700], | ||||
|         fontWeight: FontWeight.bold, | ||||
|       ), | ||||
|       controller: albumTitleController, | ||||
|       onTap: () { | ||||
|         isAlbumTitleTextFieldFocus.value = true; | ||||
|   | ||||
| @@ -4,24 +4,24 @@ import 'package:flutter/material.dart'; | ||||
| import 'package:fluttertoast/fluttertoast.dart'; | ||||
| import 'package:hooks_riverpod/hooks_riverpod.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/asset_selection.provider.dart'; | ||||
| import 'package:immich_mobile/modules/sharing/providers/shared_album.provider.dart'; | ||||
| import 'package:immich_mobile/routing/router.dart'; | ||||
| import 'package:immich_mobile/shared/ui/immich_toast.dart'; | ||||
| import 'package:immich_mobile/shared/views/immich_loading_overlay.dart'; | ||||
| import 'package:openapi/api.dart'; | ||||
|  | ||||
| class AlbumViewerAppbar extends HookConsumerWidget with PreferredSizeWidget { | ||||
|   const AlbumViewerAppbar({ | ||||
|     Key? key, | ||||
|     required AsyncValue<SharedAlbum> albumInfo, | ||||
|     required AsyncValue<AlbumResponseDto?> albumInfo, | ||||
|     required this.userId, | ||||
|     required this.albumId, | ||||
|   })  : _albumInfo = albumInfo, | ||||
|         super(key: key); | ||||
|  | ||||
|   final AsyncValue<SharedAlbum> _albumInfo; | ||||
|   final AsyncValue<AlbumResponseDto?> _albumInfo; | ||||
|   final String userId; | ||||
|   final String albumId; | ||||
|  | ||||
| @@ -105,7 +105,7 @@ class AlbumViewerAppbar extends HookConsumerWidget with PreferredSizeWidget { | ||||
|  | ||||
|     _buildBottomSheetActionButton() { | ||||
|       if (isMultiSelectionEnable) { | ||||
|         if (_albumInfo.asData?.value.ownerId == userId) { | ||||
|         if (_albumInfo.asData?.value?.ownerId == userId) { | ||||
|           return ListTile( | ||||
|             leading: const Icon(Icons.delete_sweep_rounded), | ||||
|             title: const Text( | ||||
| @@ -118,7 +118,7 @@ class AlbumViewerAppbar extends HookConsumerWidget with PreferredSizeWidget { | ||||
|           return const SizedBox(); | ||||
|         } | ||||
|       } else { | ||||
|         if (_albumInfo.asData?.value.ownerId == userId) { | ||||
|         if (_albumInfo.asData?.value?.ownerId == userId) { | ||||
|           return ListTile( | ||||
|             leading: const Icon(Icons.delete_forever_rounded), | ||||
|             title: const Text( | ||||
|   | ||||
| @@ -2,15 +2,17 @@ import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:flutter_hooks/flutter_hooks.dart'; | ||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| import 'package:immich_mobile/modules/sharing/models/shared_album.model.dart'; | ||||
| import 'package:immich_mobile/modules/sharing/providers/album_viewer.provider.dart'; | ||||
| import 'package:openapi/api.dart'; | ||||
|  | ||||
| class AlbumViewerEditableTitle extends HookConsumerWidget { | ||||
|   final SharedAlbum albumInfo; | ||||
|   final AlbumResponseDto albumInfo; | ||||
|   final FocusNode titleFocusNode; | ||||
|   const AlbumViewerEditableTitle( | ||||
|       {Key? key, required this.albumInfo, required this.titleFocusNode}) | ||||
|       : super(key: key); | ||||
|   const AlbumViewerEditableTitle({ | ||||
|     Key? key, | ||||
|     required this.albumInfo, | ||||
|     required this.titleFocusNode, | ||||
|   }) : super(key: key); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context, WidgetRef ref) { | ||||
| @@ -24,12 +26,15 @@ class AlbumViewerEditableTitle extends HookConsumerWidget { | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     useEffect(() { | ||||
|     useEffect( | ||||
|       () { | ||||
|         titleFocusNode.addListener(onFocusModeChange); | ||||
|         return () { | ||||
|           titleFocusNode.removeListener(onFocusModeChange); | ||||
|         }; | ||||
|     }, []); | ||||
|       }, | ||||
|       [], | ||||
|     ); | ||||
|  | ||||
|     return TextField( | ||||
|       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/modules/login/providers/authentication.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:openapi/api.dart'; | ||||
|  | ||||
| class AlbumViewerThumbnail extends HookConsumerWidget { | ||||
|   final ImmichAsset asset; | ||||
|   final AssetResponseDto asset; | ||||
|  | ||||
|   const AlbumViewerThumbnail({Key? key, required this.asset}) : super(key: key); | ||||
|  | ||||
| @@ -20,7 +20,7 @@ class AlbumViewerThumbnail extends HookConsumerWidget { | ||||
|     final cacheKey = useState(1); | ||||
|     var box = Hive.box(userInfoBox); | ||||
|     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; | ||||
|     final selectedAssetsInAlbumViewer = | ||||
|         ref.watch(assetSelectionProvider).selectedAssetsInAlbumViewer; | ||||
| @@ -28,7 +28,7 @@ class AlbumViewerThumbnail extends HookConsumerWidget { | ||||
|         ref.watch(assetSelectionProvider).isMultiselectEnable; | ||||
|  | ||||
|     _viewAsset() { | ||||
|       if (asset.type == 'IMAGE') { | ||||
|       if (asset.type == AssetTypeEnum.IMAGE) { | ||||
|         AutoRouter.of(context).push( | ||||
|           ImageViewerRoute( | ||||
|             imageUrl: | ||||
| @@ -43,7 +43,8 @@ class AlbumViewerThumbnail extends HookConsumerWidget { | ||||
|           VideoViewerRoute( | ||||
|             videoUrl: | ||||
|                 '${box.get(serverEndpointKey)}/asset/file?aid=${asset.deviceAssetId}&did=${asset.deviceId}', | ||||
|               asset: asset), | ||||
|             asset: asset, | ||||
|           ), | ||||
|         ); | ||||
|       } | ||||
|     } | ||||
| @@ -170,17 +171,14 @@ class AlbumViewerThumbnail extends HookConsumerWidget { | ||||
|     return GestureDetector( | ||||
|       onTap: isMultiSelectionEnable ? _handleSelectionGesture : _viewAsset, | ||||
|       onLongPress: _enableMultiSelection, | ||||
|       child: Hero( | ||||
|         tag: asset.id, | ||||
|       child: Stack( | ||||
|         children: [ | ||||
|           _buildThumbnailImage(), | ||||
|           _buildAssetStoreLocationIcon(), | ||||
|             if (asset.type != 'IMAGE') _buildVideoLabel(), | ||||
|           if (asset.type != AssetTypeEnum.IMAGE) _buildVideoLabel(), | ||||
|           if (isMultiSelectionEnable) _buildAssetSelectionIcon(), | ||||
|         ], | ||||
|       ), | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -1,10 +1,10 @@ | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:hooks_riverpod/hooks_riverpod.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 { | ||||
|   final List<ImmichAsset> assetGroup; | ||||
|   final List<AssetResponseDto> assetGroup; | ||||
|   const AssetGridByMonth({Key? key, required this.assetGroup}) | ||||
|       : super(key: key); | ||||
|   @override | ||||
|   | ||||
| @@ -2,15 +2,17 @@ import 'package:flutter/material.dart'; | ||||
| import 'package:flutter/services.dart'; | ||||
| import 'package:hooks_riverpod/hooks_riverpod.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 { | ||||
|   final String month; | ||||
|   final List<ImmichAsset> assetGroup; | ||||
|   final List<AssetResponseDto> assetGroup; | ||||
|  | ||||
|   const MonthGroupTitle( | ||||
|       {Key? key, required this.month, required this.assetGroup}) | ||||
|       : super(key: key); | ||||
|   const MonthGroupTitle({ | ||||
|     Key? key, | ||||
|     required this.month, | ||||
|     required this.assetGroup, | ||||
|   }) : super(key: key); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context, WidgetRef ref) { | ||||
| @@ -75,7 +77,11 @@ class MonthGroupTitle extends HookConsumerWidget { | ||||
|     return SliverToBoxAdapter( | ||||
|       child: Padding( | ||||
|         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( | ||||
|           children: [ | ||||
|             GestureDetector( | ||||
|   | ||||
| @@ -5,10 +5,10 @@ import 'package:hive_flutter/hive_flutter.dart'; | ||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| import 'package:immich_mobile/constants/hive_box.dart'; | ||||
| import 'package:immich_mobile/modules/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 { | ||||
|   final ImmichAsset asset; | ||||
|   final AssetResponseDto asset; | ||||
|  | ||||
|   const SelectionThumbnailImage({Key? key, required this.asset}) | ||||
|       : super(key: key); | ||||
| @@ -18,14 +18,14 @@ class SelectionThumbnailImage extends HookConsumerWidget { | ||||
|     final cacheKey = useState(1); | ||||
|     var box = Hive.box(userInfoBox); | ||||
|     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(assetSelectionProvider).selectedNewAssetsForAlbum; | ||||
|     var newAssetsForAlbum = | ||||
|         ref.watch(assetSelectionProvider).selectedAdditionalAssetsForAlbum; | ||||
|     var isAlbumExist = ref.watch(assetSelectionProvider).isAlbumExist; | ||||
|  | ||||
|     Widget _buildSelectionIcon(ImmichAsset asset) { | ||||
|     Widget _buildSelectionIcon(AssetResponseDto asset) { | ||||
|       if (selectedAsset.contains(asset) && !isAlbumExist) { | ||||
|         return Icon( | ||||
|           Icons.check_circle, | ||||
| @@ -103,7 +103,7 @@ class SelectionThumbnailImage extends HookConsumerWidget { | ||||
|               cacheKey: "${asset.id}-${cacheKey.value}", | ||||
|               width: 150, | ||||
|               height: 150, | ||||
|               memCacheHeight: asset.type == 'IMAGE' ? 150 : 150, | ||||
|               memCacheHeight: asset.type == AssetTypeEnum.IMAGE ? 150 : 150, | ||||
|               fit: BoxFit.cover, | ||||
|               imageUrl: thumbnailRequestUrl, | ||||
|               httpHeaders: { | ||||
| @@ -131,14 +131,14 @@ class SelectionThumbnailImage extends HookConsumerWidget { | ||||
|               child: _buildSelectionIcon(asset), | ||||
|             ), | ||||
|           ), | ||||
|           if (asset.type != 'IMAGE') | ||||
|           if (asset.type != AssetTypeEnum.IMAGE) | ||||
|             Positioned( | ||||
|               bottom: 5, | ||||
|               right: 5, | ||||
|               child: Row( | ||||
|                 children: [ | ||||
|                   Text( | ||||
|                     '${asset.duration?.substring(0, 7)}', | ||||
|                     asset.duration.substring(0, 7), | ||||
|                     style: const TextStyle( | ||||
|                       color: Colors.white, | ||||
|                       fontSize: 10, | ||||
|   | ||||
| @@ -4,10 +4,10 @@ import 'package:flutter_hooks/flutter_hooks.dart'; | ||||
| import 'package:hive_flutter/hive_flutter.dart'; | ||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| import 'package:immich_mobile/constants/hive_box.dart'; | ||||
| import 'package:immich_mobile/shared/models/immich_asset.model.dart'; | ||||
| import 'package:openapi/api.dart'; | ||||
|  | ||||
| class SharedAlbumThumbnailImage extends HookConsumerWidget { | ||||
|   final ImmichAsset asset; | ||||
|   final AssetResponseDto asset; | ||||
|  | ||||
|   const SharedAlbumThumbnailImage({Key? key, required this.asset}) | ||||
|       : super(key: key); | ||||
| @@ -18,7 +18,7 @@ class SharedAlbumThumbnailImage extends HookConsumerWidget { | ||||
|  | ||||
|     var box = Hive.box(userInfoBox); | ||||
|     var thumbnailRequestUrl = | ||||
|         '${box.get(serverEndpointKey)}/asset/file?aid=${asset.deviceAssetId}&did=${asset.deviceId}&isThumb=true'; | ||||
|         '${box.get(serverEndpointKey)}/asset/thumbnail/${asset.id}'; | ||||
|  | ||||
|     return GestureDetector( | ||||
|       onTap: () { | ||||
| @@ -30,7 +30,7 @@ class SharedAlbumThumbnailImage extends HookConsumerWidget { | ||||
|             cacheKey: "${asset.id}-${cacheKey.value}", | ||||
|             width: 500, | ||||
|             height: 500, | ||||
|             memCacheHeight: asset.type == 'IMAGE' ? 500 : 500, | ||||
|             memCacheHeight: 500, | ||||
|             fit: BoxFit.cover, | ||||
|             imageUrl: thumbnailRequestUrl, | ||||
|             httpHeaders: {"Authorization": "Bearer ${box.get(accessTokenKey)}"}, | ||||
|   | ||||
| @@ -40,7 +40,8 @@ class SharingSliverAppBar extends StatelessWidget { | ||||
|                   child: TextButton.icon( | ||||
|                     style: ButtonStyle( | ||||
|                       backgroundColor: MaterialStateProperty.all( | ||||
|                           Theme.of(context).primaryColor.withAlpha(20)), | ||||
|                         Theme.of(context).primaryColor.withAlpha(20), | ||||
|                       ), | ||||
|                       // foregroundColor: MaterialStateProperty.all(Colors.white), | ||||
|                     ), | ||||
|                     onPressed: () { | ||||
| @@ -65,7 +66,8 @@ class SharingSliverAppBar extends StatelessWidget { | ||||
|                   child: TextButton.icon( | ||||
|                     style: ButtonStyle( | ||||
|                       backgroundColor: MaterialStateProperty.all( | ||||
|                           Theme.of(context).primaryColor.withAlpha(20)), | ||||
|                         Theme.of(context).primaryColor.withAlpha(20), | ||||
|                       ), | ||||
|                       // foregroundColor: MaterialStateProperty.all(Colors.white), | ||||
|                     ), | ||||
|                     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/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/shared_album.model.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/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_sliver_persistent_app_bar_delegate.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 { | ||||
|   final String albumId; | ||||
| @@ -30,18 +29,18 @@ class AlbumViewerPage extends HookConsumerWidget { | ||||
|   Widget build(BuildContext context, WidgetRef ref) { | ||||
|     FocusNode titleFocusNode = useFocusNode(); | ||||
|     ScrollController scrollController = useScrollController(); | ||||
|     AsyncValue<SharedAlbum> albumInfo = | ||||
|     AsyncValue<AlbumResponseDto?> albumInfo = | ||||
|         ref.watch(sharedAlbumDetailProvider(albumId)); | ||||
|  | ||||
|     final userId = ref.watch(authenticationProvider).userId; | ||||
|  | ||||
|     /// Find out if the assets in album exist on the device | ||||
|     /// If they exist, add to selected asset state to show they are already selected. | ||||
|     void _onAddPhotosPressed(SharedAlbum albumInfo) async { | ||||
|       if (albumInfo.assets?.isNotEmpty == true) { | ||||
|     void _onAddPhotosPressed(AlbumResponseDto albumInfo) async { | ||||
|       if (albumInfo.assets.isNotEmpty == true) { | ||||
|         ref | ||||
|             .watch(assetSelectionProvider.notifier) | ||||
|             .addNewAssets(albumInfo.assets!.toList()); | ||||
|             .addNewAssets(albumInfo.assets.toList()); | ||||
|       } | ||||
|  | ||||
|       ref.watch(assetSelectionProvider.notifier).setIsAlbumExist(true); | ||||
| @@ -57,7 +56,9 @@ class AlbumViewerPage extends HookConsumerWidget { | ||||
|           var isSuccess = await ref | ||||
|               .watch(sharedAlbumServiceProvider) | ||||
|               .addAdditionalAssetToAlbum( | ||||
|                   returnPayload.selectedAdditionalAsset, albumId); | ||||
|                 returnPayload.selectedAdditionalAsset, | ||||
|                 albumId, | ||||
|               ); | ||||
|  | ||||
|           if (isSuccess) { | ||||
|             ref.refresh(sharedAlbumDetailProvider(albumId)); | ||||
| @@ -72,10 +73,11 @@ class AlbumViewerPage extends HookConsumerWidget { | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     void _onAddUsersPressed(SharedAlbum albumInfo) async { | ||||
|       List<String>? sharedUserIds = await AutoRouter.of(context) | ||||
|           .push<List<String>?>( | ||||
|               SelectAdditionalUserForSharingRoute(albumInfo: albumInfo)); | ||||
|     void _onAddUsersPressed(AlbumResponseDto albumInfo) async { | ||||
|       List<String>? sharedUserIds = | ||||
|           await AutoRouter.of(context).push<List<String>?>( | ||||
|         SelectAdditionalUserForSharingRoute(albumInfo: albumInfo), | ||||
|       ); | ||||
|  | ||||
|       if (sharedUserIds != null) { | ||||
|         ImmichLoadingOverlayController.appLoader.show(); | ||||
| @@ -92,7 +94,7 @@ class AlbumViewerPage extends HookConsumerWidget { | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     Widget _buildTitle(SharedAlbum albumInfo) { | ||||
|     Widget _buildTitle(AlbumResponseDto albumInfo) { | ||||
|       return Padding( | ||||
|         padding: const EdgeInsets.only(left: 8, right: 8, top: 16), | ||||
|         child: userId == albumInfo.ownerId | ||||
| @@ -102,19 +104,24 @@ class AlbumViewerPage extends HookConsumerWidget { | ||||
|               ) | ||||
|             : Padding( | ||||
|                 padding: const EdgeInsets.only(left: 8.0), | ||||
|                 child: Text(albumInfo.albumName, | ||||
|                 child: Text( | ||||
|                   albumInfo.albumName, | ||||
|                   style: const TextStyle( | ||||
|                         fontSize: 24, fontWeight: FontWeight.bold)), | ||||
|                     fontSize: 24, | ||||
|                     fontWeight: FontWeight.bold, | ||||
|                   ), | ||||
|                 ), | ||||
|               ), | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     Widget _buildAlbumDateRange(SharedAlbum albumInfo) { | ||||
|     Widget _buildAlbumDateRange(AlbumResponseDto albumInfo) { | ||||
|       String startDate = ""; | ||||
|       DateTime parsedStartDate = | ||||
|           DateTime.parse(albumInfo.assets!.first.createdAt); | ||||
|           DateTime.parse(albumInfo.assets.first.createdAt); | ||||
|       DateTime parsedEndDate = DateTime.parse( | ||||
|           albumInfo.assets?.last.createdAt ?? '11111111'); //Need default. | ||||
|         albumInfo.assets.last.createdAt, | ||||
|       ); //Need default. | ||||
|  | ||||
|       if (parsedStartDate.year == parsedEndDate.year) { | ||||
|         startDate = DateFormat('LLL d').format(parsedStartDate); | ||||
| @@ -129,18 +136,21 @@ class AlbumViewerPage extends HookConsumerWidget { | ||||
|         child: Text( | ||||
|           "$startDate-$endDate", | ||||
|           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( | ||||
|         child: Column( | ||||
|           crossAxisAlignment: CrossAxisAlignment.start, | ||||
|           children: [ | ||||
|             _buildTitle(albumInfo), | ||||
|             if (albumInfo.assets?.isNotEmpty == true) | ||||
|             if (albumInfo.assets.isNotEmpty == true) | ||||
|               _buildAlbumDateRange(albumInfo), | ||||
|             SizedBox( | ||||
|               height: 60, | ||||
| @@ -172,8 +182,8 @@ class AlbumViewerPage extends HookConsumerWidget { | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     Widget _buildImageGrid(SharedAlbum albumInfo) { | ||||
|       if (albumInfo.assets?.isNotEmpty == true) { | ||||
|     Widget _buildImageGrid(AlbumResponseDto albumInfo) { | ||||
|       if (albumInfo.assets.isNotEmpty) { | ||||
|         return SliverPadding( | ||||
|           padding: const EdgeInsets.only(top: 10.0), | ||||
|           sliver: SliverGrid( | ||||
| @@ -184,9 +194,9 @@ class AlbumViewerPage extends HookConsumerWidget { | ||||
|             ), | ||||
|             delegate: SliverChildBuilderDelegate( | ||||
|               (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(); | ||||
|     } | ||||
|  | ||||
|     Widget _buildControlButton(SharedAlbum albumInfo) { | ||||
|     Widget _buildControlButton(AlbumResponseDto albumInfo) { | ||||
|       return Padding( | ||||
|         padding: const EdgeInsets.only(left: 16.0, top: 8, bottom: 8), | ||||
|         child: SizedBox( | ||||
| @@ -219,7 +229,7 @@ class AlbumViewerPage extends HookConsumerWidget { | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     Widget _buildBody(SharedAlbum albumInfo) { | ||||
|     Widget _buildBody(AlbumResponseDto albumInfo) { | ||||
|       return GestureDetector( | ||||
|         onTap: () { | ||||
|           titleFocusNode.unfocus(); | ||||
| @@ -252,9 +262,16 @@ class AlbumViewerPage extends HookConsumerWidget { | ||||
|  | ||||
|     return Scaffold( | ||||
|       appBar: AlbumViewerAppbar( | ||||
|           albumInfo: albumInfo, userId: userId, albumId: albumId), | ||||
|         albumInfo: albumInfo, | ||||
|         userId: userId, | ||||
|         albumId: albumId, | ||||
|       ), | ||||
|       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")), | ||||
|         loading: () => const Center( | ||||
|           child: ImmichLoadingIndicator(), | ||||
|   | ||||
| @@ -59,7 +59,8 @@ class CreateSharedAlbumPage extends HookConsumerWidget { | ||||
|           isAlbumTitleEmpty: isAlbumTitleEmpty, | ||||
|           albumTitleTextFieldFocusNode: albumTitleTextFieldFocusNode, | ||||
|           albumTitleController: albumTitleController, | ||||
|             isAlbumTitleTextFieldFocus: isAlbumTitleTextFieldFocus), | ||||
|           isAlbumTitleTextFieldFocus: isAlbumTitleTextFieldFocus, | ||||
|         ), | ||||
|       ); | ||||
|     } | ||||
|  | ||||
| @@ -67,8 +68,8 @@ class CreateSharedAlbumPage extends HookConsumerWidget { | ||||
|       if (selectedAssets.isEmpty) { | ||||
|         return SliverToBoxAdapter( | ||||
|           child: Padding( | ||||
|             padding: EdgeInsets.only(top: 200, left: 18), | ||||
|             child: Text( | ||||
|             padding: const EdgeInsets.only(top: 200, left: 18), | ||||
|             child: const Text( | ||||
|               'create_shared_album_page_share_add_assets', | ||||
|               style: TextStyle(fontSize: 12), | ||||
|             ).tr(), | ||||
| @@ -90,9 +91,12 @@ class CreateSharedAlbumPage extends HookConsumerWidget { | ||||
|                 padding: | ||||
|                     const EdgeInsets.symmetric(vertical: 22, horizontal: 16), | ||||
|                 side: const BorderSide( | ||||
|                       color: Color.fromARGB(255, 206, 206, 206)), | ||||
|                   color: Color.fromARGB(255, 206, 206, 206), | ||||
|                 ), | ||||
|                 shape: RoundedRectangleBorder( | ||||
|                       borderRadius: BorderRadius.circular(5))), | ||||
|                   borderRadius: BorderRadius.circular(5), | ||||
|                 ), | ||||
|               ), | ||||
|               onPressed: _onSelectPhotosButtonPressed, | ||||
|               icon: const Icon(Icons.add_rounded), | ||||
|               label: Padding( | ||||
| @@ -102,7 +106,8 @@ class CreateSharedAlbumPage extends HookConsumerWidget { | ||||
|                   style: TextStyle( | ||||
|                     fontSize: 16, | ||||
|                     color: Colors.grey[700], | ||||
|                       fontWeight: FontWeight.bold), | ||||
|                     fontWeight: FontWeight.bold, | ||||
|                   ), | ||||
|                 ).tr(), | ||||
|               ), | ||||
|             ), | ||||
| @@ -147,7 +152,8 @@ class CreateSharedAlbumPage extends HookConsumerWidget { | ||||
|                 return GestureDetector( | ||||
|                   onTap: _onBackgroundTapped, | ||||
|                   child: SharedAlbumThumbnailImage( | ||||
|                       asset: selectedAssets.toList()[index]), | ||||
|                     asset: selectedAssets.toList()[index], | ||||
|                   ), | ||||
|                 ); | ||||
|               }, | ||||
|               childCount: selectedAssets.length, | ||||
| @@ -168,7 +174,8 @@ class CreateSharedAlbumPage extends HookConsumerWidget { | ||||
|             ref.watch(assetSelectionProvider.notifier).removeAll(); | ||||
|             AutoRouter.of(context).pop(); | ||||
|           }, | ||||
|               icon: const Icon(Icons.close_rounded)), | ||||
|           icon: const Icon(Icons.close_rounded), | ||||
|         ), | ||||
|         title: const Text( | ||||
|           'share_create_album', | ||||
|           style: TextStyle(color: Colors.black), | ||||
| @@ -180,7 +187,7 @@ class CreateSharedAlbumPage extends HookConsumerWidget { | ||||
|                 : null, | ||||
|             child: Text( | ||||
|               'create_shared_album_page_share'.tr(), | ||||
|                 style: TextStyle( | ||||
|               style: const TextStyle( | ||||
|                 fontWeight: FontWeight.bold, | ||||
|               ), | ||||
|             ), | ||||
| @@ -212,6 +219,7 @@ class CreateSharedAlbumPage extends HookConsumerWidget { | ||||
|             _buildSelectedImageGrid(), | ||||
|           ], | ||||
|         ), | ||||
|         )); | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -3,29 +3,28 @@ import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:flutter_hooks/flutter_hooks.dart'; | ||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| import 'package:immich_mobile/modules/sharing/models/shared_album.model.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:openapi/api.dart'; | ||||
|  | ||||
| class SelectAdditionalUserForSharingPage extends HookConsumerWidget { | ||||
|   final SharedAlbum albumInfo; | ||||
|   final AlbumResponseDto albumInfo; | ||||
|  | ||||
|   const SelectAdditionalUserForSharingPage({Key? key, required this.albumInfo}) | ||||
|       : super(key: key); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context, WidgetRef ref) { | ||||
|     AsyncValue<List<User>> suggestedShareUsers = | ||||
|     AsyncValue<List<UserResponseDto>> suggestedShareUsers = | ||||
|         ref.watch(suggestedSharedUsersProvider); | ||||
|     final sharedUsersList = useState<Set<User>>({}); | ||||
|     final sharedUsersList = useState<Set<UserResponseDto>>({}); | ||||
|  | ||||
|     _addNewUsersHandler() { | ||||
|       AutoRouter.of(context) | ||||
|           .pop(sharedUsersList.value.map((e) => e.id).toList()); | ||||
|     } | ||||
|  | ||||
|     _buildTileIcon(User user) { | ||||
|     _buildTileIcon(UserResponseDto user) { | ||||
|       if (sharedUsersList.value.contains(user)) { | ||||
|         return CircleAvatar( | ||||
|           backgroundColor: Theme.of(context).primaryColor, | ||||
| @@ -43,7 +42,7 @@ class SelectAdditionalUserForSharingPage extends HookConsumerWidget { | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     _buildUserList(List<User> users) { | ||||
|     _buildUserList(List<UserResponseDto> users) { | ||||
|       List<Widget> usersChip = []; | ||||
|  | ||||
|       for (var user in sharedUsersList.value) { | ||||
| @@ -57,7 +56,8 @@ class SelectAdditionalUserForSharingPage extends HookConsumerWidget { | ||||
|                 style: const TextStyle( | ||||
|                   fontSize: 12, | ||||
|                   color: Colors.black87, | ||||
|                     fontWeight: FontWeight.bold), | ||||
|                   fontWeight: FontWeight.bold, | ||||
|                 ), | ||||
|               ), | ||||
|             ), | ||||
|           ), | ||||
| @@ -70,13 +70,14 @@ class SelectAdditionalUserForSharingPage extends HookConsumerWidget { | ||||
|             children: [...usersChip], | ||||
|           ), | ||||
|           Padding( | ||||
|             padding: EdgeInsets.all(16.0), | ||||
|             padding: const EdgeInsets.all(16.0), | ||||
|             child: Text( | ||||
|               'select_additional_user_for_sharing_page_suggestions'.tr(), | ||||
|               style: TextStyle( | ||||
|               style: const TextStyle( | ||||
|                 fontSize: 14, | ||||
|                 color: Colors.grey, | ||||
|                   fontWeight: FontWeight.bold), | ||||
|                 fontWeight: FontWeight.bold, | ||||
|               ), | ||||
|             ), | ||||
|           ), | ||||
|           ListView.builder( | ||||
| @@ -87,13 +88,16 @@ class SelectAdditionalUserForSharingPage extends HookConsumerWidget { | ||||
|                 title: Text( | ||||
|                   users[index].email, | ||||
|                   style: const TextStyle( | ||||
|                       fontSize: 14, fontWeight: FontWeight.bold), | ||||
|                     fontSize: 14, | ||||
|                     fontWeight: FontWeight.bold, | ||||
|                   ), | ||||
|                 ), | ||||
|                 onTap: () { | ||||
|                   if (sharedUsersList.value.contains(users[index])) { | ||||
|                     sharedUsersList.value = sharedUsersList.value | ||||
|                         .where((selectedUser) => | ||||
|                             selectedUser.id != users[index].id) | ||||
|                         .where( | ||||
|                           (selectedUser) => selectedUser.id != users[index].id, | ||||
|                         ) | ||||
|                         .toSet(); | ||||
|                   } else { | ||||
|                     sharedUsersList.value = { | ||||
| @@ -139,7 +143,8 @@ class SelectAdditionalUserForSharingPage extends HookConsumerWidget { | ||||
|         data: (users) { | ||||
|           for (var sharedUsers in albumInfo.sharedUsers) { | ||||
|             users.removeWhere( | ||||
|                 (u) => u.id == sharedUsers.id || u.id == albumInfo.ownerId); | ||||
|               (u) => u.id == sharedUsers.id || u.id == albumInfo.ownerId, | ||||
|             ); | ||||
|           } | ||||
|  | ||||
|           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/services/shared_album.service.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:openapi/api.dart'; | ||||
|  | ||||
| class SelectUserForSharingPage extends HookConsumerWidget { | ||||
|   const SelectUserForSharingPage({Key? key}) : super(key: key); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context, WidgetRef ref) { | ||||
|     final sharedUsersList = useState<Set<User>>({}); | ||||
|     AsyncValue<List<User>> suggestedShareUsers = | ||||
|     final sharedUsersList = useState<Set<UserResponseDto>>({}); | ||||
|     AsyncValue<List<UserResponseDto>> suggestedShareUsers = | ||||
|         ref.watch(suggestedSharedUsersProvider); | ||||
|  | ||||
|     _createSharedAlbum() async { | ||||
| @@ -37,10 +38,14 @@ class SelectUserForSharingPage extends HookConsumerWidget { | ||||
|             .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)) { | ||||
|         return CircleAvatar( | ||||
|           backgroundColor: Theme.of(context).primaryColor, | ||||
| @@ -58,7 +63,7 @@ class SelectUserForSharingPage extends HookConsumerWidget { | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     _buildUserList(List<User> users) { | ||||
|     _buildUserList(List<UserResponseDto> users) { | ||||
|       List<Widget> usersChip = []; | ||||
|  | ||||
|       for (var user in sharedUsersList.value) { | ||||
| @@ -72,7 +77,8 @@ class SelectUserForSharingPage extends HookConsumerWidget { | ||||
|                 style: const TextStyle( | ||||
|                   fontSize: 12, | ||||
|                   color: Colors.black87, | ||||
|                     fontWeight: FontWeight.bold), | ||||
|                   fontWeight: FontWeight.bold, | ||||
|                 ), | ||||
|               ), | ||||
|             ), | ||||
|           ), | ||||
| @@ -85,13 +91,14 @@ class SelectUserForSharingPage extends HookConsumerWidget { | ||||
|             children: [...usersChip], | ||||
|           ), | ||||
|           Padding( | ||||
|             padding: EdgeInsets.all(16.0), | ||||
|             child: Text( | ||||
|               'share_suggestions', | ||||
|             padding: const EdgeInsets.all(16.0), | ||||
|             child: const Text( | ||||
|               'select_user_for_sharing_page_share_suggestions', | ||||
|               style: TextStyle( | ||||
|                 fontSize: 14, | ||||
|                 color: Colors.grey, | ||||
|                   fontWeight: FontWeight.bold), | ||||
|                 fontWeight: FontWeight.bold, | ||||
|               ), | ||||
|             ).tr(), | ||||
|           ), | ||||
|           ListView.builder( | ||||
| @@ -102,13 +109,16 @@ class SelectUserForSharingPage extends HookConsumerWidget { | ||||
|                 title: Text( | ||||
|                   users[index].email, | ||||
|                   style: const TextStyle( | ||||
|                       fontSize: 14, fontWeight: FontWeight.bold), | ||||
|                     fontSize: 14, | ||||
|                     fontWeight: FontWeight.bold, | ||||
|                   ), | ||||
|                 ), | ||||
|                 onTap: () { | ||||
|                   if (sharedUsersList.value.contains(users[index])) { | ||||
|                     sharedUsersList.value = sharedUsersList.value | ||||
|                         .where((selectedUser) => | ||||
|                             selectedUser.id != users[index].id) | ||||
|                         .where( | ||||
|                           (selectedUser) => selectedUser.id != users[index].id, | ||||
|                         ) | ||||
|                         .toSet(); | ||||
|                   } else { | ||||
|                     sharedUsersList.value = { | ||||
| @@ -146,7 +156,8 @@ class SelectUserForSharingPage extends HookConsumerWidget { | ||||
|             child: const Text( | ||||
|               "share_create_album", | ||||
|               style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold), | ||||
|               ).tr()) | ||||
|             ).tr(), | ||||
|           ) | ||||
|         ], | ||||
|       ), | ||||
|       body: suggestedShareUsers.when( | ||||
|   | ||||
| @@ -5,10 +5,10 @@ import 'package:flutter_hooks/flutter_hooks.dart'; | ||||
| import 'package:hive/hive.dart'; | ||||
| import 'package:hooks_riverpod/hooks_riverpod.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/ui/sharing_sliver_appbar.dart'; | ||||
| import 'package:immich_mobile/routing/router.dart'; | ||||
| import 'package:openapi/api.dart'; | ||||
| import 'package:transparent_image/transparent_image.dart'; | ||||
|  | ||||
| class SharingPage extends HookConsumerWidget { | ||||
| @@ -18,13 +18,16 @@ class SharingPage extends HookConsumerWidget { | ||||
|   Widget build(BuildContext context, WidgetRef ref) { | ||||
|     var box = Hive.box(userInfoBox); | ||||
|     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(); | ||||
|  | ||||
|         return null; | ||||
|     }, []); | ||||
|       }, | ||||
|       [], | ||||
|     ); | ||||
|  | ||||
|     _buildAlbumList() { | ||||
|       return SliverList( | ||||
| @@ -62,7 +65,8 @@ class SharingPage extends HookConsumerWidget { | ||||
|                 style: TextStyle( | ||||
|                   fontSize: 16, | ||||
|                   fontWeight: FontWeight.bold, | ||||
|                     color: Colors.grey.shade800), | ||||
|                   color: Colors.grey.shade800, | ||||
|                 ), | ||||
|               ), | ||||
|               onTap: () { | ||||
|                 AutoRouter.of(context) | ||||
| @@ -133,9 +137,9 @@ class SharingPage extends HookConsumerWidget { | ||||
|         slivers: [ | ||||
|           const SharingSliverAppBar(), | ||||
|           SliverPadding( | ||||
|             padding: EdgeInsets.symmetric(horizontal: 12, vertical: 12), | ||||
|             padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12), | ||||
|             sliver: SliverToBoxAdapter( | ||||
|               child: Text( | ||||
|               child: const Text( | ||||
|                 "sharing_page_album", | ||||
|                 style: TextStyle( | ||||
|                   fontWeight: FontWeight.bold, | ||||
|   | ||||
| @@ -1,21 +1,25 @@ | ||||
| import 'dart:convert'; | ||||
|  | ||||
| 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 { | ||||
|   final NetworkService _networkService = NetworkService(); | ||||
|  | ||||
|   final ApiService _apiService; | ||||
|   AuthGuard(this._apiService); | ||||
|   @override | ||||
|   void onNavigation(NavigationResolver resolver, StackRouter router) async { | ||||
|     try { | ||||
|       var res = await _networkService.postRequest(url: 'auth/validateToken'); | ||||
|       var jsonReponse = jsonDecode(res.toString()); | ||||
|       if (jsonReponse['authStatus']) { | ||||
|       var res = await _apiService.authenticationApi.validateAccessToken(); | ||||
|  | ||||
|       if (res != null && res.authStatus) { | ||||
|         resolver.next(true); | ||||
|       } else { | ||||
|         router.replaceAll([const LoginRoute()]); | ||||
|       } | ||||
|     } 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: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/backup_album_selection_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_result_page.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/asset_selection_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/sharing_page.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/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/tab_controller_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'; | ||||
|  | ||||
| part 'router.gr.dart'; | ||||
| @@ -74,5 +75,9 @@ part 'router.gr.dart'; | ||||
|   ], | ||||
| ) | ||||
| 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 heroTag, | ||||
|       required String thumbnailUrl, | ||||
|       required ImmichAsset asset}) | ||||
|       required AssetResponseDto asset}) | ||||
|       : super(ImageViewerRoute.name, | ||||
|             path: '/image-viewer-page', | ||||
|             args: ImageViewerRouteArgs( | ||||
| @@ -263,7 +263,7 @@ class ImageViewerRouteArgs { | ||||
|  | ||||
|   final String thumbnailUrl; | ||||
|  | ||||
|   final ImmichAsset asset; | ||||
|   final AssetResponseDto asset; | ||||
|  | ||||
|   @override | ||||
|   String toString() { | ||||
| @@ -275,7 +275,7 @@ class ImageViewerRouteArgs { | ||||
| /// [VideoViewerPage] | ||||
| class VideoViewerRoute extends PageRouteInfo<VideoViewerRouteArgs> { | ||||
|   VideoViewerRoute( | ||||
|       {Key? key, required String videoUrl, required ImmichAsset asset}) | ||||
|       {Key? key, required String videoUrl, required AssetResponseDto asset}) | ||||
|       : super(VideoViewerRoute.name, | ||||
|             path: '/video-viewer-page', | ||||
|             args: VideoViewerRouteArgs( | ||||
| @@ -292,7 +292,7 @@ class VideoViewerRouteArgs { | ||||
|  | ||||
|   final String videoUrl; | ||||
|  | ||||
|   final ImmichAsset asset; | ||||
|   final AssetResponseDto asset; | ||||
|  | ||||
|   @override | ||||
|   String toString() { | ||||
| @@ -390,7 +390,7 @@ class AlbumViewerRouteArgs { | ||||
| class SelectAdditionalUserForSharingRoute | ||||
|     extends PageRouteInfo<SelectAdditionalUserForSharingRouteArgs> { | ||||
|   SelectAdditionalUserForSharingRoute( | ||||
|       {Key? key, required SharedAlbum albumInfo}) | ||||
|       {Key? key, required AlbumResponseDto albumInfo}) | ||||
|       : super(SelectAdditionalUserForSharingRoute.name, | ||||
|             path: '/select-additional-user-for-sharing-page', | ||||
|             args: SelectAdditionalUserForSharingRouteArgs( | ||||
| @@ -405,7 +405,7 @@ class SelectAdditionalUserForSharingRouteArgs { | ||||
|  | ||||
|   final Key? key; | ||||
|  | ||||
|   final SharedAlbum albumInfo; | ||||
|   final AlbumResponseDto albumInfo; | ||||
|  | ||||
|   @override | ||||
|   String toString() { | ||||
|   | ||||
| @@ -23,7 +23,9 @@ class TabNavigationObserver extends AutoRouterObserver { | ||||
|  | ||||
|   @override | ||||
|   Future<void> didChangeTabRoute( | ||||
|       TabPageRoute route, TabPageRoute previousRoute) async { | ||||
|     TabPageRoute route, | ||||
|     TabPageRoute previousRoute, | ||||
|   ) async { | ||||
|     // Perform tasks on re-visit to SearchRoute | ||||
|     if (route.name == 'SearchRoute') { | ||||
|       // 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:immich_mobile/shared/models/mapbox_info.model.dart'; | ||||
| import 'package:immich_mobile/shared/models/server_version.model.dart'; | ||||
| import 'package:openapi/api.dart'; | ||||
|  | ||||
| class ServerInfoState { | ||||
|   final MapboxInfo mapboxInfo; | ||||
|   final ServerVersion serverVersion; | ||||
|   final ServerVersionReponseDto serverVersion; | ||||
|   final bool isVersionMismatch; | ||||
|   final String versionMismatchErrorMessage; | ||||
|  | ||||
|   ServerInfoState({ | ||||
|     required this.mapboxInfo, | ||||
|     required this.serverVersion, | ||||
|     required this.isVersionMismatch, | ||||
|     required this.versionMismatchErrorMessage, | ||||
|   }); | ||||
|  | ||||
|   ServerInfoState copyWith({ | ||||
|     MapboxInfo? mapboxInfo, | ||||
|     ServerVersion? serverVersion, | ||||
|     ServerVersionReponseDto? serverVersion, | ||||
|     bool? isVersionMismatch, | ||||
|     String? versionMismatchErrorMessage, | ||||
|   }) { | ||||
|     return ServerInfoState( | ||||
|       mapboxInfo: mapboxInfo ?? this.mapboxInfo, | ||||
|       serverVersion: serverVersion ?? this.serverVersion, | ||||
|       isVersionMismatch: isVersionMismatch ?? this.isVersionMismatch, | ||||
|       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 | ||||
|   String toString() { | ||||
|     return 'ServerInfoState(mapboxInfo: $mapboxInfo, serverVersion: $serverVersion, isVersionMismatch: $isVersionMismatch, versionMismatchErrorMessage: $versionMismatchErrorMessage)'; | ||||
|     return 'ServerInfoState( serverVersion: $serverVersion, isVersionMismatch: $isVersionMismatch, versionMismatchErrorMessage: $versionMismatchErrorMessage)'; | ||||
|   } | ||||
|  | ||||
|   @override | ||||
| @@ -64,7 +34,6 @@ class ServerInfoState { | ||||
|     if (identical(this, other)) return true; | ||||
|  | ||||
|     return other is ServerInfoState && | ||||
|         other.mapboxInfo == mapboxInfo && | ||||
|         other.serverVersion == serverVersion && | ||||
|         other.isVersionMismatch == isVersionMismatch && | ||||
|         other.versionMismatchErrorMessage == versionMismatchErrorMessage; | ||||
| @@ -72,8 +41,7 @@ class ServerInfoState { | ||||
|  | ||||
|   @override | ||||
|   int get hashCode { | ||||
|     return mapboxInfo.hashCode ^ | ||||
|         serverVersion.hashCode ^ | ||||
|     return serverVersion.hashCode ^ | ||||
|         isVersionMismatch.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: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/shared/models/immich_asset.model.dart'; | ||||
| import 'package:immich_mobile/shared/services/device_info.service.dart'; | ||||
| import 'package:collection/collection.dart'; | ||||
| import 'package:intl/intl.dart'; | ||||
| import 'package:openapi/api.dart'; | ||||
| import 'package:photo_manager/photo_manager.dart'; | ||||
|  | ||||
| class AssetNotifier extends StateNotifier<List<ImmichAsset>> { | ||||
| class AssetNotifier extends StateNotifier<List<AssetResponseDto>> { | ||||
|   final AssetService _assetService; | ||||
|   final DeviceInfoService _deviceInfoService = DeviceInfoService(); | ||||
|  | ||||
|   AssetNotifier(this._assetService) : super([]); | ||||
|  | ||||
|   getAllAsset() async { | ||||
|     List<ImmichAsset>? allAssets = await _assetService.getAllAsset(); | ||||
|     var allAssets = await _assetService.getAllAsset(); | ||||
|  | ||||
|     if (allAssets != null) { | ||||
|       state = allAssets; | ||||
| @@ -26,11 +25,11 @@ class AssetNotifier extends StateNotifier<List<ImmichAsset>> { | ||||
|     state = []; | ||||
|   } | ||||
|  | ||||
|   onNewAssetUploaded(ImmichAsset newAsset) { | ||||
|   onNewAssetUploaded(AssetResponseDto newAsset) { | ||||
|     state = [...state, newAsset]; | ||||
|   } | ||||
|  | ||||
|   deleteAssets(Set<ImmichAsset> deleteAssets) async { | ||||
|   deleteAssets(Set<AssetResponseDto> deleteAssets) async { | ||||
|     var deviceInfo = await _deviceInfoService.getDeviceInfo(); | ||||
|     var deviceId = deviceInfo["deviceId"]; | ||||
|     var deleteIdList = <String>[]; | ||||
| @@ -53,14 +52,15 @@ class AssetNotifier extends StateNotifier<List<ImmichAsset>> { | ||||
|     } | ||||
|  | ||||
|     // Delete asset on server | ||||
|     List<DeleteAssetResponse>? deleteAssetResult = | ||||
|     List<DeleteAssetResponseDto>? deleteAssetResult = | ||||
|         await _assetService.deleteAssets(deleteAssets); | ||||
|  | ||||
|     if (deleteAssetResult == null) { | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     for (var asset in deleteAssetResult) { | ||||
|       if (asset.status == 'success') { | ||||
|       if (asset.status == DeleteAssetStatus.SUCCESS) { | ||||
|         state = | ||||
|             state.where((immichAsset) => immichAsset.id != asset.id).toList(); | ||||
|       } | ||||
| @@ -69,7 +69,7 @@ class AssetNotifier extends StateNotifier<List<ImmichAsset>> { | ||||
| } | ||||
|  | ||||
| final assetProvider = | ||||
|     StateNotifierProvider<AssetNotifier, List<ImmichAsset>>((ref) { | ||||
|     StateNotifierProvider<AssetNotifier, List<AssetResponseDto>>((ref) { | ||||
|   return AssetNotifier(ref.watch(assetServiceProvider)); | ||||
| }); | ||||
|  | ||||
| @@ -77,17 +77,25 @@ final assetGroupByDateTimeProvider = StateProvider((ref) { | ||||
|   var assets = ref.watch(assetProvider); | ||||
|  | ||||
|   assets.sortByCompare<DateTime>( | ||||
|       (e) => DateTime.parse(e.createdAt), (a, b) => b.compareTo(a)); | ||||
|   return assets.groupListsBy((element) => | ||||
|       DateFormat('y-MM-dd').format(DateTime.parse(element.createdAt))); | ||||
|     (e) => DateTime.parse(e.createdAt), | ||||
|     (a, b) => b.compareTo(a), | ||||
|   ); | ||||
|   return assets.groupListsBy( | ||||
|     (element) => | ||||
|         DateFormat('y-MM-dd').format(DateTime.parse(element.createdAt)), | ||||
|   ); | ||||
| }); | ||||
|  | ||||
| final assetGroupByMonthYearProvider = StateProvider((ref) { | ||||
|   var assets = ref.watch(assetProvider); | ||||
|  | ||||
|   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) => | ||||
|       DateFormat('MMMM, y').format(DateTime.parse(element.createdAt))); | ||||
|   return assets.groupListsBy( | ||||
|     (element) => | ||||
|         DateFormat('MMMM, y').format(DateTime.parse(element.createdAt)), | ||||
|   ); | ||||
| }); | ||||
|   | ||||
| @@ -56,4 +56,5 @@ class ReleaseInfoNotifier extends StateNotifier<String> { | ||||
| } | ||||
|  | ||||
| final releaseInfoProvider = StateNotifierProvider<ReleaseInfoNotifier, String>( | ||||
|     (ref) => ReleaseInfoNotifier()); | ||||
|   (ref) => ReleaseInfoNotifier(), | ||||
| ); | ||||
|   | ||||
| @@ -1,18 +1,20 @@ | ||||
| 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_version.model.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'; | ||||
|  | ||||
| class ServerInfoNotifier extends StateNotifier<ServerInfoState> { | ||||
|   ServerInfoNotifier(this._serverInfoService) | ||||
|       : super( | ||||
|           ServerInfoState( | ||||
|             mapboxInfo: MapboxInfo(isEnable: false, mapboxSecret: ""), | ||||
|             serverVersion: | ||||
|                 ServerVersion(major: 0, patch: 0, minor: 0, build: 0), | ||||
|             serverVersion: ServerVersionReponseDto( | ||||
|               major: 0, | ||||
|               patch_: 0, | ||||
|               minor: 0, | ||||
|               build: 0, | ||||
|             ), | ||||
|             isVersionMismatch: false, | ||||
|             versionMismatchErrorMessage: "", | ||||
|           ), | ||||
| @@ -21,7 +23,8 @@ class ServerInfoNotifier extends StateNotifier<ServerInfoState> { | ||||
|   final ServerInfoService _serverInfoService; | ||||
|  | ||||
|   getServerVersion() async { | ||||
|     ServerVersion? serverVersion = await _serverInfoService.getServerVersion(); | ||||
|     ServerVersionReponseDto? serverVersion = | ||||
|         await _serverInfoService.getServerVersion(); | ||||
|  | ||||
|     if (serverVersion == null) { | ||||
|       state = state.copyWith( | ||||
| @@ -59,7 +62,9 @@ class ServerInfoNotifier extends StateNotifier<ServerInfoState> { | ||||
|     } | ||||
|  | ||||
|     state = state.copyWith( | ||||
|         isVersionMismatch: false, versionMismatchErrorMessage: ""); | ||||
|       isVersionMismatch: false, | ||||
|       versionMismatchErrorMessage: "", | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   Map<String, int> _getDetailVersion(String version) { | ||||
|   | ||||
| @@ -5,8 +5,8 @@ import 'package:hive/hive.dart'; | ||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| import 'package:immich_mobile/constants/hive_box.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:openapi/api.dart'; | ||||
| import 'package:socket_io_client/socket_io_client.dart'; | ||||
|  | ||||
| class WebscoketState { | ||||
| @@ -92,8 +92,11 @@ class WebsocketNotifier extends StateNotifier<WebscoketState> { | ||||
|  | ||||
|         socket.on('on_upload_success', (data) { | ||||
|           var jsonString = jsonDecode(data.toString()); | ||||
|           ImmichAsset newAsset = ImmichAsset.fromMap(jsonString); | ||||
|           AssetResponseDto? newAsset = AssetResponseDto.fromJson(jsonString); | ||||
|  | ||||
|           if (newAsset != null) { | ||||
|             ref.watch(assetProvider.notifier).onNewAssetUploaded(newAsset); | ||||
|           } | ||||
|         }); | ||||
|       } catch (e) { | ||||
|         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"); | ||||
|     state.socket?.on('on_upload_success', (data) { | ||||
|       var jsonString = jsonDecode(data.toString()); | ||||
|       ImmichAsset newAsset = ImmichAsset.fromMap(jsonString); | ||||
|       AssetResponseDto? newAsset = AssetResponseDto.fromJson(jsonString); | ||||
|  | ||||
|       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 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| import 'package:openapi/api.dart'; | ||||
|  | ||||
| final deviceInfoServiceProvider = Provider((_) => DeviceInfoService()); | ||||
|  | ||||
| @@ -9,12 +10,12 @@ class DeviceInfoService { | ||||
|   Future<Map<String, dynamic>> getDeviceInfo() async { | ||||
|     // Get device info | ||||
|     var deviceId = await FlutterUdid.consistentUdid; | ||||
|     var deviceType = ""; | ||||
|     var deviceType = DeviceTypeEnum.ANDROID; | ||||
|  | ||||
|     if (Platform.isAndroid) { | ||||
|       deviceType = "ANDROID"; | ||||
|       deviceType = DeviceTypeEnum.ANDROID; | ||||
|     } else if (Platform.isIOS) { | ||||
|       deviceType = "IOS"; | ||||
|       deviceType = DeviceTypeEnum.IOS; | ||||
|     } | ||||
|  | ||||
|     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( | ||||
|       {required String url, | ||||
|   Future<dynamic> getRequest({ | ||||
|     required String url, | ||||
|     bool isByteResponse = false, | ||||
|       bool isStreamReponse = false}) async { | ||||
|     bool isStreamReponse = false, | ||||
|   }) async { | ||||
|     try { | ||||
|       var savedEndpoint = Hive.box(userInfoBox).get(serverEndpointKey); | ||||
|  | ||||
|   | ||||
| @@ -1,33 +1,33 @@ | ||||
| import 'package:dio/dio.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| import 'package:immich_mobile/shared/models/server_info.model.dart'; | ||||
| import 'package:immich_mobile/shared/models/server_version.model.dart'; | ||||
| import 'package:immich_mobile/shared/services/network.service.dart'; | ||||
| import 'package:immich_mobile/shared/services/api.service.dart'; | ||||
| import 'package:openapi/api.dart'; | ||||
|  | ||||
| final serverInfoServiceProvider = | ||||
|     Provider((ref) => ServerInfoService(ref.watch(networkServiceProvider))); | ||||
| final serverInfoServiceProvider = Provider( | ||||
|   (ref) => ServerInfoService( | ||||
|     ref.watch(apiServiceProvider), | ||||
|   ), | ||||
| ); | ||||
|  | ||||
| class ServerInfoService { | ||||
|   final NetworkService _networkService; | ||||
|   ServerInfoService(this._networkService); | ||||
|   final ApiService _apiService; | ||||
|   ServerInfoService(this._apiService); | ||||
|  | ||||
|   Future<ServerInfo> getServerInfo() async { | ||||
|     Response response = await _networkService.getRequest(url: 'server-info'); | ||||
|  | ||||
|     return ServerInfo.fromJson(response.toString()); | ||||
|   } | ||||
|  | ||||
|   Future<ServerVersion?> getServerVersion() async { | ||||
|   Future<ServerInfoResponseDto?> getServerInfo() async { | ||||
|     try { | ||||
|       Response response = | ||||
|           await _networkService.getRequest(url: 'server-info/version'); | ||||
|  | ||||
|       return ServerVersion.fromJson(response.toString()); | ||||
|       return await _apiService.serverInfoApi.getServerInfo(); | ||||
|     } catch (e) { | ||||
|       debugPrint("Error getting server info"); | ||||
|     } | ||||
|  | ||||
|       debugPrint("Error [getServerInfo] ${e.toString()}"); | ||||
|       return null; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   Future<ServerVersionReponseDto?> getServerVersion() async { | ||||
|     try { | ||||
|       return await _apiService.serverInfoApi.getServerVersion(); | ||||
|     } catch (e) { | ||||
|       debugPrint("Error getting server info"); | ||||
|       return null; | ||||
|     } | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -1,70 +1,49 @@ | ||||
| import 'dart:convert'; | ||||
|  | ||||
| import 'package:dio/dio.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:hive/hive.dart'; | ||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| import 'package:http/http.dart'; | ||||
| import 'package:http_parser/http_parser.dart'; | ||||
| import 'package:image_picker/image_picker.dart'; | ||||
| import 'package:immich_mobile/constants/hive_box.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/shared/services/api.service.dart'; | ||||
| import 'package:immich_mobile/utils/files_helper.dart'; | ||||
| import 'package:openapi/api.dart'; | ||||
|  | ||||
| final userServiceProvider = | ||||
|     Provider((ref) => UserService(ref.watch(networkServiceProvider))); | ||||
| final userServiceProvider = Provider( | ||||
|   (ref) => UserService( | ||||
|     ref.watch(apiServiceProvider), | ||||
|   ), | ||||
| ); | ||||
|  | ||||
| class UserService { | ||||
|   final NetworkService _networkService; | ||||
|   UserService(this._networkService); | ||||
|   final ApiService _apiService; | ||||
|  | ||||
|   Future<List<User>> getAllUsersInfo() async { | ||||
|   UserService(this._apiService); | ||||
|  | ||||
|   Future<List<UserResponseDto>?> getAllUsersInfo({required bool isAll}) async { | ||||
|     try { | ||||
|       var res = await _networkService.getRequest(url: 'user'); | ||||
|       List<dynamic> decodedData = jsonDecode(res.toString()); | ||||
|       List<User> result = List.from(decodedData.map((e) => User.fromMap(e))); | ||||
|  | ||||
|       return result; | ||||
|       return await _apiService.userApi.getAllUsers(isAll); | ||||
|     } catch (e) { | ||||
|       debugPrint("Error getAllUsersInfo  ${e.toString()}"); | ||||
|       debugPrint("Error [getAllUsersInfo]  ${e.toString()}"); | ||||
|       return null; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|     return []; | ||||
|   } | ||||
|  | ||||
|   Future<UploadProfileImageResponse?> uploadProfileImage(XFile image) async { | ||||
|     var dio = Dio(); | ||||
|     dio.interceptors.add(AuthenticatedRequestInterceptor()); | ||||
|     String savedEndpoint = Hive.box(userInfoBox).get(serverEndpointKey); | ||||
|   Future<CreateProfileImageResponseDto?> uploadProfileImage(XFile image) async { | ||||
|     try { | ||||
|       var mimeType = FileHelper.getMimeType(image.path); | ||||
|  | ||||
|     final imageData = MultipartFile.fromBytes( | ||||
|       return await _apiService.userApi.createProfileImage( | ||||
|         MultipartFile.fromBytes( | ||||
|           'file', | ||||
|           await image.readAsBytes(), | ||||
|           filename: image.name, | ||||
|           contentType: MediaType( | ||||
|             mimeType["type"], | ||||
|             mimeType["subType"], | ||||
|           ), | ||||
|         ), | ||||
|       ); | ||||
|  | ||||
|     final formData = FormData.fromMap({'file': imageData}); | ||||
|  | ||||
|     try { | ||||
|       Response res = await dio.post( | ||||
|         '$savedEndpoint/user/profile-image', | ||||
|         data: formData, | ||||
|       ); | ||||
|  | ||||
|       var payload = UploadProfileImageResponse.fromJson(res.toString()); | ||||
|  | ||||
|       return payload; | ||||
|     } on DioError catch (e) { | ||||
|       debugPrint("Error uploading file: ${e.response}"); | ||||
|       return null; | ||||
|     } catch (e) { | ||||
|       debugPrint("Error uploading file: $e"); | ||||
|       debugPrint("Error [uploadProfileImage] ${e.toString()}"); | ||||
|       return null; | ||||
|     } | ||||
|   } | ||||
|   | ||||
| @@ -22,7 +22,10 @@ class ImmichSliverPersistentAppBarDelegate | ||||
|  | ||||
|   @override | ||||
|   Widget build( | ||||
|       BuildContext context, double shrinkOffset, bool overlapsContent) { | ||||
|     BuildContext context, | ||||
|     double shrinkOffset, | ||||
|     bool overlapsContent, | ||||
|   ) { | ||||
|     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