mirror of
				https://github.com/KevinMidboe/immich.git
				synced 2025-10-29 17:40:28 +00:00 
			
		
		
		
	Implemented user profile upload and show on web/mobile (#191)
* Update mobile dependencies * Added image picker * Added mechanism to upload profile image * Added image type to send to web * Added styling for circle avatar * Fixxed issue with sharp cannot resize image properly * Finished displaying and uploading user profile * Added user profile to web
This commit is contained in:
		| @@ -9,6 +9,8 @@ PODS: | ||||
|   - FMDB (2.7.5): | ||||
|     - FMDB/standard (= 2.7.5) | ||||
|   - FMDB/standard (2.7.5) | ||||
|   - image_picker_ios (0.0.1): | ||||
|     - Flutter | ||||
|   - package_info_plus (0.4.5): | ||||
|     - Flutter | ||||
|   - path_provider_ios (0.0.1): | ||||
| @@ -30,6 +32,7 @@ DEPENDENCIES: | ||||
|   - Flutter (from `Flutter`) | ||||
|   - flutter_udid (from `.symlinks/plugins/flutter_udid/ios`) | ||||
|   - fluttertoast (from `.symlinks/plugins/fluttertoast/ios`) | ||||
|   - image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`) | ||||
|   - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) | ||||
|   - path_provider_ios (from `.symlinks/plugins/path_provider_ios/ios`) | ||||
|   - photo_manager (from `.symlinks/plugins/photo_manager/ios`) | ||||
| @@ -50,6 +53,8 @@ EXTERNAL SOURCES: | ||||
|     :path: ".symlinks/plugins/flutter_udid/ios" | ||||
|   fluttertoast: | ||||
|     :path: ".symlinks/plugins/fluttertoast/ios" | ||||
|   image_picker_ios: | ||||
|     :path: ".symlinks/plugins/image_picker_ios/ios" | ||||
|   package_info_plus: | ||||
|     :path: ".symlinks/plugins/package_info_plus/ios" | ||||
|   path_provider_ios: | ||||
| @@ -68,6 +73,7 @@ SPEC CHECKSUMS: | ||||
|   flutter_udid: 0848809dbed4c055175747ae6a45a8b4f6771e1c | ||||
|   fluttertoast: 16fbe6039d06a763f3533670197d01fc73459037 | ||||
|   FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a | ||||
|   image_picker_ios: b786a5dcf033a8336a657191401bfdf12017dabb | ||||
|   package_info_plus: 6c92f08e1f853dc01228d6f553146438dafcd14e | ||||
|   path_provider_ios: 14f3d2fd28c4fdb42f44e0f751d12861c43cee02 | ||||
|   photo_manager: 4f6810b7dfc4feb03b461ac1a70dacf91fba7604 | ||||
|   | ||||
| @@ -43,6 +43,12 @@ | ||||
|     <key>NSPhotoLibraryAddUsageDescription</key> | ||||
|     <string>We need to manage backup your photos album</string> | ||||
|  | ||||
|     <key>NSCameraUsageDescription</key> | ||||
|     <string>We need to access the camera to let you take beautiful video using this app</string> | ||||
|  | ||||
|     <key>NSMicrophoneUsageDescription</key> | ||||
|     <string>We need to access the microphone to let you take beautiful video using this app</string> | ||||
|  | ||||
|     <key>UILaunchStoryboardName</key> | ||||
|     <string>LaunchScreen</string> | ||||
|     <key>UIMainStoryboardFile</key> | ||||
| @@ -68,7 +74,7 @@ | ||||
|     <true /> | ||||
|     <key>ITSAppUsesNonExemptEncryption</key> | ||||
|     <false /> | ||||
|   	<key>CADisableMinimumFrameDurationOnPhone</key> | ||||
| 	<true/> | ||||
| </dict> | ||||
| </plist> | ||||
|     <key>CADisableMinimumFrameDurationOnPhone</key> | ||||
|     <true /> | ||||
|   </dict> | ||||
| </plist> | ||||
| @@ -76,7 +76,7 @@ class _ImmichAppState extends ConsumerState<ImmichApp> with WidgetsBindingObserv | ||||
|   } | ||||
|  | ||||
|   Future<void> initApp() async { | ||||
|     WidgetsBinding.instance?.addObserver(this); | ||||
|     WidgetsBinding.instance.addObserver(this); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
| @@ -87,7 +87,7 @@ class _ImmichAppState extends ConsumerState<ImmichApp> with WidgetsBindingObserv | ||||
|  | ||||
|   @override | ||||
|   void dispose() { | ||||
|     WidgetsBinding.instance?.removeObserver(this); | ||||
|     WidgetsBinding.instance.removeObserver(this); | ||||
|     super.dispose(); | ||||
|   } | ||||
|  | ||||
|   | ||||
| @@ -38,5 +38,7 @@ class HiveBackupAlbumsAdapter extends TypeAdapter<HiveBackupAlbums> { | ||||
|   @override | ||||
|   bool operator ==(Object other) => | ||||
|       identical(this, other) || | ||||
|       other is HiveBackupAlbumsAdapter && runtimeType == other.runtimeType && typeId == other.typeId; | ||||
|       other is HiveBackupAlbumsAdapter && | ||||
|           runtimeType == other.runtimeType && | ||||
|           typeId == other.typeId; | ||||
| } | ||||
|   | ||||
| @@ -45,12 +45,16 @@ class BackupControllerPage extends HookConsumerWidget { | ||||
|           child: Column( | ||||
|             crossAxisAlignment: CrossAxisAlignment.start, | ||||
|             children: [ | ||||
|               LinearPercentIndicator( | ||||
|               Padding( | ||||
|                 padding: const EdgeInsets.only(top: 8.0), | ||||
|                 lineHeight: 5.0, | ||||
|                 percent: backupState.serverInfo.diskUsagePercentage / 100.0, | ||||
|                 backgroundColor: Colors.grey, | ||||
|                 progressColor: Theme.of(context).primaryColor, | ||||
|                 child: LinearPercentIndicator( | ||||
|                   padding: const EdgeInsets.symmetric(horizontal: 0, vertical: 0), | ||||
|                   barRadius: const Radius.circular(2), | ||||
|                   lineHeight: 6.0, | ||||
|                   percent: backupState.serverInfo.diskUsagePercentage / 100.0, | ||||
|                   backgroundColor: Colors.grey, | ||||
|                   progressColor: Theme.of(context).primaryColor, | ||||
|                 ), | ||||
|               ), | ||||
|               Padding( | ||||
|                 padding: const EdgeInsets.only(top: 12.0), | ||||
|   | ||||
| @@ -0,0 +1,93 @@ | ||||
| import 'dart:convert'; | ||||
|  | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| import 'package:image_picker/image_picker.dart'; | ||||
|  | ||||
| import 'package:immich_mobile/shared/services/user.service.dart'; | ||||
|  | ||||
| enum UploadProfileStatus { | ||||
|   idle, | ||||
|   loading, | ||||
|   success, | ||||
|   failure, | ||||
| } | ||||
|  | ||||
| class UploadProfileImageState { | ||||
|   // enum | ||||
|   final UploadProfileStatus status; | ||||
|   final String profileImagePath; | ||||
|   UploadProfileImageState({ | ||||
|     required this.status, | ||||
|     required this.profileImagePath, | ||||
|   }); | ||||
|  | ||||
|   UploadProfileImageState copyWith({ | ||||
|     UploadProfileStatus? status, | ||||
|     String? profileImagePath, | ||||
|   }) { | ||||
|     return UploadProfileImageState( | ||||
|       status: status ?? this.status, | ||||
|       profileImagePath: profileImagePath ?? this.profileImagePath, | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   Map<String, dynamic> toMap() { | ||||
|     final result = <String, dynamic>{}; | ||||
|  | ||||
|     result.addAll({'status': status.index}); | ||||
|     result.addAll({'profileImagePath': profileImagePath}); | ||||
|  | ||||
|     return result; | ||||
|   } | ||||
|  | ||||
|   factory UploadProfileImageState.fromMap(Map<String, dynamic> map) { | ||||
|     return UploadProfileImageState( | ||||
|       status: UploadProfileStatus.values[map['status'] ?? 0], | ||||
|       profileImagePath: map['profileImagePath'] ?? '', | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   String toJson() => json.encode(toMap()); | ||||
|  | ||||
|   factory UploadProfileImageState.fromJson(String source) => UploadProfileImageState.fromMap(json.decode(source)); | ||||
|  | ||||
|   @override | ||||
|   String toString() => 'UploadProfileImageState(status: $status, profileImagePath: $profileImagePath)'; | ||||
|  | ||||
|   @override | ||||
|   bool operator ==(Object other) { | ||||
|     if (identical(this, other)) return true; | ||||
|  | ||||
|     return other is UploadProfileImageState && other.status == status && other.profileImagePath == profileImagePath; | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   int get hashCode => status.hashCode ^ profileImagePath.hashCode; | ||||
| } | ||||
|  | ||||
| class UploadProfileImageNotifier extends StateNotifier<UploadProfileImageState> { | ||||
|   UploadProfileImageNotifier() | ||||
|       : super(UploadProfileImageState( | ||||
|           profileImagePath: '', | ||||
|           status: UploadProfileStatus.idle, | ||||
|         )); | ||||
|  | ||||
|   Future<bool> upload(XFile file) async { | ||||
|     state = state.copyWith(status: UploadProfileStatus.loading); | ||||
|  | ||||
|     var res = await UserService().uploadProfileImage(file); | ||||
|  | ||||
|     if (res != null) { | ||||
|       debugPrint("Succesfully upload profile image"); | ||||
|       state = state.copyWith(status: UploadProfileStatus.success, profileImagePath: res.profileImagePath); | ||||
|       return true; | ||||
|     } | ||||
|  | ||||
|     state = state.copyWith(status: UploadProfileStatus.failure); | ||||
|     return false; | ||||
|   } | ||||
| } | ||||
|  | ||||
| final uploadProfileImageProvider = | ||||
|     StateNotifierProvider<UploadProfileImageNotifier, UploadProfileImageState>(((ref) => UploadProfileImageNotifier())); | ||||
| @@ -1,7 +1,11 @@ | ||||
| import 'package:auto_route/auto_route.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:flutter_hooks/flutter_hooks.dart'; | ||||
| import 'package:hive_flutter/hive_flutter.dart'; | ||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| import 'package:image_picker/image_picker.dart'; | ||||
| import 'package:immich_mobile/constants/hive_box.dart'; | ||||
| import 'package:immich_mobile/modules/home/providers/upload_profile_image.provider.dart'; | ||||
| import 'package:immich_mobile/shared/providers/asset.provider.dart'; | ||||
| import 'package:immich_mobile/modules/login/models/authentication_state.model.dart'; | ||||
| import 'package:immich_mobile/modules/login/providers/authentication.provider.dart'; | ||||
| @@ -9,17 +13,21 @@ import 'package:immich_mobile/shared/models/server_info_state.model.dart'; | ||||
| import 'package:immich_mobile/modules/backup/providers/backup.provider.dart'; | ||||
| import 'package:immich_mobile/shared/providers/server_info.provider.dart'; | ||||
| import 'package:immich_mobile/shared/providers/websocket.provider.dart'; | ||||
| import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart'; | ||||
| import 'package:package_info_plus/package_info_plus.dart'; | ||||
| import 'dart:math'; | ||||
|  | ||||
| class ProfileDrawer extends HookConsumerWidget { | ||||
|   const ProfileDrawer({Key? key}) : super(key: key); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context, WidgetRef ref) { | ||||
|     String endpoint = Hive.box(userInfoBox).get(serverEndpointKey); | ||||
|     AuthenticationState _authState = ref.watch(authenticationProvider); | ||||
|     ServerInfoState _serverInfoState = ref.watch(serverInfoProvider); | ||||
|  | ||||
|     final uploadProfileImageStatus = ref.watch(uploadProfileImageProvider).status; | ||||
|     final appInfo = useState({}); | ||||
|     var dummmy = Random().nextInt(1024); | ||||
|  | ||||
|     _getPackageInfo() async { | ||||
|       PackageInfo packageInfo = await PackageInfo.fromPlatform(); | ||||
| @@ -30,19 +38,74 @@ class ProfileDrawer extends HookConsumerWidget { | ||||
|       }; | ||||
|     } | ||||
|  | ||||
|     _buildUserProfileImage() { | ||||
|       if (_authState.profileImagePath.isEmpty) { | ||||
|         return const CircleAvatar( | ||||
|           radius: 35, | ||||
|           backgroundImage: AssetImage('assets/immich-logo-no-outline.png'), | ||||
|           backgroundColor: Colors.transparent, | ||||
|         ); | ||||
|       } | ||||
|  | ||||
|       if (uploadProfileImageStatus == UploadProfileStatus.idle) { | ||||
|         if (_authState.profileImagePath.isNotEmpty) { | ||||
|           return CircleAvatar( | ||||
|             radius: 35, | ||||
|             backgroundImage: NetworkImage('$endpoint/user/profile-image/${_authState.userId}?d=${dummmy++}'), | ||||
|             backgroundColor: Colors.transparent, | ||||
|           ); | ||||
|         } else { | ||||
|           return const CircleAvatar( | ||||
|             radius: 35, | ||||
|             backgroundImage: AssetImage('assets/immich-logo-no-outline.png'), | ||||
|             backgroundColor: Colors.transparent, | ||||
|           ); | ||||
|         } | ||||
|       } | ||||
|  | ||||
|       if (uploadProfileImageStatus == UploadProfileStatus.success) { | ||||
|         return CircleAvatar( | ||||
|           radius: 35, | ||||
|           backgroundImage: NetworkImage('$endpoint/user/profile-image/${_authState.userId}?d=${dummmy++}'), | ||||
|           backgroundColor: Colors.transparent, | ||||
|         ); | ||||
|       } | ||||
|  | ||||
|       if (uploadProfileImageStatus == UploadProfileStatus.failure) { | ||||
|         return const CircleAvatar( | ||||
|           radius: 35, | ||||
|           backgroundImage: AssetImage('assets/immich-logo-no-outline.png'), | ||||
|           backgroundColor: Colors.transparent, | ||||
|         ); | ||||
|       } | ||||
|  | ||||
|       if (uploadProfileImageStatus == UploadProfileStatus.loading) { | ||||
|         return const ImmichLoadingIndicator(); | ||||
|       } | ||||
|  | ||||
|       return Container(); | ||||
|     } | ||||
|  | ||||
|     _pickUserProfileImage() async { | ||||
|       final XFile? image = await ImagePicker().pickImage(source: ImageSource.gallery, maxHeight: 1024, maxWidth: 1024); | ||||
|  | ||||
|       if (image != null) { | ||||
|         var success = await ref.watch(uploadProfileImageProvider.notifier).upload(image); | ||||
|  | ||||
|         if (success) { | ||||
|           ref | ||||
|               .watch(authenticationProvider.notifier) | ||||
|               .updateUserProfileImagePath(ref.read(uploadProfileImageProvider).profileImagePath); | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     useEffect(() { | ||||
|       _getPackageInfo(); | ||||
|  | ||||
|       _buildUserProfileImage(); | ||||
|       return null; | ||||
|     }, []); | ||||
|  | ||||
|     return Drawer( | ||||
|       shape: const RoundedRectangleBorder( | ||||
|         borderRadius: BorderRadius.only( | ||||
|           topRight: Radius.circular(5), | ||||
|           bottomRight: Radius.circular(5), | ||||
|         ), | ||||
|       ), | ||||
|       child: Column( | ||||
|         mainAxisAlignment: MainAxisAlignment.spaceBetween, | ||||
|         children: [ | ||||
| @@ -51,22 +114,60 @@ class ProfileDrawer extends HookConsumerWidget { | ||||
|             padding: EdgeInsets.zero, | ||||
|             children: [ | ||||
|               DrawerHeader( | ||||
|                 decoration: BoxDecoration( | ||||
|                   color: Colors.grey[200], | ||||
|                 decoration: const BoxDecoration( | ||||
|                   gradient: LinearGradient( | ||||
|                     colors: [Color.fromARGB(255, 216, 219, 238), Color.fromARGB(255, 226, 230, 231)], | ||||
|                     begin: Alignment.centerRight, | ||||
|                     end: Alignment.centerLeft, | ||||
|                   ), | ||||
|                 ), | ||||
|                 child: Column( | ||||
|                   mainAxisAlignment: MainAxisAlignment.center, | ||||
|                   crossAxisAlignment: CrossAxisAlignment.center, | ||||
|                   mainAxisAlignment: MainAxisAlignment.start, | ||||
|                   crossAxisAlignment: CrossAxisAlignment.start, | ||||
|                   children: [ | ||||
|                     const Image( | ||||
|                       image: AssetImage('assets/immich-logo-no-outline.png'), | ||||
|                       width: 50, | ||||
|                       filterQuality: FilterQuality.high, | ||||
|                     Stack( | ||||
|                       clipBehavior: Clip.none, | ||||
|                       children: [ | ||||
|                         _buildUserProfileImage(), | ||||
|                         Positioned( | ||||
|                           bottom: 0, | ||||
|                           right: -5, | ||||
|                           child: GestureDetector( | ||||
|                             onTap: _pickUserProfileImage, | ||||
|                             child: Material( | ||||
|                               color: Colors.grey[50], | ||||
|                               elevation: 2, | ||||
|                               shape: RoundedRectangleBorder( | ||||
|                                 borderRadius: BorderRadius.circular(50.0), | ||||
|                               ), | ||||
|                               child: Padding( | ||||
|                                 padding: const EdgeInsets.all(5.0), | ||||
|                                 child: Icon( | ||||
|                                   Icons.edit, | ||||
|                                   color: Theme.of(context).primaryColor, | ||||
|                                   size: 14, | ||||
|                                 ), | ||||
|                               ), | ||||
|                             ), | ||||
|                           ), | ||||
|                         ), | ||||
|                       ], | ||||
|                     ), | ||||
|                     const Padding(padding: EdgeInsets.all(8)), | ||||
|                     Text( | ||||
|                       _authState.userEmail, | ||||
|                       style: TextStyle(color: Theme.of(context).primaryColor, fontWeight: FontWeight.bold), | ||||
|                       "${_authState.firstName} ${_authState.lastName}", | ||||
|                       style: TextStyle( | ||||
|                         color: Theme.of(context).primaryColor, | ||||
|                         fontWeight: FontWeight.bold, | ||||
|                         fontSize: 24, | ||||
|                       ), | ||||
|                     ), | ||||
|                     Padding( | ||||
|                       padding: const EdgeInsets.only(top: 4.0), | ||||
|                       child: Text( | ||||
|                         _authState.userEmail, | ||||
|                         style: TextStyle(color: Colors.grey[800], fontSize: 12), | ||||
|                       ), | ||||
|                     ) | ||||
|                   ], | ||||
|                 ), | ||||
| @@ -97,7 +198,15 @@ class ProfileDrawer extends HookConsumerWidget { | ||||
|           Padding( | ||||
|             padding: const EdgeInsets.all(8.0), | ||||
|             child: Card( | ||||
|               elevation: 0, | ||||
|               color: Colors.grey[100], | ||||
|               shape: RoundedRectangleBorder( | ||||
|                 borderRadius: BorderRadius.circular(5), // if you need this | ||||
|                 side: const BorderSide( | ||||
|                   color: Color.fromARGB(101, 201, 201, 201), | ||||
|                   width: 1, | ||||
|                 ), | ||||
|               ), | ||||
|               child: Padding( | ||||
|                 padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 8), | ||||
|                 child: Column( | ||||
|   | ||||
| @@ -8,6 +8,11 @@ class AuthenticationState { | ||||
|   final String userId; | ||||
|   final String userEmail; | ||||
|   final bool isAuthenticated; | ||||
|   final String firstName; | ||||
|   final String lastName; | ||||
|   final bool isAdmin; | ||||
|   final bool isFirstLogin; | ||||
|   final String profileImagePath; | ||||
|   final DeviceInfoRemote deviceInfo; | ||||
|  | ||||
|   AuthenticationState({ | ||||
| @@ -16,6 +21,11 @@ class AuthenticationState { | ||||
|     required this.userId, | ||||
|     required this.userEmail, | ||||
|     required this.isAuthenticated, | ||||
|     required this.firstName, | ||||
|     required this.lastName, | ||||
|     required this.isAdmin, | ||||
|     required this.isFirstLogin, | ||||
|     required this.profileImagePath, | ||||
|     required this.deviceInfo, | ||||
|   }); | ||||
|  | ||||
| @@ -25,6 +35,11 @@ class AuthenticationState { | ||||
|     String? userId, | ||||
|     String? userEmail, | ||||
|     bool? isAuthenticated, | ||||
|     String? firstName, | ||||
|     String? lastName, | ||||
|     bool? isAdmin, | ||||
|     bool? isFirstLoggedIn, | ||||
|     String? profileImagePath, | ||||
|     DeviceInfoRemote? deviceInfo, | ||||
|   }) { | ||||
|     return AuthenticationState( | ||||
| @@ -33,24 +48,36 @@ class AuthenticationState { | ||||
|       userId: userId ?? this.userId, | ||||
|       userEmail: userEmail ?? this.userEmail, | ||||
|       isAuthenticated: isAuthenticated ?? this.isAuthenticated, | ||||
|       firstName: firstName ?? this.firstName, | ||||
|       lastName: lastName ?? this.lastName, | ||||
|       isAdmin: isAdmin ?? this.isAdmin, | ||||
|       isFirstLogin: isFirstLoggedIn ?? isFirstLogin, | ||||
|       profileImagePath: profileImagePath ?? this.profileImagePath, | ||||
|       deviceInfo: deviceInfo ?? this.deviceInfo, | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   String toString() { | ||||
|     return 'AuthenticationState(deviceId: $deviceId, deviceType: $deviceType, userId: $userId, userEmail: $userEmail, isAuthenticated: $isAuthenticated, deviceInfo: $deviceInfo)'; | ||||
|     return 'AuthenticationState(deviceId: $deviceId, deviceType: $deviceType, userId: $userId, userEmail: $userEmail, isAuthenticated: $isAuthenticated, firstName: $firstName, lastName: $lastName, isAdmin: $isAdmin, isFirstLoggedIn: $isFirstLogin, profileImagePath: $profileImagePath, deviceInfo: $deviceInfo)'; | ||||
|   } | ||||
|  | ||||
|   Map<String, dynamic> toMap() { | ||||
|     return { | ||||
|       'deviceId': deviceId, | ||||
|       'deviceType': deviceType, | ||||
|       'userId': userId, | ||||
|       'userEmail': userEmail, | ||||
|       'isAuthenticated': isAuthenticated, | ||||
|       'deviceInfo': deviceInfo.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({'isFirstLogin': isFirstLogin}); | ||||
|     result.addAll({'profileImagePath': profileImagePath}); | ||||
|     result.addAll({'deviceInfo': deviceInfo.toMap()}); | ||||
|  | ||||
|     return result; | ||||
|   } | ||||
|  | ||||
|   factory AuthenticationState.fromMap(Map<String, dynamic> map) { | ||||
| @@ -60,6 +87,11 @@ class AuthenticationState { | ||||
|       userId: map['userId'] ?? '', | ||||
|       userEmail: map['userEmail'] ?? '', | ||||
|       isAuthenticated: map['isAuthenticated'] ?? false, | ||||
|       firstName: map['firstName'] ?? '', | ||||
|       lastName: map['lastName'] ?? '', | ||||
|       isAdmin: map['isAdmin'] ?? false, | ||||
|       isFirstLogin: map['isFirstLogin'] ?? false, | ||||
|       profileImagePath: map['profileImagePath'] ?? '', | ||||
|       deviceInfo: DeviceInfoRemote.fromMap(map['deviceInfo']), | ||||
|     ); | ||||
|   } | ||||
| @@ -78,6 +110,11 @@ class AuthenticationState { | ||||
|         other.userId == userId && | ||||
|         other.userEmail == userEmail && | ||||
|         other.isAuthenticated == isAuthenticated && | ||||
|         other.firstName == firstName && | ||||
|         other.lastName == lastName && | ||||
|         other.isAdmin == isAdmin && | ||||
|         other.isFirstLogin == isFirstLogin && | ||||
|         other.profileImagePath == profileImagePath && | ||||
|         other.deviceInfo == deviceInfo; | ||||
|   } | ||||
|  | ||||
| @@ -88,6 +125,11 @@ class AuthenticationState { | ||||
|         userId.hashCode ^ | ||||
|         userEmail.hashCode ^ | ||||
|         isAuthenticated.hashCode ^ | ||||
|         firstName.hashCode ^ | ||||
|         lastName.hashCode ^ | ||||
|         isAdmin.hashCode ^ | ||||
|         isFirstLogin.hashCode ^ | ||||
|         profileImagePath.hashCode ^ | ||||
|         deviceInfo.hashCode; | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -4,31 +4,58 @@ class LogInReponse { | ||||
|   final String accessToken; | ||||
|   final String userId; | ||||
|   final String userEmail; | ||||
|   final String firstName; | ||||
|   final String lastName; | ||||
|   final String profileImagePath; | ||||
|   final bool isAdmin; | ||||
|   final bool isFirstLogin; | ||||
|  | ||||
|   LogInReponse({ | ||||
|     required this.accessToken, | ||||
|     required this.userId, | ||||
|     required this.userEmail, | ||||
|     required this.firstName, | ||||
|     required this.lastName, | ||||
|     required this.profileImagePath, | ||||
|     required this.isAdmin, | ||||
|     required this.isFirstLogin, | ||||
|   }); | ||||
|  | ||||
|   LogInReponse copyWith({ | ||||
|     String? accessToken, | ||||
|     String? userId, | ||||
|     String? userEmail, | ||||
|     String? firstName, | ||||
|     String? lastName, | ||||
|     String? profileImagePath, | ||||
|     bool? isAdmin, | ||||
|     bool? isFirstLogin, | ||||
|   }) { | ||||
|     return LogInReponse( | ||||
|       accessToken: accessToken ?? this.accessToken, | ||||
|       userId: userId ?? this.userId, | ||||
|       userEmail: userEmail ?? this.userEmail, | ||||
|       firstName: firstName ?? this.firstName, | ||||
|       lastName: lastName ?? this.lastName, | ||||
|       profileImagePath: profileImagePath ?? this.profileImagePath, | ||||
|       isAdmin: isAdmin ?? this.isAdmin, | ||||
|       isFirstLogin: isFirstLogin ?? this.isFirstLogin, | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   Map<String, dynamic> toMap() { | ||||
|     return { | ||||
|       'accessToken': accessToken, | ||||
|       'userId': userId, | ||||
|       'userEmail': userEmail, | ||||
|     }; | ||||
|     final result = <String, dynamic>{}; | ||||
|  | ||||
|     result.addAll({'accessToken': accessToken}); | ||||
|     result.addAll({'userId': userId}); | ||||
|     result.addAll({'userEmail': userEmail}); | ||||
|     result.addAll({'firstName': firstName}); | ||||
|     result.addAll({'lastName': lastName}); | ||||
|     result.addAll({'profileImagePath': profileImagePath}); | ||||
|     result.addAll({'isAdmin': isAdmin}); | ||||
|     result.addAll({'isFirstLogin': isFirstLogin}); | ||||
|  | ||||
|     return result; | ||||
|   } | ||||
|  | ||||
|   factory LogInReponse.fromMap(Map<String, dynamic> map) { | ||||
| @@ -36,6 +63,11 @@ class LogInReponse { | ||||
|       accessToken: map['accessToken'] ?? '', | ||||
|       userId: map['userId'] ?? '', | ||||
|       userEmail: map['userEmail'] ?? '', | ||||
|       firstName: map['firstName'] ?? '', | ||||
|       lastName: map['lastName'] ?? '', | ||||
|       profileImagePath: map['profileImagePath'] ?? '', | ||||
|       isAdmin: map['isAdmin'] ?? false, | ||||
|       isFirstLogin: map['isFirstLogin'] ?? false, | ||||
|     ); | ||||
|   } | ||||
|  | ||||
| @@ -44,7 +76,9 @@ class LogInReponse { | ||||
|   factory LogInReponse.fromJson(String source) => LogInReponse.fromMap(json.decode(source)); | ||||
|  | ||||
|   @override | ||||
|   String toString() => 'LogInReponse(accessToken: $accessToken, userId: $userId, userEmail: $userEmail)'; | ||||
|   String toString() { | ||||
|     return 'LogInReponse(accessToken: $accessToken, userId: $userId, userEmail: $userEmail, firstName: $firstName, lastName: $lastName, profileImagePath: $profileImagePath, isAdmin: $isAdmin, isFirstLogin: $isFirstLogin)'; | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   bool operator ==(Object other) { | ||||
| @@ -53,9 +87,23 @@ class LogInReponse { | ||||
|     return other is LogInReponse && | ||||
|         other.accessToken == accessToken && | ||||
|         other.userId == userId && | ||||
|         other.userEmail == userEmail; | ||||
|         other.userEmail == userEmail && | ||||
|         other.firstName == firstName && | ||||
|         other.lastName == lastName && | ||||
|         other.profileImagePath == profileImagePath && | ||||
|         other.isAdmin == isAdmin && | ||||
|         other.isFirstLogin == isFirstLogin; | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   int get hashCode => accessToken.hashCode ^ userId.hashCode ^ userEmail.hashCode; | ||||
|   int get hashCode { | ||||
|     return accessToken.hashCode ^ | ||||
|         userId.hashCode ^ | ||||
|         userEmail.hashCode ^ | ||||
|         firstName.hashCode ^ | ||||
|         lastName.hashCode ^ | ||||
|         profileImagePath.hashCode ^ | ||||
|         isAdmin.hashCode ^ | ||||
|         isFirstLogin.hashCode; | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -17,9 +17,14 @@ class AuthenticationNotifier extends StateNotifier<AuthenticationState> { | ||||
|           AuthenticationState( | ||||
|             deviceId: "", | ||||
|             deviceType: "", | ||||
|             isAuthenticated: false, | ||||
|             userId: "", | ||||
|             userEmail: "", | ||||
|             firstName: '', | ||||
|             lastName: '', | ||||
|             profileImagePath: '', | ||||
|             isAdmin: false, | ||||
|             isFirstLogin: false, | ||||
|             isAuthenticated: false, | ||||
|             deviceInfo: DeviceInfoRemote( | ||||
|               id: 0, | ||||
|               userId: "", | ||||
| @@ -76,6 +81,11 @@ class AuthenticationNotifier extends StateNotifier<AuthenticationState> { | ||||
|         isAuthenticated: true, | ||||
|         userId: payload.userId, | ||||
|         userEmail: payload.userEmail, | ||||
|         firstName: payload.firstName, | ||||
|         lastName: payload.lastName, | ||||
|         profileImagePath: payload.profileImagePath, | ||||
|         isAdmin: payload.isAdmin, | ||||
|         isFirstLoggedIn: payload.isFirstLogin, | ||||
|       ); | ||||
|  | ||||
|       if (isSavedLoginInfo) { | ||||
| @@ -114,9 +124,14 @@ class AuthenticationNotifier extends StateNotifier<AuthenticationState> { | ||||
|     state = AuthenticationState( | ||||
|       deviceId: "", | ||||
|       deviceType: "", | ||||
|       isAuthenticated: false, | ||||
|       userId: "", | ||||
|       userEmail: "", | ||||
|       firstName: '', | ||||
|       lastName: '', | ||||
|       profileImagePath: '', | ||||
|       isFirstLogin: false, | ||||
|       isAuthenticated: false, | ||||
|       isAdmin: false, | ||||
|       deviceInfo: DeviceInfoRemote( | ||||
|         id: 0, | ||||
|         userId: "", | ||||
| @@ -139,6 +154,10 @@ class AuthenticationNotifier extends StateNotifier<AuthenticationState> { | ||||
|     DeviceInfoRemote deviceInfoRemote = await _backupService.setAutoBackup(backupState, deviceId, deviceType); | ||||
|     state = state.copyWith(deviceInfo: deviceInfoRemote); | ||||
|   } | ||||
|  | ||||
|   updateUserProfileImagePath(String path) { | ||||
|     state = state.copyWith(profileImagePath: path); | ||||
|   } | ||||
| } | ||||
|  | ||||
| final authenticationProvider = StateNotifierProvider<AuthenticationNotifier, AuthenticationState>((ref) { | ||||
|   | ||||
| @@ -0,0 +1,53 @@ | ||||
| import 'dart:convert'; | ||||
|  | ||||
| class UploadProfileImageResponse { | ||||
|   final String userId; | ||||
|   final String profileImagePath; | ||||
|   UploadProfileImageResponse({ | ||||
|     required this.userId, | ||||
|     required this.profileImagePath, | ||||
|   }); | ||||
|  | ||||
|   UploadProfileImageResponse copyWith({ | ||||
|     String? userId, | ||||
|     String? profileImagePath, | ||||
|   }) { | ||||
|     return UploadProfileImageResponse( | ||||
|       userId: userId ?? this.userId, | ||||
|       profileImagePath: profileImagePath ?? this.profileImagePath, | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   Map<String, dynamic> toMap() { | ||||
|     final result = <String, dynamic>{}; | ||||
|  | ||||
|     result.addAll({'userId': userId}); | ||||
|     result.addAll({'profileImagePath': profileImagePath}); | ||||
|  | ||||
|     return result; | ||||
|   } | ||||
|  | ||||
|   factory UploadProfileImageResponse.fromMap(Map<String, dynamic> map) { | ||||
|     return UploadProfileImageResponse( | ||||
|       userId: map['userId'] ?? '', | ||||
|       profileImagePath: map['profileImagePath'] ?? '', | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   String toJson() => json.encode(toMap()); | ||||
|  | ||||
|   factory UploadProfileImageResponse.fromJson(String source) => UploadProfileImageResponse.fromMap(json.decode(source)); | ||||
|  | ||||
|   @override | ||||
|   String toString() => 'UploadProfileImageReponse(userId: $userId, profileImagePath: $profileImagePath)'; | ||||
|  | ||||
|   @override | ||||
|   bool operator ==(Object other) { | ||||
|     if (identical(this, other)) return true; | ||||
|  | ||||
|     return other is UploadProfileImageResponse && other.userId == userId && other.profileImagePath == profileImagePath; | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   int get hashCode => userId.hashCode ^ profileImagePath.hashCode; | ||||
| } | ||||
| @@ -2,8 +2,15 @@ import 'dart:convert'; | ||||
|  | ||||
| import 'package:dio/dio.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:hive/hive.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_info.model.dart'; | ||||
| import 'package:immich_mobile/shared/services/network.service.dart'; | ||||
| import 'package:immich_mobile/utils/dio_http_interceptor.dart'; | ||||
| import 'package:immich_mobile/utils/files_helper.dart'; | ||||
| import 'package:http_parser/http_parser.dart'; | ||||
|  | ||||
| class UserService { | ||||
|   final NetworkService _networkService = NetworkService(); | ||||
| @@ -21,4 +28,39 @@ class UserService { | ||||
|  | ||||
|     return []; | ||||
|   } | ||||
|  | ||||
|   Future<UploadProfileImageResponse?> uploadProfileImage(XFile image) async { | ||||
|     var dio = Dio(); | ||||
|     dio.interceptors.add(AuthenticatedRequestInterceptor()); | ||||
|     String savedEndpoint = Hive.box(userInfoBox).get(serverEndpointKey); | ||||
|     var mimeType = FileHelper.getMimeType(image.path); | ||||
|  | ||||
|     final imageData = MultipartFile.fromBytes( | ||||
|       await image.readAsBytes(), | ||||
|       filename: image.name, | ||||
|       contentType: MediaType( | ||||
|         mimeType["type"], | ||||
|         mimeType["subType"], | ||||
|       ), | ||||
|     ); | ||||
|  | ||||
|     final formData = FormData.fromMap({'file': imageData}); | ||||
|  | ||||
|     try { | ||||
|       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"); | ||||
|       return null; | ||||
|     } | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -42,14 +42,14 @@ packages: | ||||
|       name: auto_route | ||||
|       url: "https://pub.dartlang.org" | ||||
|     source: hosted | ||||
|     version: "3.2.4" | ||||
|     version: "4.0.1" | ||||
|   auto_route_generator: | ||||
|     dependency: "direct dev" | ||||
|     description: | ||||
|       name: auto_route_generator | ||||
|       url: "https://pub.dartlang.org" | ||||
|     source: hosted | ||||
|     version: "3.2.3" | ||||
|     version: "4.0.0" | ||||
|   badges: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
| @@ -126,7 +126,7 @@ packages: | ||||
|       name: cached_network_image | ||||
|       url: "https://pub.dartlang.org" | ||||
|     source: hosted | ||||
|     version: "3.2.0" | ||||
|     version: "3.2.1" | ||||
|   cached_network_image_platform_interface: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -197,6 +197,13 @@ packages: | ||||
|       url: "https://pub.dartlang.org" | ||||
|     source: hosted | ||||
|     version: "3.0.1" | ||||
|   cross_file: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: cross_file | ||||
|       url: "https://pub.dartlang.org" | ||||
|     source: hosted | ||||
|     version: "0.3.3+1" | ||||
|   crypto: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -321,6 +328,13 @@ packages: | ||||
|       url: "https://pub.dartlang.org" | ||||
|     source: hosted | ||||
|     version: "0.14.0" | ||||
|   flutter_plugin_android_lifecycle: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: flutter_plugin_android_lifecycle | ||||
|       url: "https://pub.dartlang.org" | ||||
|     source: hosted | ||||
|     version: "2.0.6" | ||||
|   flutter_riverpod: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -393,7 +407,7 @@ packages: | ||||
|       name: hive | ||||
|       url: "https://pub.dartlang.org" | ||||
|     source: hosted | ||||
|     version: "2.1.0" | ||||
|     version: "2.2.1" | ||||
|   hive_flutter: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
| @@ -450,6 +464,41 @@ packages: | ||||
|       url: "https://pub.dartlang.org" | ||||
|     source: hosted | ||||
|     version: "3.1.3" | ||||
|   image_picker: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
|       name: image_picker | ||||
|       url: "https://pub.dartlang.org" | ||||
|     source: hosted | ||||
|     version: "0.8.5+3" | ||||
|   image_picker_android: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: image_picker_android | ||||
|       url: "https://pub.dartlang.org" | ||||
|     source: hosted | ||||
|     version: "0.8.4+13" | ||||
|   image_picker_for_web: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: image_picker_for_web | ||||
|       url: "https://pub.dartlang.org" | ||||
|     source: hosted | ||||
|     version: "2.1.8" | ||||
|   image_picker_ios: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: image_picker_ios | ||||
|       url: "https://pub.dartlang.org" | ||||
|     source: hosted | ||||
|     version: "0.8.5+5" | ||||
|   image_picker_platform_interface: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: image_picker_platform_interface | ||||
|       url: "https://pub.dartlang.org" | ||||
|     source: hosted | ||||
|     version: "2.5.0" | ||||
|   intl: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
| @@ -673,7 +722,7 @@ packages: | ||||
|       name: percent_indicator | ||||
|       url: "https://pub.dartlang.org" | ||||
|     source: hosted | ||||
|     version: "3.4.0" | ||||
|     version: "4.2.2" | ||||
|   petitparser: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|   | ||||
| @@ -2,7 +2,7 @@ name: immich_mobile | ||||
| description: Immich - selfhosted backup media file on mobile phone | ||||
|  | ||||
| publish_to: "none" | ||||
| version: 1.9.1+14 | ||||
| version: 1.10.0+15 | ||||
|  | ||||
| environment: | ||||
|   sdk: ">=2.15.1 <3.0.0" | ||||
| @@ -14,13 +14,13 @@ dependencies: | ||||
|   photo_manager: ^2.0.6 | ||||
|   flutter_hooks: ^0.18.0 | ||||
|   hooks_riverpod: ^2.0.0-dev.0 | ||||
|   hive: | ||||
|   hive_flutter: | ||||
|   hive: ^2.2.1 | ||||
|   hive_flutter: ^1.1.0 | ||||
|   dio: ^4.0.4 | ||||
|   cached_network_image: ^3.2.0 | ||||
|   percent_indicator: ^3.4.0 | ||||
|   cached_network_image: ^3.2.1 | ||||
|   percent_indicator: ^4.2.2 | ||||
|   intl: ^0.17.0 | ||||
|   auto_route: ^3.2.2 | ||||
|   auto_route: ^4.0.1 | ||||
|   exif: ^3.1.1 | ||||
|   transparent_image: ^2.0.0 | ||||
|   visibility_detector: ^0.2.2 | ||||
| @@ -38,6 +38,7 @@ dependencies: | ||||
|   flutter_spinkit: ^5.1.0 | ||||
|   flutter_swipe_detector: ^2.0.0 | ||||
|   equatable: ^2.0.3 | ||||
|   image_picker: ^0.8.5+3 | ||||
|  | ||||
| dev_dependencies: | ||||
|   flutter_test: | ||||
| @@ -45,7 +46,7 @@ dev_dependencies: | ||||
|   flutter_lints: ^1.0.0 | ||||
|   hive_generator: ^1.1.2 | ||||
|   build_runner: ^2.1.7 | ||||
|   auto_route_generator: ^3.2.1 | ||||
|   auto_route_generator: ^4.0.0 | ||||
|  | ||||
| flutter: | ||||
|   uses-material-design: true | ||||
|   | ||||
| @@ -7,7 +7,7 @@ import { UpdateUserDto } from './dto/update-user.dto'; | ||||
| import { UserEntity } from './entities/user.entity'; | ||||
| import * as bcrypt from 'bcrypt'; | ||||
| import sharp from 'sharp'; | ||||
| import { createReadStream } from 'fs'; | ||||
| import { createReadStream, unlink, unlinkSync } from 'fs'; | ||||
| import { Response as Res } from 'express'; | ||||
|  | ||||
| @Injectable() | ||||
| @@ -129,25 +129,14 @@ export class UserService { | ||||
|  | ||||
|   async createProfileImage(authUser: AuthUserDto, fileInfo: Express.Multer.File) { | ||||
|     try { | ||||
|       // Convert file to jpeg | ||||
|       let filePath = '' | ||||
|       const convertImageInfo = await sharp(fileInfo.path).webp().resize(512, 512).toFile(fileInfo.path + '.webp') | ||||
|       await this.userRepository.update(authUser.id, { | ||||
|         profileImagePath: fileInfo.path | ||||
|       }) | ||||
|  | ||||
|       if (convertImageInfo) { | ||||
|         filePath = fileInfo.path + '.webp'; | ||||
|         await this.userRepository.update(authUser.id, { | ||||
|           profileImagePath: filePath | ||||
|         }) | ||||
|       } else { | ||||
|         filePath = fileInfo.path; | ||||
|         await this.userRepository.update(authUser.id, { | ||||
|           profileImagePath: filePath | ||||
|         }) | ||||
|       } | ||||
|  | ||||
|       return { | ||||
|         userId: authUser.id, | ||||
|         profileImagePath: filePath | ||||
|         profileImagePath: fileInfo.path | ||||
|       }; | ||||
|     } catch (e) { | ||||
|       Logger.error(e, 'Create User Profile Image'); | ||||
| @@ -156,10 +145,22 @@ export class UserService { | ||||
|   } | ||||
|  | ||||
|   async getUserProfileImage(userId: string, res: Res) { | ||||
|     const user = await this.userRepository.findOne({ id: userId }) | ||||
|     res.set({ | ||||
|       'Content-Type': 'image/webp', | ||||
|     }); | ||||
|     return new StreamableFile(createReadStream(user.profileImagePath)); | ||||
|     try { | ||||
|       const user = await this.userRepository.findOne({ id: userId }) | ||||
|       if (!user.profileImagePath) { | ||||
|         console.log("empty return") | ||||
|         throw new BadRequestException('User does not have a profile image'); | ||||
|       } | ||||
|  | ||||
|       res.set({ | ||||
|         'Content-Type': 'image/jpeg', | ||||
|       }); | ||||
|  | ||||
|       const fileStream = createReadStream(user.profileImagePath) | ||||
|       return new StreamableFile(fileStream); | ||||
|     } catch (e) { | ||||
|       console.log("error getting user profile") | ||||
|     } | ||||
|  | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -19,6 +19,7 @@ export const profileImageUploadOption: MulterOptions = { | ||||
|     destination: (req: Request, file: Express.Multer.File, cb: any) => { | ||||
|       const basePath = APP_UPLOAD_LOCATION; | ||||
|       const profileImageLocation = `${basePath}/${req.user['id']}/profile`; | ||||
|  | ||||
|       if (!existsSync(profileImageLocation)) { | ||||
|         mkdirSync(profileImageLocation, { recursive: true }); | ||||
|       } | ||||
| @@ -28,9 +29,10 @@ export const profileImageUploadOption: MulterOptions = { | ||||
|     }, | ||||
|  | ||||
|     filename: (req: Request, file: Express.Multer.File, cb: any) => { | ||||
|  | ||||
|       const userId = req.user['id']; | ||||
|  | ||||
|       cb(null, `${userId}`); | ||||
|       cb(null, `${userId}${extname(file.originalname)}`); | ||||
|     }, | ||||
|   }), | ||||
| }; | ||||
|   | ||||
| @@ -1,12 +1,20 @@ | ||||
| <script lang="ts"> | ||||
| 	import { page } from '$app/stores'; | ||||
| 	import type { ImmichUser } from '$lib/models/immich-user'; | ||||
| 	import { onMount } from 'svelte'; | ||||
| 	import { fade } from 'svelte/transition'; | ||||
| 	import { serverEndpoint } from '../../constants'; | ||||
|  | ||||
| 	export let user: ImmichUser; | ||||
|  | ||||
| 	let shouldShowAccountInfo = false; | ||||
| 	let shouldShowProfileImage = false; | ||||
|  | ||||
| 	onMount(async () => { | ||||
| 		const res = await fetch(`${serverEndpoint}/user/profile-image/${user.id}`); | ||||
|  | ||||
| 		if (res.status == 200) shouldShowProfileImage = true; | ||||
| 	}); | ||||
| 	const getFirstLetter = (text?: string) => { | ||||
| 		return text?.charAt(0).toUpperCase(); | ||||
| 	}; | ||||
| @@ -39,9 +47,17 @@ | ||||
| 				on:mouseleave={() => (shouldShowAccountInfo = false)} | ||||
| 			> | ||||
| 				<button | ||||
| 					class="flex place-items-center place-content-center rounded-full bg-immich-primary/80 h-10 w-10 text-gray-100 hover:bg-immich-primary" | ||||
| 					class="flex place-items-center place-content-center rounded-full bg-immich-primary/80 h-12 w-12 text-gray-100 hover:bg-immich-primary" | ||||
| 				> | ||||
| 					{getFirstLetter(user.firstName)}{getFirstLetter(user.lastName)} | ||||
| 					{#if shouldShowProfileImage} | ||||
| 						<img | ||||
| 							src={`${serverEndpoint}/user/profile-image/${user.id}`} | ||||
| 							alt="profile-img" | ||||
| 							class="inline rounded-full h-12 w-12 object-cover shadow-md" | ||||
| 						/> | ||||
| 					{:else} | ||||
| 						{getFirstLetter(user.firstName)}{getFirstLetter(user.lastName)} | ||||
| 					{/if} | ||||
| 				</button> | ||||
|  | ||||
| 				{#if shouldShowAccountInfo} | ||||
|   | ||||
		Reference in New Issue
	
	Block a user