mirror of
				https://github.com/KevinMidboe/immich.git
				synced 2025-10-29 17:40:28 +00:00 
			
		
		
		
	Download asset to local and error fixing (#100)
* Update photo_manager pub package * Added download endpoint for assets * Successfully save a photo to the local device's gallery * Save save a video to the local device's gallery * Fixed #97 * Added download loading indicator * Refactor and increase the font size for curated search thumbnail images * Reposition loading animation on the search result page
This commit is contained in:
		
							
								
								
									
										5
									
								
								Makefile
									
									
									
									
									
								
							
							
						
						
									
										5
									
								
								Makefile
									
									
									
									
									
								
							| @@ -8,4 +8,7 @@ dev-scale: | ||||
| 	docker-compose -f ./docker/docker-compose.dev.yml up --build -V  --scale immich_server=3 --remove-orphans  | ||||
|  | ||||
| prod: | ||||
| 	docker-compose -f ./docker/docker-compose.yml up --build -V --remove-orphans | ||||
| 	docker-compose -f ./docker/docker-compose.yml up --build -V --remove-orphans | ||||
|  | ||||
| prod-scale: | ||||
| 	docker-compose -f ./docker/docker-compose.yml up --build -V --scale immich_server=3 --scale immich_microservices=3 --remove-orphans | ||||
| @@ -22,6 +22,7 @@ services: | ||||
|       - database | ||||
|     networks: | ||||
|       - immich_network | ||||
|     restart: unless-stopped | ||||
|  | ||||
|   immich_microservices: | ||||
|     image: immich-microservices:1.4.0 | ||||
| @@ -43,7 +44,7 @@ services: | ||||
|       - database | ||||
|     networks: | ||||
|       - immich_network | ||||
|  | ||||
|     restart: unless-stopped | ||||
|  | ||||
|   redis: | ||||
|     container_name: immich_redis | ||||
|   | ||||
| @@ -39,6 +39,7 @@ export class ImageClassifierService { | ||||
|           } | ||||
|         } | ||||
|  | ||||
|         tf.dispose(decodedImage); | ||||
|         return tags; | ||||
|       } | ||||
|     } catch (e) { | ||||
|   | ||||
| @@ -29,6 +29,7 @@ export class ObjectDetectionService { | ||||
|           } | ||||
|         } | ||||
|  | ||||
|         tf.dispose(decodedImage); | ||||
|         return [...tags]; | ||||
|       } | ||||
|     } catch (e) { | ||||
|   | ||||
| @@ -51,7 +51,7 @@ android { | ||||
|     defaultConfig { | ||||
|         // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). | ||||
|         applicationId "app.alextran.immich" | ||||
|         minSdkVersion 20 | ||||
|         minSdkVersion 21 | ||||
|         targetSdkVersion flutter.targetSdkVersion | ||||
|         versionCode flutterVersionCode.toInteger() | ||||
|         versionName flutterVersionName | ||||
|   | ||||
| @@ -20,4 +20,7 @@ | ||||
|   </application> | ||||
|   <uses-permission android:name="android.permission.INTERNET" /> | ||||
|   <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> | ||||
|   <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> | ||||
|   <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> | ||||
|   <uses-permission android:name="android.permission.ACCESS_MEDIA_LOCATION" /> | ||||
| </manifest> | ||||
| @@ -13,7 +13,7 @@ PODS: | ||||
|     - Flutter | ||||
|   - path_provider_ios (0.0.1): | ||||
|     - Flutter | ||||
|   - photo_manager (1.0.0): | ||||
|   - photo_manager (2.0.0): | ||||
|     - Flutter | ||||
|     - FlutterMacOS | ||||
|   - SAMKeychain (1.5.3) | ||||
| @@ -70,7 +70,7 @@ SPEC CHECKSUMS: | ||||
|   FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a | ||||
|   package_info_plus: 6c92f08e1f853dc01228d6f553146438dafcd14e | ||||
|   path_provider_ios: 7d7ce634493af4477d156294792024ec3485acd5 | ||||
|   photo_manager: 84fa94fbeb82e607333ea9a13c43b58e0903a463 | ||||
|   photo_manager: 4f6810b7dfc4feb03b461ac1a70dacf91fba7604 | ||||
|   SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c | ||||
|   sqflite: 6d358c025f5b867b29ed92fc697fd34924e11904 | ||||
|   Toast: 91b396c56ee72a5790816f40d3a94dd357abc196 | ||||
|   | ||||
| @@ -1,66 +1,72 @@ | ||||
| <?xml version="1.0" encoding="UTF-8"?> | ||||
| <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> | ||||
| <plist version="1.0"> | ||||
| <dict> | ||||
| 	<key>CFBundleDevelopmentRegion</key> | ||||
| 	<string>$(DEVELOPMENT_LANGUAGE)</string> | ||||
| 	<key>CFBundleDisplayName</key> | ||||
| 	<string>Immich</string> | ||||
| 	<key>CFBundleExecutable</key> | ||||
| 	<string>$(EXECUTABLE_NAME)</string> | ||||
| 	<key>CFBundleIdentifier</key> | ||||
| 	<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string> | ||||
| 	<key>CFBundleInfoDictionaryVersion</key> | ||||
| 	<string>6.0</string> | ||||
| 	<key>CFBundleName</key> | ||||
| 	<string>immich_mobile</string> | ||||
| 	<key>CFBundlePackageType</key> | ||||
| 	<string>APPL</string> | ||||
| 	<key>CFBundleShortVersionString</key> | ||||
| 	<string>$(FLUTTER_BUILD_NAME)</string> | ||||
| 	<key>CFBundleSignature</key> | ||||
| 	<string>????</string> | ||||
| 	<key>CFBundleVersion</key> | ||||
| 	<string>2</string> | ||||
| 	<key>LSRequiresIPhoneOS</key> | ||||
| 	<true/> | ||||
| 	<key>MGLMapboxMetricsEnabledSettingShownInApp</key> | ||||
| 	<true/> | ||||
| 	<key>NSAppTransportSecurity</key> | ||||
| 	<dict> | ||||
| 		<key>NSAllowsArbitraryLoads</key> | ||||
| 		<true/> | ||||
| 	</dict> | ||||
| 	<key>NSLocationAlwaysUsageDescription</key> | ||||
| 	<string>Enable location setting to show position of assets on map</string> | ||||
| 	<key>NSLocationWhenInUseUsageDescription</key> | ||||
| 	<string>Enable location setting to show position of assets on map</string> | ||||
| 	<key>NSPhotoLibraryUsageDescription</key> | ||||
| 	<string>We need to manage backup your photos album</string> | ||||
| 	<key>UILaunchStoryboardName</key> | ||||
| 	<string>LaunchScreen</string> | ||||
| 	<key>UIMainStoryboardFile</key> | ||||
| 	<string>Main</string> | ||||
| 	<key>UISupportedInterfaceOrientations</key> | ||||
| 	<array> | ||||
| 		<string>UIInterfaceOrientationPortrait</string> | ||||
| 		<string>UIInterfaceOrientationLandscapeLeft</string> | ||||
| 		<string>UIInterfaceOrientationLandscapeRight</string> | ||||
| 	</array> | ||||
| 	<key>UISupportedInterfaceOrientations~ipad</key> | ||||
| 	<array> | ||||
| 		<string>UIInterfaceOrientationPortrait</string> | ||||
| 		<string>UIInterfaceOrientationPortraitUpsideDown</string> | ||||
| 		<string>UIInterfaceOrientationLandscapeLeft</string> | ||||
| 		<string>UIInterfaceOrientationLandscapeRight</string> | ||||
| 	</array> | ||||
| 	<key>UIUserInterfaceStyle</key> | ||||
| 	<string>Light</string> | ||||
| 	<key>UIViewControllerBasedStatusBarAppearance</key> | ||||
| 	<true/> | ||||
| 	<key>io.flutter.embedded_views_preview</key> | ||||
| 	<true/> | ||||
| 	<key>ITSAppUsesNonExemptEncryption</key> | ||||
| 	<false/> | ||||
| </dict> | ||||
| </plist> | ||||
|   <dict> | ||||
|     <key>CFBundleDevelopmentRegion</key> | ||||
|     <string>$(DEVELOPMENT_LANGUAGE)</string> | ||||
|     <key>CFBundleDisplayName</key> | ||||
|     <string>Immich</string> | ||||
|     <key>CFBundleExecutable</key> | ||||
|     <string>$(EXECUTABLE_NAME)</string> | ||||
|     <key>CFBundleIdentifier</key> | ||||
|     <string>$(PRODUCT_BUNDLE_IDENTIFIER)</string> | ||||
|     <key>CFBundleInfoDictionaryVersion</key> | ||||
|     <string>6.0</string> | ||||
|     <key>CFBundleName</key> | ||||
|     <string>immich_mobile</string> | ||||
|     <key>CFBundlePackageType</key> | ||||
|     <string>APPL</string> | ||||
|     <key>CFBundleShortVersionString</key> | ||||
|     <string>$(FLUTTER_BUILD_NAME)</string> | ||||
|     <key>CFBundleSignature</key> | ||||
|     <string>????</string> | ||||
|     <key>CFBundleVersion</key> | ||||
|     <string>2</string> | ||||
|     <key>LSRequiresIPhoneOS</key> | ||||
|     <true /> | ||||
|     <key>MGLMapboxMetricsEnabledSettingShownInApp</key> | ||||
|     <true /> | ||||
|     <key>NSAppTransportSecurity</key> | ||||
|     <dict> | ||||
|       <key>NSAllowsArbitraryLoads</key> | ||||
|       <true /> | ||||
|     </dict> | ||||
|     <key>NSLocationAlwaysUsageDescription</key> | ||||
|     <string>Enable location setting to show position of assets on map</string> | ||||
|  | ||||
|     <key>NSLocationWhenInUseUsageDescription</key> | ||||
|     <string>Enable location setting to show position of assets on map</string> | ||||
|  | ||||
|     <key>NSPhotoLibraryUsageDescription</key> | ||||
|     <string>We need to manage backup your photos album</string> | ||||
|  | ||||
|     <key>NSPhotoLibraryAddUsageDescription</key> | ||||
|     <string>We need to manage backup your photos album</string> | ||||
|  | ||||
|     <key>UILaunchStoryboardName</key> | ||||
|     <string>LaunchScreen</string> | ||||
|     <key>UIMainStoryboardFile</key> | ||||
|     <string>Main</string> | ||||
|     <key>UISupportedInterfaceOrientations</key> | ||||
|     <array> | ||||
|       <string>UIInterfaceOrientationPortrait</string> | ||||
|       <string>UIInterfaceOrientationLandscapeLeft</string> | ||||
|       <string>UIInterfaceOrientationLandscapeRight</string> | ||||
|     </array> | ||||
|     <key>UISupportedInterfaceOrientations~ipad</key> | ||||
|     <array> | ||||
|       <string>UIInterfaceOrientationPortrait</string> | ||||
|       <string>UIInterfaceOrientationPortraitUpsideDown</string> | ||||
|       <string>UIInterfaceOrientationLandscapeLeft</string> | ||||
|       <string>UIInterfaceOrientationLandscapeRight</string> | ||||
|     </array> | ||||
|     <key>UIUserInterfaceStyle</key> | ||||
|     <string>Light</string> | ||||
|     <key>UIViewControllerBasedStatusBarAppearance</key> | ||||
|     <true /> | ||||
|     <key>io.flutter.embedded_views_preview</key> | ||||
|     <true /> | ||||
|     <key>ITSAppUsesNonExemptEncryption</key> | ||||
|     <false /> | ||||
|   </dict> | ||||
| </plist> | ||||
| @@ -97,6 +97,7 @@ class _ImmichAppState extends ConsumerState<ImmichApp> with WidgetsBindingObserv | ||||
|         textTheme: GoogleFonts.workSansTextTheme( | ||||
|           Theme.of(context).textTheme.apply(fontSizeFactor: 1.0), | ||||
|         ), | ||||
|         snackBarTheme: SnackBarThemeData(contentTextStyle: TextStyle(fontFamily: GoogleFonts.workSans().fontFamily)), | ||||
|         scaffoldBackgroundColor: const Color(0xFFf6f8fe), | ||||
|         appBarTheme: const AppBarTheme( | ||||
|           backgroundColor: Colors.white, | ||||
|   | ||||
| @@ -1,28 +1,34 @@ | ||||
| import 'dart:convert'; | ||||
|  | ||||
| enum DownloadAssetStatus { idle, loading, success, error } | ||||
|  | ||||
| class ImageViewerPageState { | ||||
|   final bool isBottomSheetEnable; | ||||
|   // enum | ||||
|   final DownloadAssetStatus downloadAssetStatus; | ||||
|  | ||||
|   ImageViewerPageState({ | ||||
|     required this.isBottomSheetEnable, | ||||
|     required this.downloadAssetStatus, | ||||
|   }); | ||||
|  | ||||
|   ImageViewerPageState copyWith({ | ||||
|     bool? isBottomSheetEnable, | ||||
|     DownloadAssetStatus? downloadAssetStatus, | ||||
|   }) { | ||||
|     return ImageViewerPageState( | ||||
|       isBottomSheetEnable: isBottomSheetEnable ?? this.isBottomSheetEnable, | ||||
|       downloadAssetStatus: downloadAssetStatus ?? this.downloadAssetStatus, | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   Map<String, dynamic> toMap() { | ||||
|     return { | ||||
|       'isBottomSheetEnable': isBottomSheetEnable, | ||||
|     }; | ||||
|     final result = <String, dynamic>{}; | ||||
|  | ||||
|     result.addAll({'downloadAssetStatus': downloadAssetStatus.index}); | ||||
|  | ||||
|     return result; | ||||
|   } | ||||
|  | ||||
|   factory ImageViewerPageState.fromMap(Map<String, dynamic> map) { | ||||
|     return ImageViewerPageState( | ||||
|       isBottomSheetEnable: map['isBottomSheetEnable'] ?? false, | ||||
|       downloadAssetStatus: DownloadAssetStatus.values[map['downloadAssetStatus'] ?? 0], | ||||
|     ); | ||||
|   } | ||||
|  | ||||
| @@ -31,15 +37,15 @@ class ImageViewerPageState { | ||||
|   factory ImageViewerPageState.fromJson(String source) => ImageViewerPageState.fromMap(json.decode(source)); | ||||
|  | ||||
|   @override | ||||
|   String toString() => 'ImageViewerPageState(isBottomSheetEnable: $isBottomSheetEnable)'; | ||||
|   String toString() => 'ImageViewerPageState(downloadAssetStatus: $downloadAssetStatus)'; | ||||
|  | ||||
|   @override | ||||
|   bool operator ==(Object other) { | ||||
|     if (identical(this, other)) return true; | ||||
|  | ||||
|     return other is ImageViewerPageState && other.isBottomSheetEnable == isBottomSheetEnable; | ||||
|     return other is ImageViewerPageState && other.downloadAssetStatus == downloadAssetStatus; | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   int get hashCode => isBottomSheetEnable.hashCode; | ||||
|   int get hashCode => downloadAssetStatus.hashCode; | ||||
| } | ||||
|   | ||||
| @@ -0,0 +1,6 @@ | ||||
| class RequestDownloadAssetInfo { | ||||
|   final String assetId; | ||||
|   final String deviceId; | ||||
|  | ||||
|   RequestDownloadAssetInfo(this.assetId, this.deviceId); | ||||
| } | ||||
| @@ -1,21 +1,43 @@ | ||||
| import 'package:flutter/material.dart'; | ||||
| 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/home/models/home_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'; | ||||
|  | ||||
| class ImageViewerPageStateNotifier extends StateNotifier<ImageViewerPageState> { | ||||
|   ImageViewerPageStateNotifier() : super(ImageViewerPageState(isBottomSheetEnable: false)); | ||||
| class ImageViewerStateNotifier extends StateNotifier<ImageViewerPageState> { | ||||
|   final ImageViewerService _imageViewerService = ImageViewerService(); | ||||
|  | ||||
|   void toggleBottomSheet() { | ||||
|     bool isBottomSheetEnable = state.isBottomSheetEnable; | ||||
|   ImageViewerStateNotifier() : super(ImageViewerPageState(downloadAssetStatus: DownloadAssetStatus.idle)); | ||||
|  | ||||
|     if (isBottomSheetEnable) { | ||||
|       state.copyWith(isBottomSheetEnable: false); | ||||
|   void downloadAsset(ImmichAsset asset, BuildContext context) async { | ||||
|     state = state.copyWith(downloadAssetStatus: DownloadAssetStatus.loading); | ||||
|  | ||||
|     bool isSuccess = await _imageViewerService.downloadAssetToDevice(asset); | ||||
|  | ||||
|     if (isSuccess) { | ||||
|       state = state.copyWith(downloadAssetStatus: DownloadAssetStatus.success); | ||||
|  | ||||
|       ImmichToast.show( | ||||
|         context: context, | ||||
|         msg: "Download Success", | ||||
|         toastType: ToastType.success, | ||||
|         gravity: ToastGravity.BOTTOM, | ||||
|       ); | ||||
|     } else { | ||||
|       state.copyWith(isBottomSheetEnable: true); | ||||
|       state = state.copyWith(downloadAssetStatus: DownloadAssetStatus.error); | ||||
|       ImmichToast.show( | ||||
|         context: context, | ||||
|         msg: "Download Error", | ||||
|         toastType: ToastType.error, | ||||
|         gravity: ToastGravity.BOTTOM, | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     state = state.copyWith(downloadAssetStatus: DownloadAssetStatus.idle); | ||||
|   } | ||||
| } | ||||
|  | ||||
| final homePageStateProvider = StateNotifierProvider<ImageViewerPageStateNotifier, ImageViewerPageState>( | ||||
|     ((ref) => ImageViewerPageStateNotifier())); | ||||
| final imageViewerStateProvider = | ||||
|     StateNotifierProvider<ImageViewerStateNotifier, ImageViewerPageState>(((ref) => ImageViewerStateNotifier())); | ||||
|   | ||||
| @@ -0,0 +1,50 @@ | ||||
| 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: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'; | ||||
|  | ||||
| class ImageViewerService { | ||||
|   Future<bool> downloadAssetToDevice(ImmichAsset 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)}"}, | ||||
|       ); | ||||
|  | ||||
|       final AssetEntity? entity; | ||||
|  | ||||
|       if (asset.type == 'IMAGE') { | ||||
|         entity = await PhotoManager.editor.saveImage( | ||||
|           res.bodyBytes, | ||||
|           title: p.basename(asset.originalPath), | ||||
|         ); | ||||
|       } else { | ||||
|         final tempDir = await getTemporaryDirectory(); | ||||
|         File tempFile = await File('${tempDir.path}/$fileName').create(); | ||||
|         tempFile.writeAsBytesSync(res.bodyBytes); | ||||
|         entity = await PhotoManager.editor.saveVideo(tempFile, title: fileName); | ||||
|       } | ||||
|  | ||||
|       if (entity != null) { | ||||
|         return true; | ||||
|       } | ||||
|     } catch (e) { | ||||
|       debugPrint("Error saving file $e"); | ||||
|       return false; | ||||
|     } | ||||
|  | ||||
|     return false; | ||||
|   } | ||||
| } | ||||
| @@ -0,0 +1,24 @@ | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:flutter_spinkit/flutter_spinkit.dart'; | ||||
|  | ||||
| class DownloadLoadingIndicator extends StatelessWidget { | ||||
|   const DownloadLoadingIndicator({ | ||||
|     Key? key, | ||||
|   }) : super(key: key); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return Container( | ||||
|       height: 60, | ||||
|       width: 60, | ||||
|       decoration: BoxDecoration( | ||||
|         color: Theme.of(context).primaryColor, | ||||
|         borderRadius: BorderRadius.circular(10), | ||||
|       ), | ||||
|       child: const SpinKitDancingSquare( | ||||
|         color: Colors.white, | ||||
|         size: 30.0, | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
| @@ -1,14 +1,19 @@ | ||||
| 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'; | ||||
|  | ||||
| class TopControlAppBar extends StatelessWidget with PreferredSizeWidget { | ||||
|   const TopControlAppBar({Key? key, required this.asset, required this.onMoreInfoPressed}) : super(key: key); | ||||
| class TopControlAppBar extends ConsumerWidget with PreferredSizeWidget { | ||||
|   const TopControlAppBar( | ||||
|       {Key? key, required this.asset, required this.onMoreInfoPressed, required this.onDownloadPressed}) | ||||
|       : super(key: key); | ||||
|  | ||||
|   final ImmichAsset asset; | ||||
|   final Function onMoreInfoPressed; | ||||
|   final Function onDownloadPressed; | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|   Widget build(BuildContext context, WidgetRef ref) { | ||||
|     double iconSize = 18.0; | ||||
|  | ||||
|     return AppBar( | ||||
| @@ -29,7 +34,7 @@ class TopControlAppBar extends StatelessWidget with PreferredSizeWidget { | ||||
|           iconSize: iconSize, | ||||
|           splashRadius: iconSize, | ||||
|           onPressed: () { | ||||
|             print("download"); | ||||
|             onDownloadPressed(); | ||||
|           }, | ||||
|           icon: const Icon(Icons.cloud_download_rounded), | ||||
|         ), | ||||
|   | ||||
| @@ -4,6 +4,9 @@ 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/asset_viewer/models/image_viewer_page_state.model.dart'; | ||||
| import 'package:immich_mobile/modules/asset_viewer/providers/image_viewer_page_state.provider.dart'; | ||||
| import 'package:immich_mobile/modules/asset_viewer/ui/download_loading_indicator.dart'; | ||||
| import 'package:immich_mobile/modules/asset_viewer/ui/exif_bottom_sheet.dart'; | ||||
| import 'package:immich_mobile/modules/asset_viewer/ui/top_control_app_bar.dart'; | ||||
| import 'package:immich_mobile/modules/home/services/asset.service.dart'; | ||||
| @@ -25,6 +28,7 @@ class ImageViewerPage extends HookConsumerWidget { | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context, WidgetRef ref) { | ||||
|     final downloadAssetStatus = ref.watch(imageViewerStateProvider).downloadAssetStatus; | ||||
|     var box = Hive.box(userInfoBox); | ||||
|  | ||||
|     getAssetExif() async { | ||||
| @@ -42,65 +46,77 @@ class ImageViewerPage extends HookConsumerWidget { | ||||
|         asset: asset, | ||||
|         onMoreInfoPressed: () { | ||||
|           showModalBottomSheet( | ||||
|               backgroundColor: Colors.black, | ||||
|               barrierColor: Colors.transparent, | ||||
|               isScrollControlled: false, | ||||
|               context: context, | ||||
|               builder: (context) { | ||||
|                 return ExifBottomSheet(assetDetail: assetDetail!); | ||||
|               }); | ||||
|             backgroundColor: Colors.black, | ||||
|             barrierColor: Colors.transparent, | ||||
|             isScrollControlled: false, | ||||
|             context: context, | ||||
|             builder: (context) { | ||||
|               return ExifBottomSheet(assetDetail: assetDetail!); | ||||
|             }, | ||||
|           ); | ||||
|         }, | ||||
|         onDownloadPressed: () { | ||||
|           ref.watch(imageViewerStateProvider.notifier).downloadAsset(asset, context); | ||||
|         }, | ||||
|       ), | ||||
|       body: SafeArea( | ||||
|         child: Center( | ||||
|           child: Hero( | ||||
|             tag: heroTag, | ||||
|             child: CachedNetworkImage( | ||||
|               fit: BoxFit.cover, | ||||
|               imageUrl: imageUrl, | ||||
|               httpHeaders: {"Authorization": "Bearer ${box.get(accessTokenKey)}"}, | ||||
|               fadeInDuration: const Duration(milliseconds: 250), | ||||
|               errorWidget: (context, url, error) => ConstrainedBox( | ||||
|                 constraints: const BoxConstraints(maxWidth: 300), | ||||
|                 child: Wrap( | ||||
|                   spacing: 32, | ||||
|                   runSpacing: 32, | ||||
|                   alignment: WrapAlignment.center, | ||||
|                   children: [ | ||||
|                     const Text( | ||||
|                       "Failed To Render Image - Possibly Corrupted Data", | ||||
|                       textAlign: TextAlign.center, | ||||
|                       style: TextStyle(fontSize: 16, color: Colors.white), | ||||
|         child: Stack( | ||||
|           children: [ | ||||
|             Center( | ||||
|               child: Hero( | ||||
|                 tag: heroTag, | ||||
|                 child: CachedNetworkImage( | ||||
|                   fit: BoxFit.cover, | ||||
|                   imageUrl: imageUrl, | ||||
|                   httpHeaders: {"Authorization": "Bearer ${box.get(accessTokenKey)}"}, | ||||
|                   fadeInDuration: const Duration(milliseconds: 250), | ||||
|                   errorWidget: (context, url, error) => ConstrainedBox( | ||||
|                     constraints: const BoxConstraints(maxWidth: 300), | ||||
|                     child: Wrap( | ||||
|                       spacing: 32, | ||||
|                       runSpacing: 32, | ||||
|                       alignment: WrapAlignment.center, | ||||
|                       children: [ | ||||
|                         const Text( | ||||
|                           "Failed To Render Image - Possibly Corrupted Data", | ||||
|                           textAlign: TextAlign.center, | ||||
|                           style: TextStyle(fontSize: 16, color: Colors.white), | ||||
|                         ), | ||||
|                         SingleChildScrollView( | ||||
|                           child: Text( | ||||
|                             error.toString(), | ||||
|                             textAlign: TextAlign.center, | ||||
|                             style: TextStyle(fontSize: 12, color: Colors.grey[400]), | ||||
|                           ), | ||||
|                         ), | ||||
|                       ], | ||||
|                     ), | ||||
|                     SingleChildScrollView( | ||||
|                       child: Text( | ||||
|                         error.toString(), | ||||
|                         textAlign: TextAlign.center, | ||||
|                         style: TextStyle(fontSize: 12, color: Colors.grey[400]), | ||||
|                   ), | ||||
|                   placeholder: (context, url) { | ||||
|                     return CachedNetworkImage( | ||||
|                       cacheKey: thumbnailUrl, | ||||
|                       fit: BoxFit.cover, | ||||
|                       imageUrl: thumbnailUrl, | ||||
|                       httpHeaders: {"Authorization": "Bearer ${box.get(accessTokenKey)}"}, | ||||
|                       placeholderFadeInDuration: const Duration(milliseconds: 0), | ||||
|                       progressIndicatorBuilder: (context, url, downloadProgress) => Transform.scale( | ||||
|                         scale: 0.2, | ||||
|                         child: CircularProgressIndicator(value: downloadProgress.progress), | ||||
|                       ), | ||||
|                     ), | ||||
|                   ], | ||||
|                       errorWidget: (context, url, error) => Icon( | ||||
|                         Icons.error, | ||||
|                         color: Colors.grey[300], | ||||
|                       ), | ||||
|                     ); | ||||
|                   }, | ||||
|                 ), | ||||
|               ), | ||||
|               placeholder: (context, url) { | ||||
|                 return CachedNetworkImage( | ||||
|                   cacheKey: thumbnailUrl, | ||||
|                   fit: BoxFit.cover, | ||||
|                   imageUrl: thumbnailUrl, | ||||
|                   httpHeaders: {"Authorization": "Bearer ${box.get(accessTokenKey)}"}, | ||||
|                   placeholderFadeInDuration: const Duration(milliseconds: 0), | ||||
|                   progressIndicatorBuilder: (context, url, downloadProgress) => Transform.scale( | ||||
|                     scale: 0.2, | ||||
|                     child: CircularProgressIndicator(value: downloadProgress.progress), | ||||
|                   ), | ||||
|                   errorWidget: (context, url, error) => Icon( | ||||
|                     Icons.error, | ||||
|                     color: Colors.grey[300], | ||||
|                   ), | ||||
|                 ); | ||||
|               }, | ||||
|             ), | ||||
|           ), | ||||
|             if (downloadAssetStatus == DownloadAssetStatus.loading) | ||||
|               const Center( | ||||
|                 child: DownloadLoadingIndicator(), | ||||
|               ), | ||||
|           ], | ||||
|         ), | ||||
|       ), | ||||
|     ); | ||||
|   | ||||
| @@ -1,35 +1,74 @@ | ||||
| import 'package:auto_route/auto_route.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:flutter/services.dart'; | ||||
| 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:chewie/chewie.dart'; | ||||
| import 'package:immich_mobile/modules/asset_viewer/models/image_viewer_page_state.model.dart'; | ||||
| import 'package:immich_mobile/modules/asset_viewer/providers/image_viewer_page_state.provider.dart'; | ||||
| import 'package:immich_mobile/modules/asset_viewer/ui/download_loading_indicator.dart'; | ||||
| import 'package:immich_mobile/modules/asset_viewer/ui/exif_bottom_sheet.dart'; | ||||
| import 'package:immich_mobile/modules/asset_viewer/ui/top_control_app_bar.dart'; | ||||
| import 'package:immich_mobile/modules/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:video_player/video_player.dart'; | ||||
| 
 | ||||
| class VideoViewerPage extends StatelessWidget { | ||||
| // ignore: must_be_immutable | ||||
| class VideoViewerPage extends HookConsumerWidget { | ||||
|   final String videoUrl; | ||||
|   final ImmichAsset asset; | ||||
|   ImmichAssetWithExif? assetDetail; | ||||
|   final AssetService _assetService = AssetService(); | ||||
| 
 | ||||
|   const VideoViewerPage({Key? key, required this.videoUrl}) : super(key: key); | ||||
|   VideoViewerPage({Key? key, required this.videoUrl, required this.asset}) : super(key: key); | ||||
| 
 | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|   Widget build(BuildContext context, WidgetRef ref) { | ||||
|     final downloadAssetStatus = ref.watch(imageViewerStateProvider).downloadAssetStatus; | ||||
| 
 | ||||
|     String jwtToken = Hive.box(userInfoBox).get(accessTokenKey); | ||||
| 
 | ||||
|     getAssetExif() async { | ||||
|       assetDetail = await _assetService.getAssetById(asset.id); | ||||
|     } | ||||
| 
 | ||||
|     useEffect(() { | ||||
|       getAssetExif(); | ||||
|       return null; | ||||
|     }, []); | ||||
| 
 | ||||
|     return Scaffold( | ||||
|       backgroundColor: Colors.black, | ||||
|       appBar: AppBar( | ||||
|         systemOverlayStyle: SystemUiOverlayStyle.light, | ||||
|         backgroundColor: Colors.black, | ||||
|         leading: IconButton( | ||||
|             onPressed: () { | ||||
|               AutoRouter.of(context).pop(); | ||||
|       appBar: TopControlAppBar( | ||||
|         asset: asset, | ||||
|         onMoreInfoPressed: () { | ||||
|           showModalBottomSheet( | ||||
|             backgroundColor: Colors.black, | ||||
|             barrierColor: Colors.transparent, | ||||
|             isScrollControlled: false, | ||||
|             context: context, | ||||
|             builder: (context) { | ||||
|               return ExifBottomSheet(assetDetail: assetDetail!); | ||||
|             }, | ||||
|             icon: const Icon(Icons.arrow_back_ios)), | ||||
|           ); | ||||
|         }, | ||||
|         onDownloadPressed: () { | ||||
|           ref.watch(imageViewerStateProvider.notifier).downloadAsset(asset, context); | ||||
|         }, | ||||
|       ), | ||||
|       body: SafeArea( | ||||
|         child: VideoThumbnailPlayer( | ||||
|           url: videoUrl, | ||||
|           jwtToken: jwtToken, | ||||
|         child: Stack( | ||||
|           children: [ | ||||
|             VideoThumbnailPlayer( | ||||
|               url: videoUrl, | ||||
|               jwtToken: jwtToken, | ||||
|             ), | ||||
|             if (downloadAssetStatus == DownloadAssetStatus.loading) | ||||
|               const Center( | ||||
|                 child: DownloadLoadingIndicator(), | ||||
|               ), | ||||
|           ], | ||||
|         ), | ||||
|       ), | ||||
|     ); | ||||
| @@ -65,8 +65,8 @@ class ThumbnailImage extends HookConsumerWidget { | ||||
|           } else { | ||||
|             AutoRouter.of(context).push( | ||||
|               VideoViewerRoute( | ||||
|                 videoUrl: '${box.get(serverEndpointKey)}/asset/file?aid=${asset.deviceAssetId}&did=${asset.deviceId}', | ||||
|               ), | ||||
|                   videoUrl: '${box.get(serverEndpointKey)}/asset/file?aid=${asset.deviceAssetId}&did=${asset.deviceId}', | ||||
|                   asset: asset), | ||||
|             ); | ||||
|           } | ||||
|         } | ||||
|   | ||||
| @@ -128,9 +128,10 @@ class LoginButton extends ConsumerWidget { | ||||
|             AutoRouter.of(context).pushNamed("/tab-controller-page"); | ||||
|           } else { | ||||
|             ImmichToast.show( | ||||
|                 context: context, | ||||
|                 msg: "Error logging you in, check server url, email and password!", | ||||
|                 toastType: ToastType.error); | ||||
|               context: context, | ||||
|               msg: "Error logging you in, check server url, email and password!", | ||||
|               toastType: ToastType.error, | ||||
|             ); | ||||
|           } | ||||
|         }, | ||||
|         child: const Text("Login")); | ||||
|   | ||||
							
								
								
									
										67
									
								
								mobile/lib/modules/search/ui/thumbnail_with_info.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										67
									
								
								mobile/lib/modules/search/ui/thumbnail_with_info.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,67 @@ | ||||
| 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, required this.textInfo, required this.imageUrl, required this.onTap}) | ||||
|       : super(key: key); | ||||
|  | ||||
|   final String textInfo; | ||||
|   final String imageUrl; | ||||
|   final Function onTap; | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     var box = Hive.box(userInfoBox); | ||||
|  | ||||
|     return GestureDetector( | ||||
|       onTap: () { | ||||
|         onTap(); | ||||
|       }, | ||||
|       child: Padding( | ||||
|         padding: const EdgeInsets.only(right: 8.0), | ||||
|         child: SizedBox( | ||||
|           width: MediaQuery.of(context).size.width / 2, | ||||
|           child: Stack( | ||||
|             alignment: Alignment.bottomCenter, | ||||
|             children: [ | ||||
|               Container( | ||||
|                 foregroundDecoration: BoxDecoration( | ||||
|                   borderRadius: BorderRadius.circular(10), | ||||
|                   color: Colors.black26, | ||||
|                 ), | ||||
|                 child: ClipRRect( | ||||
|                   borderRadius: BorderRadius.circular(10), | ||||
|                   child: CachedNetworkImage( | ||||
|                     width: 250, | ||||
|                     height: 250, | ||||
|                     fit: BoxFit.cover, | ||||
|                     imageUrl: imageUrl, | ||||
|                     httpHeaders: {"Authorization": "Bearer ${box.get(accessTokenKey)}"}, | ||||
|                   ), | ||||
|                 ), | ||||
|               ), | ||||
|               Positioned( | ||||
|                 bottom: 8, | ||||
|                 left: 10, | ||||
|                 child: SizedBox( | ||||
|                   width: MediaQuery.of(context).size.width / 3, | ||||
|                   child: Text( | ||||
|                     textInfo.capitalizeFirstLetter(), | ||||
|                     style: const TextStyle( | ||||
|                       color: Colors.white, | ||||
|                       fontWeight: FontWeight.bold, | ||||
|                       fontSize: 16, | ||||
|                     ), | ||||
|                   ), | ||||
|                 ), | ||||
|               ), | ||||
|             ], | ||||
|           ), | ||||
|         ), | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
| @@ -1,5 +1,4 @@ | ||||
| import 'package:auto_route/auto_route.dart'; | ||||
| import 'package:cached_network_image/cached_network_image.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:flutter_hooks/flutter_hooks.dart'; | ||||
| import 'package:hive_flutter/hive_flutter.dart'; | ||||
| @@ -10,6 +9,7 @@ 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'; | ||||
| import 'package:immich_mobile/modules/search/ui/thumbnail_with_info.dart'; | ||||
| import 'package:immich_mobile/routing/router.dart'; | ||||
| import 'package:immich_mobile/utils/capitalize_first_letter.dart'; | ||||
|  | ||||
| @@ -40,12 +40,12 @@ class SearchPage extends HookConsumerWidget { | ||||
|  | ||||
|     _buildPlaces() { | ||||
|       return curatedLocation.when( | ||||
|         loading: () => const CircularProgressIndicator(), | ||||
|         loading: () => const SizedBox(width: 60, height: 60, child: CircularProgressIndicator.adaptive()), | ||||
|         error: (err, stack) => Text('Error: $err'), | ||||
|         data: (curatedLocations) { | ||||
|           return curatedLocations.isNotEmpty | ||||
|               ? SizedBox( | ||||
|                   height: MediaQuery.of(context).size.width / 3, | ||||
|                   height: MediaQuery.of(context).size.width / 2, | ||||
|                   child: ListView.builder( | ||||
|                     padding: const EdgeInsets.only(left: 16), | ||||
|                     scrollDirection: Axis.horizontal, | ||||
| @@ -66,7 +66,7 @@ class SearchPage extends HookConsumerWidget { | ||||
|                   ), | ||||
|                 ) | ||||
|               : SizedBox( | ||||
|                   height: MediaQuery.of(context).size.width / 3, | ||||
|                   height: MediaQuery.of(context).size.width / 2, | ||||
|                   child: ListView.builder( | ||||
|                     padding: const EdgeInsets.only(left: 16), | ||||
|                     scrollDirection: Axis.horizontal, | ||||
| @@ -87,12 +87,12 @@ class SearchPage extends HookConsumerWidget { | ||||
|  | ||||
|     _buildThings() { | ||||
|       return curatedObjects.when( | ||||
|         loading: () => const CircularProgressIndicator(), | ||||
|         loading: () => const SizedBox(width: 60, height: 60, child: CircularProgressIndicator.adaptive()), | ||||
|         error: (err, stack) => Text('Error: $err'), | ||||
|         data: (objects) { | ||||
|           return objects.isNotEmpty | ||||
|               ? SizedBox( | ||||
|                   height: MediaQuery.of(context).size.width / 3, | ||||
|                   height: MediaQuery.of(context).size.width / 2, | ||||
|                   child: ListView.builder( | ||||
|                     padding: const EdgeInsets.only(left: 16), | ||||
|                     scrollDirection: Axis.horizontal, | ||||
| @@ -114,7 +114,7 @@ class SearchPage extends HookConsumerWidget { | ||||
|                   ), | ||||
|                 ) | ||||
|               : SizedBox( | ||||
|                   height: MediaQuery.of(context).size.width / 3, | ||||
|                   height: MediaQuery.of(context).size.width / 2, | ||||
|                   child: ListView.builder( | ||||
|                     padding: const EdgeInsets.only(left: 16), | ||||
|                     scrollDirection: Axis.horizontal, | ||||
| @@ -172,66 +172,3 @@ class SearchPage extends HookConsumerWidget { | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|  | ||||
| class ThumbnailWithInfo extends StatelessWidget { | ||||
|   const ThumbnailWithInfo({Key? key, required this.textInfo, required this.imageUrl, required this.onTap}) | ||||
|       : super(key: key); | ||||
|  | ||||
|   final String textInfo; | ||||
|   final String imageUrl; | ||||
|   final Function onTap; | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     var box = Hive.box(userInfoBox); | ||||
|  | ||||
|     return GestureDetector( | ||||
|       onTap: () { | ||||
|         onTap(); | ||||
|       }, | ||||
|       child: Padding( | ||||
|         padding: const EdgeInsets.only(right: 8.0), | ||||
|         child: SizedBox( | ||||
|           width: MediaQuery.of(context).size.width / 3, | ||||
|           height: MediaQuery.of(context).size.width / 3, | ||||
|           child: Stack( | ||||
|             alignment: Alignment.bottomCenter, | ||||
|             children: [ | ||||
|               Container( | ||||
|                 foregroundDecoration: BoxDecoration( | ||||
|                   borderRadius: BorderRadius.circular(10), | ||||
|                   color: Colors.black26, | ||||
|                 ), | ||||
|                 child: ClipRRect( | ||||
|                   borderRadius: BorderRadius.circular(10), | ||||
|                   child: CachedNetworkImage( | ||||
|                     width: 150, | ||||
|                     height: 150, | ||||
|                     fit: BoxFit.cover, | ||||
|                     imageUrl: imageUrl, | ||||
|                     httpHeaders: {"Authorization": "Bearer ${box.get(accessTokenKey)}"}, | ||||
|                   ), | ||||
|                 ), | ||||
|               ), | ||||
|               Positioned( | ||||
|                 bottom: 8, | ||||
|                 left: 10, | ||||
|                 child: SizedBox( | ||||
|                   width: MediaQuery.of(context).size.width / 3, | ||||
|                   child: Text( | ||||
|                     textInfo.capitalizeFirstLetter(), | ||||
|                     style: const TextStyle( | ||||
|                       color: Colors.white, | ||||
|                       fontWeight: FontWeight.bold, | ||||
|                       fontSize: 12, | ||||
|                     ), | ||||
|                   ), | ||||
|                 ), | ||||
|               ), | ||||
|             ], | ||||
|           ), | ||||
|         ), | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -1,6 +1,7 @@ | ||||
| import 'package:auto_route/auto_route.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:flutter_hooks/flutter_hooks.dart'; | ||||
| import 'package:flutter_spinkit/flutter_spinkit.dart'; | ||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| import 'package:immich_mobile/modules/home/ui/daily_title_text.dart'; | ||||
| import 'package:immich_mobile/modules/home/ui/draggable_scrollbar.dart'; | ||||
| @@ -107,7 +108,10 @@ class SearchResultPage extends HookConsumerWidget { | ||||
|       } | ||||
|  | ||||
|       if (searchResultPageState.isLoading) { | ||||
|         return const CircularProgressIndicator.adaptive(); | ||||
|         return Center( | ||||
|             child: SpinKitDancingSquare( | ||||
|           color: Theme.of(context).primaryColor, | ||||
|         )); | ||||
|       } | ||||
|  | ||||
|       if (searchResultPageState.isSuccess) { | ||||
|   | ||||
| @@ -9,7 +9,7 @@ import 'package:immich_mobile/shared/models/immich_asset.model.dart'; | ||||
| import 'package:immich_mobile/shared/views/backup_controller_page.dart'; | ||||
| import 'package:immich_mobile/modules/asset_viewer/views/image_viewer_page.dart'; | ||||
| import 'package:immich_mobile/shared/views/tab_controller_page.dart'; | ||||
| import 'package:immich_mobile/shared/views/video_viewer_page.dart'; | ||||
| import 'package:immich_mobile/modules/asset_viewer/views/video_viewer_page.dart'; | ||||
|  | ||||
| part 'router.gr.dart'; | ||||
|  | ||||
|   | ||||
| @@ -44,7 +44,8 @@ class _$AppRouter extends RootStackRouter { | ||||
|       final args = routeData.argsAs<VideoViewerRouteArgs>(); | ||||
|       return MaterialPageX<dynamic>( | ||||
|           routeData: routeData, | ||||
|           child: VideoViewerPage(key: args.key, videoUrl: args.videoUrl)); | ||||
|           child: VideoViewerPage( | ||||
|               key: args.key, videoUrl: args.videoUrl, asset: args.asset)); | ||||
|     }, | ||||
|     BackupControllerRoute.name: (routeData) { | ||||
|       return MaterialPageX<dynamic>( | ||||
| @@ -163,24 +164,29 @@ class ImageViewerRouteArgs { | ||||
| /// generated route for | ||||
| /// [VideoViewerPage] | ||||
| class VideoViewerRoute extends PageRouteInfo<VideoViewerRouteArgs> { | ||||
|   VideoViewerRoute({Key? key, required String videoUrl}) | ||||
|   VideoViewerRoute( | ||||
|       {Key? key, required String videoUrl, required ImmichAsset asset}) | ||||
|       : super(VideoViewerRoute.name, | ||||
|             path: '/video-viewer-page', | ||||
|             args: VideoViewerRouteArgs(key: key, videoUrl: videoUrl)); | ||||
|             args: VideoViewerRouteArgs( | ||||
|                 key: key, videoUrl: videoUrl, asset: asset)); | ||||
|  | ||||
|   static const String name = 'VideoViewerRoute'; | ||||
| } | ||||
|  | ||||
| class VideoViewerRouteArgs { | ||||
|   const VideoViewerRouteArgs({this.key, required this.videoUrl}); | ||||
|   const VideoViewerRouteArgs( | ||||
|       {this.key, required this.videoUrl, required this.asset}); | ||||
|  | ||||
|   final Key? key; | ||||
|  | ||||
|   final String videoUrl; | ||||
|  | ||||
|   final ImmichAsset asset; | ||||
|  | ||||
|   @override | ||||
|   String toString() { | ||||
|     return 'VideoViewerRouteArgs{key: $key, videoUrl: $videoUrl}'; | ||||
|     return 'VideoViewerRouteArgs{key: $key, videoUrl: $videoUrl, asset: $asset}'; | ||||
|   } | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -10,6 +10,8 @@ class ImmichAsset { | ||||
|   final String modifiedAt; | ||||
|   final bool isFavorite; | ||||
|   final String? duration; | ||||
|   final String originalPath; | ||||
|   final String resizePath; | ||||
|  | ||||
|   ImmichAsset({ | ||||
|     required this.id, | ||||
| @@ -21,6 +23,8 @@ class ImmichAsset { | ||||
|     required this.modifiedAt, | ||||
|     required this.isFavorite, | ||||
|     this.duration, | ||||
|     required this.originalPath, | ||||
|     required this.resizePath, | ||||
|   }); | ||||
|  | ||||
|   ImmichAsset copyWith({ | ||||
| @@ -33,6 +37,8 @@ class ImmichAsset { | ||||
|     String? modifiedAt, | ||||
|     bool? isFavorite, | ||||
|     String? duration, | ||||
|     String? originalPath, | ||||
|     String? resizePath, | ||||
|   }) { | ||||
|     return ImmichAsset( | ||||
|       id: id ?? this.id, | ||||
| @@ -44,6 +50,8 @@ class ImmichAsset { | ||||
|       modifiedAt: modifiedAt ?? this.modifiedAt, | ||||
|       isFavorite: isFavorite ?? this.isFavorite, | ||||
|       duration: duration ?? this.duration, | ||||
|       originalPath: originalPath ?? this.originalPath, | ||||
|       resizePath: resizePath ?? this.resizePath, | ||||
|     ); | ||||
|   } | ||||
|  | ||||
| @@ -58,6 +66,8 @@ class ImmichAsset { | ||||
|       'modifiedAt': modifiedAt, | ||||
|       'isFavorite': isFavorite, | ||||
|       'duration': duration, | ||||
|       'originalPath': originalPath, | ||||
|       'resizePath': resizePath, | ||||
|     }; | ||||
|   } | ||||
|  | ||||
| @@ -72,6 +82,8 @@ class ImmichAsset { | ||||
|       modifiedAt: map['modifiedAt'] ?? '', | ||||
|       isFavorite: map['isFavorite'] ?? false, | ||||
|       duration: map['duration'], | ||||
|       originalPath: map['originalPath'] ?? '', | ||||
|       resizePath: map['resizePath'] ?? '', | ||||
|     ); | ||||
|   } | ||||
|  | ||||
| @@ -81,7 +93,7 @@ class ImmichAsset { | ||||
|  | ||||
|   @override | ||||
|   String toString() { | ||||
|     return 'ImmichAsset(id: $id, deviceAssetId: $deviceAssetId, userId: $userId, deviceId: $deviceId, type: $type, createdAt: $createdAt, modifiedAt: $modifiedAt, isFavorite: $isFavorite, duration: $duration)'; | ||||
|     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 | ||||
| @@ -97,7 +109,9 @@ class ImmichAsset { | ||||
|         other.createdAt == createdAt && | ||||
|         other.modifiedAt == modifiedAt && | ||||
|         other.isFavorite == isFavorite && | ||||
|         other.duration == duration; | ||||
|         other.duration == duration && | ||||
|         other.originalPath == originalPath && | ||||
|         other.resizePath == resizePath; | ||||
|   } | ||||
|  | ||||
|   @override | ||||
| @@ -110,6 +124,8 @@ class ImmichAsset { | ||||
|         createdAt.hashCode ^ | ||||
|         modifiedAt.hashCode ^ | ||||
|         isFavorite.hashCode ^ | ||||
|         duration.hashCode; | ||||
|         duration.hashCode ^ | ||||
|         originalPath.hashCode ^ | ||||
|         resizePath.hashCode; | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -73,7 +73,7 @@ class BackupService { | ||||
|           }); | ||||
|  | ||||
|           // Build thumbnail multipart data | ||||
|           var thumbnailData = await entity.thumbDataWithSize(1280, 720); | ||||
|           var thumbnailData = await entity.thumbnailDataWithSize(const ThumbnailSize(720, 1280)); | ||||
|           if (thumbnailData != null) { | ||||
|             thumbnailUploadData = MultipartFile.fromBytes( | ||||
|               List.from(thumbnailData), | ||||
|   | ||||
| @@ -1,3 +1,4 @@ | ||||
| import 'dart:async'; | ||||
| import 'dart:convert'; | ||||
|  | ||||
| import 'package:dio/dio.dart'; | ||||
| @@ -25,16 +26,36 @@ class NetworkService { | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   Future<dynamic> getRequest({required String url}) async { | ||||
|   Future<dynamic> getRequest({required String url, bool isByteResponse = false, bool isStreamReponse = false}) async { | ||||
|     try { | ||||
|       var dio = Dio(); | ||||
|       dio.interceptors.add(AuthenticatedRequestInterceptor()); | ||||
|  | ||||
|       var savedEndpoint = Hive.box(userInfoBox).get(serverEndpointKey); | ||||
|       Response res = await dio.get('$savedEndpoint/$url'); | ||||
|  | ||||
|       if (res.statusCode == 200) { | ||||
|         return res; | ||||
|       if (isByteResponse) { | ||||
|         Response<List<int>> res = await dio.get<List<int>>( | ||||
|           '$savedEndpoint/$url', | ||||
|           options: Options(responseType: ResponseType.bytes), | ||||
|         ); | ||||
|  | ||||
|         if (res.statusCode == 200) { | ||||
|           return res; | ||||
|         } | ||||
|       } else if (isStreamReponse) { | ||||
|         Response<ResponseBody> res = await dio.get<ResponseBody>( | ||||
|           '$savedEndpoint/$url', | ||||
|           options: Options(responseType: ResponseType.stream), | ||||
|         ); | ||||
|  | ||||
|         if (res.statusCode == 200) { | ||||
|           return res; | ||||
|         } | ||||
|       } else { | ||||
|         Response res = await dio.get('$savedEndpoint/$url'); | ||||
|         if (res.statusCode == 200) { | ||||
|           return res; | ||||
|         } | ||||
|       } | ||||
|     } on DioError catch (e) { | ||||
|       debugPrint("DioError: ${e.response}"); | ||||
|   | ||||
| @@ -8,12 +8,24 @@ class ImmichToast { | ||||
|     required BuildContext context, | ||||
|     required String msg, | ||||
|     ToastType toastType = ToastType.info, | ||||
|     ToastGravity gravity = ToastGravity.TOP, | ||||
|   }) { | ||||
|     FToast fToast; | ||||
|  | ||||
|     fToast = FToast(); | ||||
|     fToast.init(context); | ||||
|  | ||||
|     _getColor(ToastType type, BuildContext context) { | ||||
|       switch (type) { | ||||
|         case ToastType.info: | ||||
|           return Theme.of(context).primaryColor; | ||||
|         case ToastType.success: | ||||
|           return const Color.fromARGB(255, 78, 140, 124); | ||||
|         case ToastType.error: | ||||
|           return const Color.fromARGB(255, 220, 48, 85); | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     fToast.showToast( | ||||
|       child: Container( | ||||
|         padding: const EdgeInsets.symmetric(horizontal: 24.0, vertical: 12.0), | ||||
| @@ -36,8 +48,8 @@ class ImmichToast { | ||||
|                 : Container(), | ||||
|             (toastType == ToastType.success) | ||||
|                 ? const Icon( | ||||
|                     Icons.check, | ||||
|                     color: Color.fromARGB(255, 104, 248, 140), | ||||
|                     Icons.check_circle_rounded, | ||||
|                     color: Color.fromARGB(255, 78, 140, 124), | ||||
|                   ) | ||||
|                 : Container(), | ||||
|             (toastType == ToastType.error) | ||||
| @@ -53,7 +65,7 @@ class ImmichToast { | ||||
|               child: Text( | ||||
|                 msg, | ||||
|                 style: TextStyle( | ||||
|                   color: Theme.of(context).primaryColor, | ||||
|                   color: _getColor(toastType, context), | ||||
|                   fontWeight: FontWeight.bold, | ||||
|                   fontSize: 15, | ||||
|                 ), | ||||
| @@ -62,7 +74,7 @@ class ImmichToast { | ||||
|           ], | ||||
|         ), | ||||
|       ), | ||||
|       gravity: ToastGravity.TOP, | ||||
|       gravity: gravity, | ||||
|       toastDuration: const Duration(seconds: 2), | ||||
|     ); | ||||
|   } | ||||
|   | ||||
| @@ -328,6 +328,13 @@ packages: | ||||
|       url: "https://pub.dartlang.org" | ||||
|     source: hosted | ||||
|     version: "2.0.0-dev.0" | ||||
|   flutter_spinkit: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
|       name: flutter_spinkit | ||||
|       url: "https://pub.dartlang.org" | ||||
|     source: hosted | ||||
|     version: "5.1.0" | ||||
|   flutter_test: | ||||
|     dependency: "direct dev" | ||||
|     description: flutter | ||||
| @@ -680,7 +687,7 @@ packages: | ||||
|       name: photo_manager | ||||
|       url: "https://pub.dartlang.org" | ||||
|     source: hosted | ||||
|     version: "1.3.10" | ||||
|     version: "2.0.6" | ||||
|   photo_view: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
|   | ||||
| @@ -11,7 +11,7 @@ dependencies: | ||||
|   flutter: | ||||
|     sdk: flutter | ||||
|   cupertino_icons: ^1.0.2 | ||||
|   photo_manager: ^1.3.10 | ||||
|   photo_manager: ^2.0.6 | ||||
|   flutter_hooks: ^0.18.0 | ||||
|   hooks_riverpod: ^2.0.0-dev.0 | ||||
|   hive: | ||||
| @@ -33,11 +33,11 @@ dependencies: | ||||
|   badges: ^2.0.2 | ||||
|   photo_view: ^0.13.0 | ||||
|   socket_io_client: ^2.0.0-beta.4-nullsafety.0 | ||||
|   # mapbox_gl: ^0.15.0 | ||||
|   flutter_map: ^0.14.0 | ||||
|   flutter_udid: ^2.0.0 | ||||
|   package_info_plus: ^1.4.0 | ||||
|  | ||||
|   flutter_spinkit: ^5.1.0 | ||||
|    | ||||
| dev_dependencies: | ||||
|   flutter_test: | ||||
|     sdk: flutter | ||||
|   | ||||
| @@ -76,6 +76,15 @@ export class AssetController { | ||||
|     return 'ok'; | ||||
|   } | ||||
|  | ||||
|   @Get('/download') | ||||
|   async downloadFile( | ||||
|     @GetAuthUser() authUser: AuthUserDto, | ||||
|     @Response({ passthrough: true }) res: Res, | ||||
|     @Query(ValidationPipe) query: ServeFileDto, | ||||
|   ) { | ||||
|     return this.assetService.downloadFile(authUser, query, res); | ||||
|   } | ||||
|  | ||||
|   @Get('/file') | ||||
|   async serveFile( | ||||
|     @Headers() headers, | ||||
|   | ||||
| @@ -13,6 +13,7 @@ import { Response as Res } from 'express'; | ||||
| import { promisify } from 'util'; | ||||
| import { DeleteAssetDto } from './dto/delete-asset.dto'; | ||||
| import { SearchAssetDto } from './dto/search-asset.dto'; | ||||
| import path from 'path'; | ||||
|  | ||||
| const fileInfo = promisify(stat); | ||||
|  | ||||
| @@ -146,10 +147,26 @@ export class AssetService { | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   public async downloadFile(authUser: AuthUserDto, query: ServeFileDto, res: Res) { | ||||
|     let file = null; | ||||
|     const asset = await this.findOne(authUser, query.did, query.aid); | ||||
|  | ||||
|     if (query.isThumb === 'false' || !query.isThumb) { | ||||
|       file = createReadStream(asset.originalPath); | ||||
|     } else { | ||||
|       file = createReadStream(asset.resizePath); | ||||
|     } | ||||
|  | ||||
|     return new StreamableFile(file); | ||||
|   } | ||||
|  | ||||
|   public async serveFile(authUser: AuthUserDto, query: ServeFileDto, res: Res, headers: any) { | ||||
|     let file = null; | ||||
|     const asset = await this.findOne(authUser, query.did, query.aid); | ||||
|  | ||||
|     if (!asset) { | ||||
|       throw new BadRequestException('Asset does not exist'); | ||||
|     } | ||||
|     // Handle Sending Images | ||||
|     if (asset.type == AssetType.IMAGE || query.isThumb == 'true') { | ||||
|       res.set({ | ||||
|   | ||||
		Reference in New Issue
	
	Block a user