diff --git a/mobile/ios/Podfile.lock b/mobile/ios/Podfile.lock
index 443fa82d7e..906433adff 100644
--- a/mobile/ios/Podfile.lock
+++ b/mobile/ios/Podfile.lock
@@ -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
diff --git a/mobile/ios/Runner/Info.plist b/mobile/ios/Runner/Info.plist
index dd1c0b4840..902925c967 100644
--- a/mobile/ios/Runner/Info.plist
+++ b/mobile/ios/Runner/Info.plist
@@ -43,6 +43,12 @@
NSPhotoLibraryAddUsageDescription
We need to manage backup your photos album
+ NSCameraUsageDescription
+ We need to access the camera to let you take beautiful video using this app
+
+ NSMicrophoneUsageDescription
+ We need to access the microphone to let you take beautiful video using this app
+
UILaunchStoryboardName
LaunchScreen
UIMainStoryboardFile
@@ -68,7 +74,7 @@
ITSAppUsesNonExemptEncryption
- CADisableMinimumFrameDurationOnPhone
-
-
-
+ CADisableMinimumFrameDurationOnPhone
+
+
+
\ No newline at end of file
diff --git a/mobile/lib/main.dart b/mobile/lib/main.dart
index 69fdf359a5..9597930587 100644
--- a/mobile/lib/main.dart
+++ b/mobile/lib/main.dart
@@ -76,7 +76,7 @@ class _ImmichAppState extends ConsumerState with WidgetsBindingObserv
}
Future initApp() async {
- WidgetsBinding.instance?.addObserver(this);
+ WidgetsBinding.instance.addObserver(this);
}
@override
@@ -87,7 +87,7 @@ class _ImmichAppState extends ConsumerState with WidgetsBindingObserv
@override
void dispose() {
- WidgetsBinding.instance?.removeObserver(this);
+ WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
diff --git a/mobile/lib/modules/backup/models/hive_backup_albums.model.g.dart b/mobile/lib/modules/backup/models/hive_backup_albums.model.g.dart
index d64ce4e7ba..ada7306db9 100644
Binary files a/mobile/lib/modules/backup/models/hive_backup_albums.model.g.dart and b/mobile/lib/modules/backup/models/hive_backup_albums.model.g.dart differ
diff --git a/mobile/lib/modules/backup/views/backup_controller_page.dart b/mobile/lib/modules/backup/views/backup_controller_page.dart
index d195b81aa5..028273e0d4 100644
--- a/mobile/lib/modules/backup/views/backup_controller_page.dart
+++ b/mobile/lib/modules/backup/views/backup_controller_page.dart
@@ -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),
diff --git a/mobile/lib/modules/home/providers/upload_profile_image.provider.dart b/mobile/lib/modules/home/providers/upload_profile_image.provider.dart
new file mode 100644
index 0000000000..d0f3103c56
--- /dev/null
+++ b/mobile/lib/modules/home/providers/upload_profile_image.provider.dart
@@ -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 toMap() {
+ final result = {};
+
+ result.addAll({'status': status.index});
+ result.addAll({'profileImagePath': profileImagePath});
+
+ return result;
+ }
+
+ factory UploadProfileImageState.fromMap(Map 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 {
+ UploadProfileImageNotifier()
+ : super(UploadProfileImageState(
+ profileImagePath: '',
+ status: UploadProfileStatus.idle,
+ ));
+
+ Future 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(((ref) => UploadProfileImageNotifier()));
diff --git a/mobile/lib/modules/home/ui/profile_drawer.dart b/mobile/lib/modules/home/ui/profile_drawer.dart
index 73af9d73aa..391529413c 100644
--- a/mobile/lib/modules/home/ui/profile_drawer.dart
+++ b/mobile/lib/modules/home/ui/profile_drawer.dart
@@ -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(
diff --git a/mobile/lib/modules/login/models/authentication_state.model.dart b/mobile/lib/modules/login/models/authentication_state.model.dart
index bc0c868e22..87459e8c33 100644
--- a/mobile/lib/modules/login/models/authentication_state.model.dart
+++ b/mobile/lib/modules/login/models/authentication_state.model.dart
@@ -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 toMap() {
- return {
- 'deviceId': deviceId,
- 'deviceType': deviceType,
- 'userId': userId,
- 'userEmail': userEmail,
- 'isAuthenticated': isAuthenticated,
- 'deviceInfo': deviceInfo.toMap(),
- };
+ final result = {};
+
+ 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 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;
}
}
diff --git a/mobile/lib/modules/login/models/login_response.model.dart b/mobile/lib/modules/login/models/login_response.model.dart
index 3c2032a842..0b4e833a82 100644
--- a/mobile/lib/modules/login/models/login_response.model.dart
+++ b/mobile/lib/modules/login/models/login_response.model.dart
@@ -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 toMap() {
- return {
- 'accessToken': accessToken,
- 'userId': userId,
- 'userEmail': userEmail,
- };
+ final result = {};
+
+ 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 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;
+ }
}
diff --git a/mobile/lib/modules/login/providers/authentication.provider.dart b/mobile/lib/modules/login/providers/authentication.provider.dart
index 9882a8659c..df475b36d5 100644
--- a/mobile/lib/modules/login/providers/authentication.provider.dart
+++ b/mobile/lib/modules/login/providers/authentication.provider.dart
@@ -17,9 +17,14 @@ class AuthenticationNotifier extends StateNotifier {
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 {
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 {
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 {
DeviceInfoRemote deviceInfoRemote = await _backupService.setAutoBackup(backupState, deviceId, deviceType);
state = state.copyWith(deviceInfo: deviceInfoRemote);
}
+
+ updateUserProfileImagePath(String path) {
+ state = state.copyWith(profileImagePath: path);
+ }
}
final authenticationProvider = StateNotifierProvider((ref) {
diff --git a/mobile/lib/shared/models/upload_profile_image_repsonse.model.dart b/mobile/lib/shared/models/upload_profile_image_repsonse.model.dart
new file mode 100644
index 0000000000..da4a2f6bf3
--- /dev/null
+++ b/mobile/lib/shared/models/upload_profile_image_repsonse.model.dart
@@ -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 toMap() {
+ final result = {};
+
+ result.addAll({'userId': userId});
+ result.addAll({'profileImagePath': profileImagePath});
+
+ return result;
+ }
+
+ factory UploadProfileImageResponse.fromMap(Map 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;
+}
diff --git a/mobile/lib/shared/services/user.service.dart b/mobile/lib/shared/services/user.service.dart
index cbc5f7d94a..ca259c5f0f 100644
--- a/mobile/lib/shared/services/user.service.dart
+++ b/mobile/lib/shared/services/user.service.dart
@@ -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 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;
+ }
+ }
}
diff --git a/mobile/pubspec.lock b/mobile/pubspec.lock
index 938bfbad57..f63caf2fc2 100644
--- a/mobile/pubspec.lock
+++ b/mobile/pubspec.lock
@@ -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:
diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml
index 268e84ce95..c0c840bbc8 100644
--- a/mobile/pubspec.yaml
+++ b/mobile/pubspec.yaml
@@ -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
diff --git a/server/src/api-v1/user/user.service.ts b/server/src/api-v1/user/user.service.ts
index 2cf0fac725..e747b521f0 100644
--- a/server/src/api-v1/user/user.service.ts
+++ b/server/src/api-v1/user/user.service.ts
@@ -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")
+ }
+
}
}
diff --git a/server/src/config/profile-image-upload.config.ts b/server/src/config/profile-image-upload.config.ts
index 7ce3e648b7..29d8be5d56 100644
--- a/server/src/config/profile-image-upload.config.ts
+++ b/server/src/config/profile-image-upload.config.ts
@@ -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)}`);
},
}),
};
diff --git a/web/src/lib/components/shared/navigation-bar.svelte b/web/src/lib/components/shared/navigation-bar.svelte
index 2bf8425330..8e3cf47798 100644
--- a/web/src/lib/components/shared/navigation-bar.svelte
+++ b/web/src/lib/components/shared/navigation-bar.svelte
@@ -1,12 +1,20 @@