diff --git a/devtools_options.yaml b/devtools_options.yaml new file mode 100644 index 00000000..2bc8e05f --- /dev/null +++ b/devtools_options.yaml @@ -0,0 +1,4 @@ +description: This file stores settings for Dart & Flutter DevTools. +documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states +extensions: + - provider: true \ No newline at end of file diff --git a/lib/core/classes/telware_toast.dart b/lib/core/classes/telware_toast.dart new file mode 100644 index 00000000..83837f3f --- /dev/null +++ b/lib/core/classes/telware_toast.dart @@ -0,0 +1,43 @@ +import 'package:flutter/material.dart'; +import 'package:fluttertoast/fluttertoast.dart'; + +class TelwareToast{ + static String? _message; + + static Future showToast({ + required String msg, + Toast? toastLength, + int timeInSecForIosWeb = 1, + double? fontSize, + String? fontAsset, + ToastGravity? gravity, + Color? backgroundColor, + Color? textColor, + bool webShowClose = false, + webBgColor = "linear-gradient(to right, #00b09b, #96c93d)", + webPosition = "right", + }) async { + _message = msg; + return await Fluttertoast.showToast( + msg: msg, + toastLength: toastLength, + timeInSecForIosWeb: timeInSecForIosWeb, + fontSize: fontSize, + fontAsset: fontAsset, + gravity: gravity, + backgroundColor: backgroundColor, + textColor: textColor, + webShowClose: webShowClose, + webBgColor: webBgColor, + webPosition: webPosition, + ); + } + + static Future cancel() async { + return await Fluttertoast.cancel(); + } + + static String? get message => _message; + + TelwareToast._(); +} \ No newline at end of file diff --git a/lib/core/constants/server_constants.dart b/lib/core/constants/server_constants.dart index 424f2584..35c59f41 100644 --- a/lib/core/constants/server_constants.dart +++ b/lib/core/constants/server_constants.dart @@ -6,5 +6,11 @@ import 'package:flutter_dotenv/flutter_dotenv.dart'; final String API_URL = dotenv.env['API_URL']!; final String GOOGLE_AUTH_URL = API_URL + dotenv.env['GOOGLE_AUTH_URL']!; final String GITHUB_AUTH_URL = API_URL + dotenv.env['GITHUB_AUTH_URL']!; -final BASE_OPTIONS = - BaseOptions(baseUrl: API_URL, contentType: 'application/json'); +final BASE_OPTIONS = BaseOptions( + baseUrl: API_URL, + contentType: 'application/json', + connectTimeout: const Duration(seconds: 5), // 5 seconds + receiveTimeout: const Duration(seconds: 3), // 3 seconds + sendTimeout: const Duration(seconds: 3), // 3 seconds +); + diff --git a/lib/core/mock/user_mock.dart b/lib/core/mock/user_mock.dart index 814f5772..ad023554 100644 --- a/lib/core/mock/user_mock.dart +++ b/lib/core/mock/user_mock.dart @@ -1,15 +1,15 @@ import 'package:telware_cross_platform/core/models/user_model.dart'; -const UserModel userMock = UserModel( +final UserModel userMock = UserModel( username: 'mock.user', screenName: 'Mocka Mocka', email: 'mock@gmail.com', - status: 'verified', + status: 'online', bio: 'I am a mocking user', maxFileSize: 30, automaticDownloadEnable: true, lastSeenPrivacy: 'recently', - readReceiptsEnablePrivacy: 'enable', + readReceiptsEnablePrivacy: true, storiesPrivacy: 'private', picturePrivacy: 'global', invitePermissionsPrivacy: 'enable', diff --git a/lib/core/models/user_model.dart b/lib/core/models/user_model.dart index 10665d2b..85275bb8 100644 --- a/lib/core/models/user_model.dart +++ b/lib/core/models/user_model.dart @@ -1,7 +1,9 @@ // ignore_for_file: public_member_api_docs, sort_constructors_first import 'dart:convert'; +import 'dart:typed_data'; import 'package:hive/hive.dart'; +import 'package:telware_cross_platform/features/stories/utils/utils_functions.dart'; part 'user_model.g.dart'; @@ -26,7 +28,7 @@ class UserModel { @HiveField(8) final String lastSeenPrivacy; @HiveField(9) - final String readReceiptsEnablePrivacy; + final bool readReceiptsEnablePrivacy; @HiveField(10) final String storiesPrivacy; @HiveField(11) @@ -35,8 +37,10 @@ class UserModel { final String invitePermissionsPrivacy; @HiveField(13) final String phone; + @HiveField(14) + Uint8List? photoBytes; - const UserModel({ + UserModel({ required this.username, required this.screenName, required this.email, @@ -51,45 +55,50 @@ class UserModel { required this.picturePrivacy, required this.invitePermissionsPrivacy, required this.phone, - }); + }) { + _setPhotoBytes(); + } + + _setPhotoBytes() async { + photoBytes = await downloadImage(photo); + } @override bool operator ==(covariant UserModel other) { if (identical(this, other)) return true; - - return - other.username == username && - other.screenName == screenName && - other.email == email && - other.photo == photo && - other.status == status && - other.bio == bio && - other.maxFileSize == maxFileSize && - other.automaticDownloadEnable == automaticDownloadEnable && - other.lastSeenPrivacy == lastSeenPrivacy && - other.readReceiptsEnablePrivacy == readReceiptsEnablePrivacy && - other.storiesPrivacy == storiesPrivacy && - other.picturePrivacy == picturePrivacy && - other.invitePermissionsPrivacy == invitePermissionsPrivacy && - other.phone == phone; + + return other.username == username && + other.screenName == screenName && + other.email == email && + other.photo == photo && + other.status == status && + other.bio == bio && + other.maxFileSize == maxFileSize && + other.automaticDownloadEnable == automaticDownloadEnable && + other.lastSeenPrivacy == lastSeenPrivacy && + other.readReceiptsEnablePrivacy == readReceiptsEnablePrivacy && + other.storiesPrivacy == storiesPrivacy && + other.picturePrivacy == picturePrivacy && + other.invitePermissionsPrivacy == invitePermissionsPrivacy && + other.phone == phone; } @override int get hashCode { return username.hashCode ^ - screenName.hashCode ^ - email.hashCode ^ - photo.hashCode ^ - status.hashCode ^ - bio.hashCode ^ - maxFileSize.hashCode ^ - automaticDownloadEnable.hashCode ^ - lastSeenPrivacy.hashCode ^ - readReceiptsEnablePrivacy.hashCode ^ - storiesPrivacy.hashCode ^ - picturePrivacy.hashCode ^ - invitePermissionsPrivacy.hashCode ^ - phone.hashCode; + screenName.hashCode ^ + email.hashCode ^ + photo.hashCode ^ + status.hashCode ^ + bio.hashCode ^ + maxFileSize.hashCode ^ + automaticDownloadEnable.hashCode ^ + lastSeenPrivacy.hashCode ^ + readReceiptsEnablePrivacy.hashCode ^ + storiesPrivacy.hashCode ^ + picturePrivacy.hashCode ^ + invitePermissionsPrivacy.hashCode ^ + phone.hashCode; } @override @@ -107,7 +116,7 @@ class UserModel { int? maxFileSize, bool? automaticDownloadEnable, String? lastSeenPrivacy, - String? readReceiptsEnablePrivacy, + bool? readReceiptsEnablePrivacy, String? storiesPrivacy, String? picturePrivacy, String? invitePermissionsPrivacy, @@ -121,12 +130,15 @@ class UserModel { status: status ?? this.status, bio: bio ?? this.bio, maxFileSize: maxFileSize ?? this.maxFileSize, - automaticDownloadEnable: automaticDownloadEnable ?? this.automaticDownloadEnable, + automaticDownloadEnable: + automaticDownloadEnable ?? this.automaticDownloadEnable, lastSeenPrivacy: lastSeenPrivacy ?? this.lastSeenPrivacy, - readReceiptsEnablePrivacy: readReceiptsEnablePrivacy ?? this.readReceiptsEnablePrivacy, + readReceiptsEnablePrivacy: + readReceiptsEnablePrivacy ?? this.readReceiptsEnablePrivacy, storiesPrivacy: storiesPrivacy ?? this.storiesPrivacy, picturePrivacy: picturePrivacy ?? this.picturePrivacy, - invitePermissionsPrivacy: invitePermissionsPrivacy ?? this.invitePermissionsPrivacy, + invitePermissionsPrivacy: + invitePermissionsPrivacy ?? this.invitePermissionsPrivacy, phone: phone ?? this.phone, ); } @@ -151,9 +163,13 @@ class UserModel { } factory UserModel.fromMap(Map map) { + String screenName = map['screenName'] as String; + if (screenName.isEmpty) { + screenName = 'No Name'; + } return UserModel( username: map['username'] as String, - screenName: map['screenName'] as String, + screenName: screenName, email: map['email'] as String, photo: map['photo'] != null ? map['photo'] as String : null, status: map['status'] as String, @@ -161,15 +177,16 @@ class UserModel { maxFileSize: map['maxFileSize'] as int, automaticDownloadEnable: map['automaticDownloadEnable'] as bool, lastSeenPrivacy: map['lastSeenPrivacy'] as String, - readReceiptsEnablePrivacy: map['readReceiptsEnablePrivacy'] as String, + readReceiptsEnablePrivacy: map['readReceiptsEnablePrivacy'] as bool, storiesPrivacy: map['storiesPrivacy'] as String, picturePrivacy: map['picturePrivacy'] as String, invitePermissionsPrivacy: map['invitePermessionsPrivacy'] as String, - phone: map['phone'] as String, + phone: map['phoneNumber'] as String, ); } String toJson() => json.encode(toMap()); - factory UserModel.fromJson(String source) => UserModel.fromMap(json.decode(source) as Map); + factory UserModel.fromJson(String source) => + UserModel.fromMap(json.decode(source) as Map); } diff --git a/lib/core/models/user_model.g.dart b/lib/core/models/user_model.g.dart index e6b74f86..19f77b68 100644 --- a/lib/core/models/user_model.g.dart +++ b/lib/core/models/user_model.g.dart @@ -26,18 +26,18 @@ class UserModelAdapter extends TypeAdapter { maxFileSize: fields[6] as int, automaticDownloadEnable: fields[7] as bool, lastSeenPrivacy: fields[8] as String, - readReceiptsEnablePrivacy: fields[9] as String, + readReceiptsEnablePrivacy: fields[9] as bool, storiesPrivacy: fields[10] as String, picturePrivacy: fields[11] as String, invitePermissionsPrivacy: fields[12] as String, phone: fields[13] as String, - ); + )..photoBytes = fields[14] as Uint8List?; } @override void write(BinaryWriter writer, UserModel obj) { writer - ..writeByte(14) + ..writeByte(15) ..writeByte(0) ..write(obj.username) ..writeByte(1) @@ -65,7 +65,9 @@ class UserModelAdapter extends TypeAdapter { ..writeByte(12) ..write(obj.invitePermissionsPrivacy) ..writeByte(13) - ..write(obj.phone); + ..write(obj.phone) + ..writeByte(14) + ..write(obj.photoBytes); } @override diff --git a/lib/core/routes/routes.dart b/lib/core/routes/routes.dart index 1a5235ce..c3ef310d 100644 --- a/lib/core/routes/routes.dart +++ b/lib/core/routes/routes.dart @@ -13,8 +13,8 @@ import 'package:telware_cross_platform/features/auth/view/screens/verification_s import 'package:telware_cross_platform/features/auth/view_model/auth_view_model.dart'; import 'package:telware_cross_platform/features/home/view/screens/home_screen.dart'; import 'package:telware_cross_platform/features/home/view/screens/inbox_screen.dart'; -import 'package:telware_cross_platform/features/stories/view/screens/add_my_story_screen.dart'; -import 'package:telware_cross_platform/features/stories/view/screens/show_taken_story_screen.dart'; +import 'package:telware_cross_platform/features/stories/view/screens/add_my_image_screen.dart'; +import 'package:telware_cross_platform/features/stories/view/screens/show_taken_image_screen.dart'; import 'package:telware_cross_platform/features/stories/view/screens/story_screen.dart'; import 'package:telware_cross_platform/features/user/view/screens/block_user.dart'; import 'package:telware_cross_platform/features/user/view/screens/blocked_users.dart'; @@ -34,8 +34,8 @@ class Routes { static const String verification = VerificationScreen.route; static const String socialAuthLoading = SocialAuthLoadingScreen.route; static const String inboxScreen = InboxScreen.route; - static const String addMyStory = AddMyStoryScreen.route; - static const String showTakenStory = ShowTakenStoryScreen.route; + static const String addMyStory = AddMyImageScreen.route; + static const String showTakenStory = ShowTakenImageScreen.route; static const String storyScreen = StoryScreen.route; static const String devicesScreen = DevicesScreen.route; static const String settings = SettingsScreen.route; @@ -110,7 +110,7 @@ class Routes { path: Routes.addMyStory, pageBuilder: (context, state) => CustomTransitionPage( key: state.pageKey, - child: const AddMyStoryScreen(), + child: const AddMyImageScreen(), transitionsBuilder: _slideRightTransitionBuilder, ), ), @@ -118,7 +118,7 @@ class Routes { path: Routes.showTakenStory, pageBuilder: (context, state) => CustomTransitionPage( key: state.pageKey, - child: ShowTakenStoryScreen(image: state.extra as File), + child: ShowTakenImageScreen(image: state.extra as File), transitionsBuilder: _slideRightTransitionBuilder, ), ), diff --git a/lib/core/utils.dart b/lib/core/utils.dart index 4e6e89d7..b8e2fa85 100644 --- a/lib/core/utils.dart +++ b/lib/core/utils.dart @@ -1,7 +1,7 @@ // common utility functions are added here import 'dart:math'; import 'package:flutter/material.dart'; -import 'package:fluttertoast/fluttertoast.dart'; +import 'package:telware_cross_platform/core/classes/telware_toast.dart'; String? emailValidator(String? value) { const String emailPattern = @@ -29,15 +29,6 @@ String? passwordValidator(String? value) { return null; } -String? passwordValidatorLogIn(String? value) { - if (value == null || value.isEmpty) { - return null; - } else if (value.length < 8) { - return 'Password must be at least 8 characters long'; - } - return null; -} - // todo(ahmed): update this function to handle more cases String? confirmPasswordValidation(String? password, String? confirmedPassword) { if (password!.isEmpty || confirmedPassword!.isEmpty) return null; @@ -62,8 +53,8 @@ void showSnackBarMessage(BuildContext context, String message) { } void showToastMessage(String message) async { - await Fluttertoast.cancel(); - Fluttertoast.showToast(msg: message); + await TelwareToast.cancel(); + TelwareToast.showToast(msg: message); } String formatPhoneNumber(String phoneNumber) { @@ -102,3 +93,18 @@ String toKebabCase(String input) { // Remove leading or trailing hyphens return kebabCased.replaceAll(RegExp(r'^-+|-+$'), ''); } + +String getInitials(String name) { + if (name.isEmpty) { + return "NN"; + } + List nameParts = name.split(' '); + String initials = ""; + if (nameParts.isNotEmpty) { + initials = nameParts[0][0]; + if (nameParts.length > 1) { + initials += nameParts[1][0]; + } + } + return initials.toUpperCase(); +} diff --git a/lib/features/auth/models/auth_response_model.dart b/lib/features/auth/models/auth_response_model.dart index 3b6c490d..c1c11d3c 100644 --- a/lib/features/auth/models/auth_response_model.dart +++ b/lib/features/auth/models/auth_response_model.dart @@ -10,6 +10,11 @@ class AuthResponseModel { }); factory AuthResponseModel.fromMap(Map map) { + map.forEach( + (key, value) { + print('key: $key, value: $value, value type: ${value.runtimeType}'); + }, + ); return AuthResponseModel( user: UserModel.fromMap(map['user'] as Map), token: map['sessionId'] as String, diff --git a/lib/features/auth/repository/auth_remote_repository.dart b/lib/features/auth/repository/auth_remote_repository.dart index 356dd305..806ed6eb 100644 --- a/lib/features/auth/repository/auth_remote_repository.dart +++ b/lib/features/auth/repository/auth_remote_repository.dart @@ -1,5 +1,3 @@ -import 'dart:io'; - import 'package:fpdart/fpdart.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:dio/dio.dart'; @@ -107,15 +105,17 @@ class AuthRemoteRepository { final response = await _dio.get( '/users/me', options: Options( - headers: {HttpHeaders.authorizationHeader: 'Bearer $sessionId'}, + headers: {'X-Session-Token': sessionId}, ), ); - if (response.statusCode! > 200 || response.statusCode! < 200) { + if (response.statusCode! >= 300 || response.statusCode! < 200) { final message = response.data['message']; return Left(AppError(message)); } + debugPrint('========================================='); + debugPrint('Get me was successful'); final user = UserModel.fromMap(response.data['data']['user']); return Right(user); } on DioException catch (dioException) { @@ -140,7 +140,7 @@ class AuthRemoteRepository { }, ); - if (response.statusCode! > 200 || response.statusCode! < 200) { + if (response.statusCode! >= 300 || response.statusCode! < 200) { final String message = response.data?['message'] ?? 'Unexpected Error'; if (response.statusCode == 403) { return Left(AppError(message, code: 403)); @@ -167,11 +167,11 @@ class AuthRemoteRepository { final response = await _dio.post( route, options: Options( - headers: {HttpHeaders.authorizationHeader: 'Bearer $token'}, + headers: {'X-Session-Token': token}, ), ); - if (response.statusCode! > 200 || response.statusCode! < 200) { + if (response.statusCode! >= 300 || response.statusCode! < 200) { final message = response.data['message']; return AppError(message); } @@ -186,10 +186,12 @@ class AuthRemoteRepository { Future forgotPassword(String email) async { try { + debugPrint('email: $email'); final response = - await _dio.post('/auth/forgot-password', data: {email: email}); + await _dio.post('/auth/password/forget', data: {'email': email}); - if (response.statusCode! > 200 || response.statusCode! < 200) { + debugPrint('Forgot Password response: ${response.data ?? 'No data'}'); + if (response.statusCode! >= 300 || response.statusCode! < 200) { final String message = response.data?['message'] ?? 'Unexpected Error'; return AppError(message); } @@ -210,7 +212,7 @@ class AuthRemoteRepository { String? message; if (dioException.response != null) { message = - dioException.response!.data?['message'] ?? 'Unexpected dio Error'; + dioException.response!.data?['message'] ?? 'Unexpected server Error'; debugPrint(message); } else if (dioException.type == DioExceptionType.connectionTimeout || dioException.type == DioExceptionType.connectionError || diff --git a/lib/features/auth/view/screens/log_in_screen.dart b/lib/features/auth/view/screens/log_in_screen.dart index 42eedc9d..a6232aeb 100644 --- a/lib/features/auth/view/screens/log_in_screen.dart +++ b/lib/features/auth/view/screens/log_in_screen.dart @@ -184,7 +184,6 @@ class _LogInScreenState extends ConsumerState { right: Dimensions.inputPaddingLeft, ), obscure: true, - validator: passwordValidatorLogIn, visibilityKey: const Key('login-password-visibility'), ), _forgetPasswordButton(), @@ -193,7 +192,8 @@ class _LogInScreenState extends ConsumerState { mainAxisAlignment: MainAxisAlignment.center, children: [ const TitleElement( - name: 'Don\'t have an account? ', + padding: EdgeInsets.only(right: 5), + name: 'Don\'t have an account?', color: Palette.primaryText, fontSize: Sizes.infoText, ), diff --git a/lib/features/auth/view/screens/sign_up_screen.dart b/lib/features/auth/view/screens/sign_up_screen.dart index 3088b99b..ea784b6e 100644 --- a/lib/features/auth/view/screens/sign_up_screen.dart +++ b/lib/features/auth/view/screens/sign_up_screen.dart @@ -5,6 +5,7 @@ import 'package:flutter_shakemywidget/flutter_shakemywidget.dart'; import 'package:go_router/go_router.dart'; import 'package:phone_form_field/phone_form_field.dart'; import 'package:telware_cross_platform/core/models/signup_result.dart'; +import 'package:telware_cross_platform/features/auth/view/widget/confirmation_dialog.dart'; import 'package:vibration/vibration.dart'; import 'package:webview_flutter_plus/webview_flutter_plus.dart'; @@ -17,7 +18,6 @@ import 'package:telware_cross_platform/core/view/widget/responsive.dart'; import 'package:telware_cross_platform/features/auth/view/widget/auth_floating_action_button.dart'; import 'package:telware_cross_platform/features/auth/view/widget/auth_phone_number.dart'; import 'package:telware_cross_platform/features/auth/view/widget/auth_sub_text_button.dart'; -import 'package:telware_cross_platform/features/auth/view/widget/confirmation_dialog.dart'; import 'package:telware_cross_platform/features/auth/view/widget/shake_my_auth_input.dart'; import 'package:telware_cross_platform/features/auth/view/widget/social_log_in.dart'; import 'package:telware_cross_platform/features/auth/view/widget/title_element.dart'; @@ -321,6 +321,8 @@ class _SignUpScreenState extends ConsumerState { mainAxisAlignment: MainAxisAlignment.center, children: [ const TitleElement( + padding: EdgeInsets.only(right: 5), + name: 'Already have an account?', color: Palette.primaryText, fontSize: Sizes.infoText), diff --git a/lib/features/auth/view_model/auth_view_model.dart b/lib/features/auth/view_model/auth_view_model.dart index c3f19973..2318898d 100644 --- a/lib/features/auth/view_model/auth_view_model.dart +++ b/lib/features/auth/view_model/auth_view_model.dart @@ -36,7 +36,7 @@ class AuthViewModel extends _$AuthViewModel { } if (USE_MOCK_DATA) { - const user = userMock; + final user = userMock; ref.read(userProvider.notifier).update((_) => user); state = AuthState.authorized; return; @@ -249,16 +249,22 @@ class AuthViewModel extends _$AuthViewModel { await ref.read(authLocalRepositoryProvider).deleteUser(); ref.read(userProvider.notifier).update((_) => null); - state = AuthState.unauthenticated; + state = AuthState.unauthorized; return; - } + } final token = ref.read(tokenProvider); + // started log out operation + debugPrint('==============================='); + debugPrint('log out operation started'); + debugPrint('token: $token'); final appError = await ref .read(authRemoteRepositoryProvider) - .logOut(token: token!, route: 'auth/logout'); - + .logOut(token: token!, route: '/auth/logout'); + debugPrint('==============================='); + debugPrint('log out operation ended'); + debugPrint('Error: ${appError?.error}'); await _handleLogOutState(appError); } diff --git a/lib/features/auth/view_model/auth_view_model.g.dart b/lib/features/auth/view_model/auth_view_model.g.dart index 51a56f15..b7ef53d5 100644 --- a/lib/features/auth/view_model/auth_view_model.g.dart +++ b/lib/features/auth/view_model/auth_view_model.g.dart @@ -6,7 +6,7 @@ part of 'auth_view_model.dart'; // RiverpodGenerator // ************************************************************************** -String _$authViewModelHash() => r'1e0200c69dc6030ca961853130fbf7f3b96e0823'; +String _$authViewModelHash() => r'56ce475d492ceec78ac7c980be3946fecefc8b92'; /// See also [AuthViewModel]. @ProviderFor(AuthViewModel) diff --git a/lib/features/home/view/widget/drawer.dart b/lib/features/home/view/widget/drawer.dart index 2f351368..6fdcb0e4 100644 --- a/lib/features/home/view/widget/drawer.dart +++ b/lib/features/home/view/widget/drawer.dart @@ -1,31 +1,34 @@ import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; +import 'package:telware_cross_platform/core/providers/user_provider.dart'; import 'package:telware_cross_platform/core/theme/palette.dart'; import 'package:telware_cross_platform/core/theme/sizes.dart'; import 'package:telware_cross_platform/core/utils.dart'; -import 'package:telware_cross_platform/features/user/view/screens/profile_info_screen.dart'; import 'package:telware_cross_platform/features/user/view/screens/settings_screen.dart'; import 'package:telware_cross_platform/features/user/view/screens/user_profile_screen.dart'; -class AppDrawer extends StatelessWidget { +class AppDrawer extends ConsumerWidget { const AppDrawer({super.key}); @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { return Drawer( elevation: 20, backgroundColor: Palette.background, child: ListView( padding: EdgeInsets.zero, children: [ - _header(context), + _header(context, ref), _drawerItems(context), ], ), ); } - Widget _header(BuildContext context) { + Widget _header(BuildContext context, WidgetRef ref) { + final user = ref.watch(userProvider); + final userImageBytes = user?.photoBytes; return Container( // margin: EdgeInsets.zero, padding: EdgeInsets.only( @@ -38,27 +41,36 @@ class AppDrawer extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.start, children: [ - const Column( + Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ CircleAvatar( minRadius: 34, - backgroundImage: NetworkImage( - 'https://animenew.com.br/wp-content/uploads/2022/03/Este-e-o-PRIMEIRO-ESBOCO-do-VISUAL-de-Kamado-Tanjiro-em-Demon-Slayer-jpg.webp', - // fit: BoxFit., - ), + backgroundImage: + userImageBytes != null ? MemoryImage(userImageBytes) : null, + backgroundColor: + userImageBytes == null ? Palette.primary : null, + child: userImageBytes == null + ? Text( + getInitials(user?.screenName ?? 'Moamen Hefny'), + style: const TextStyle( + fontWeight: FontWeight.w500, + color: Palette.primaryText, + ), + ) + : null, ), - SizedBox(height: 18), + const SizedBox(height: 18), // todo: take real user name and number Text( - 'Ahmed Aladdin', - style: TextStyle( + user?.screenName ?? 'Moamen Hefny', + style: const TextStyle( color: Palette.primaryText, fontWeight: FontWeight.bold), ), - SizedBox(height: 2), + const SizedBox(height: 2), Text( - '+20 011 00000001', - style: TextStyle( + user?.phone ?? '+20 110 5035588', + style: const TextStyle( color: Palette.accentText, fontSize: Sizes.infoText), ), ], @@ -68,7 +80,8 @@ class AppDrawer extends StatelessWidget { children: [ IconButton( onPressed: () {}, - icon: const Icon(Icons.light_mode_rounded, color: Palette.icons)) + icon: const Icon(Icons.light_mode_rounded, + color: Palette.icons)) ], ), ], @@ -80,13 +93,15 @@ class AppDrawer extends StatelessWidget { return Wrap( children: [ // todo(ahmed): add routes to the drawer items when available - _drawerItem(context, Icons.account_circle_outlined, 'My Profile', verticalPadding: 5, route: UserProfileScreen.route), + _drawerItem(context, Icons.account_circle_outlined, 'My Profile', + verticalPadding: 5, route: UserProfileScreen.route), const Divider(thickness: 0.3, color: Palette.black, height: 0), _drawerItem(context, Icons.people_alt_outlined, 'New Group'), _drawerItem(context, Icons.person_outline_rounded, 'Contacts'), _drawerItem(context, Icons.call_outlined, 'Calls'), _drawerItem(context, Icons.bookmark_outline_rounded, 'Saved Messages'), - _drawerItem(context, Icons.settings_outlined, 'Settings', route: SettingsScreen.route), + _drawerItem(context, Icons.settings_outlined, 'Settings', + route: SettingsScreen.route), const Divider(thickness: 0.3, color: Palette.black, height: 0), _drawerItem(context, Icons.person_add_outlined, 'Invite Friends'), _drawerItem(context, Icons.info_outlined, 'TelWare Features'), @@ -94,10 +109,12 @@ class AppDrawer extends StatelessWidget { ); } - Widget _drawerItem(BuildContext context, IconData icon, String title, {double verticalPadding = 0, String? route}) { + Widget _drawerItem(BuildContext context, IconData icon, String title, + {double verticalPadding = 0, String? route}) { return ListTile( // tileColor: Colors.red, - contentPadding: EdgeInsets.symmetric(vertical: verticalPadding, horizontal: 19), + contentPadding: + EdgeInsets.symmetric(vertical: verticalPadding, horizontal: 19), leading: Icon(icon, size: 28, color: Palette.accentText), title: Row( children: [ diff --git a/lib/features/stories/repository/contacts_remote_repository.dart b/lib/features/stories/repository/contacts_remote_repository.dart index 55570862..08ac5e43 100644 --- a/lib/features/stories/repository/contacts_remote_repository.dart +++ b/lib/features/stories/repository/contacts_remote_repository.dart @@ -22,139 +22,15 @@ class ContactsRemoteRepository { Future> fetchContactsFromBackend() async { await Future.delayed(const Duration(seconds: 2)); List users = [ - ContactModel( - userName: 'game of thrones', - userImageUrl: - 'https://st2.depositphotos.com/2703645/7304/v/450/depositphotos_73040253-stock-illustration-male-avatar-icon.jpg', - stories: [ - StoryModel( - storyId: 'idd11', - createdAt: DateTime(2024, 10, 21, 12, 0), - storyContentUrl: - 'https://raw.githubusercontent.com/Bishoywadea/hosted_images/refs/heads/main/1.jpg', - isSeen: false, - storyCaption: 'very good caption', - seenIds: ['id1', 'id2']), - StoryModel( - storyId: 'idd12', - createdAt: DateTime(2024, 10, 21, 12, 0), - storyContentUrl: - 'https://raw.githubusercontent.com/Bishoywadea/hosted_images/refs/heads/main/2.jpeg', - isSeen: false, - storyCaption: 'very good good good caption', - seenIds: ['id2'], - ), - StoryModel( - storyId: 'idd13', - createdAt: DateTime(2024, 10, 21, 12, 0), - storyContentUrl: - 'https://www.e3lam.com/images/large/2015/01/unnamed-14.jpg', - isSeen: false, - seenIds: ['id1', 'id2'], - ), - ], - userId: 'myUser', - ), - ContactModel( - stories: [ - StoryModel( - storyId: 'id11', - createdAt: DateTime(2024, 10, 21, 12, 0), - storyContentUrl: - 'https://raw.githubusercontent.com/Bishoywadea/hosted_images/refs/heads/main/1.jpg', - isSeen: false, - seenIds: [], - ), - StoryModel( - storyId: 'id12', - createdAt: DateTime(2024, 10, 21, 12, 0), - storyContentUrl: - 'https://raw.githubusercontent.com/Bishoywadea/hosted_images/refs/heads/main/2.jpeg', - isSeen: false, - storyCaption: 'very good good good caption', - seenIds: [], - ), - ], - userName: 'game of thrones', - userImageUrl: - 'https://raw.githubusercontent.com/Bishoywadea/hosted_images/refs/heads/main/2.jpeg', - userId: 'id1', - ), - ContactModel( - stories: [ - StoryModel( - storyId: 'id21', - createdAt: DateTime(2024, 10, 21, 12, 0), - storyContentUrl: - 'https://raw.githubusercontent.com/Bishoywadea/hosted_images/refs/heads/main/1.jpg', - isSeen: false, - seenIds: [], - ), - StoryModel( - storyId: 'id22', - createdAt: DateTime(2024, 10, 21, 12, 0), - storyContentUrl: - 'https://raw.githubusercontent.com/Bishoywadea/hosted_images/refs/heads/main/2.jpeg', - isSeen: false, - seenIds: [], - ), - StoryModel( - storyId: 'id23', - createdAt: DateTime(2024, 10, 21, 12, 0), - storyContentUrl: - 'https://raw.githubusercontent.com/Bishoywadea/hosted_images/refs/heads/main/1.jpg', - isSeen: false, - seenIds: [], - ), - ], - userName: 'rings of power', - userImageUrl: - 'https://raw.githubusercontent.com/Bishoywadea/hosted_images/refs/heads/main/1.jpg', - userId: 'id2', - ), - ContactModel( - stories: [ - StoryModel( - storyId: 'id31', - createdAt: DateTime(2024, 10, 21, 12, 0), - storyContentUrl: - 'https://raw.githubusercontent.com/Bishoywadea/hosted_images/refs/heads/main/1.jpg', - isSeen: false, - storyCaption: 'very good good good caption', - seenIds: [], - ), - StoryModel( - storyId: 'id32', - createdAt: DateTime(2024, 10, 21, 12, 0), - storyContentUrl: - 'https://raw.githubusercontent.com/Bishoywadea/hosted_images/refs/heads/main/2.jpeg', - isSeen: false, - seenIds: [], - ), - StoryModel( - storyId: 'id33', - createdAt: DateTime(2024, 10, 21, 12, 0), - storyContentUrl: - 'https://raw.githubusercontent.com/Bishoywadea/hosted_images/refs/heads/main/1.jpg', - isSeen: false, - seenIds: [], - ), - ], - userName: 'rings of power', - userImageUrl: - 'https://raw.githubusercontent.com/Bishoywadea/hosted_images/refs/heads/main/1.jpg', - userId: 'id3', - ), ]; return users; } Future postStory(File storyImage, String? caption) async { - print('fdsaib'); String uploadUrl = 'http://testing.telware.tech:3000/api/v1/users/stories'; var uri = Uri.parse(uploadUrl); var request = http.MultipartRequest('POST', uri); - request.headers['X-Session-Token'] = '410b860a-de14-4cbe-b5f2-7cff8518a2f7'; + request.headers['X-Session-Token'] = authLocalRepository.getToken() ?? ''; var multipartFile = await http.MultipartFile.fromPath( 'file', @@ -178,6 +54,29 @@ class ContactsRemoteRepository { } } + Future updateProfilePicture(File storyImage) async { + String uploadUrl = 'http://testing.telware.tech:3000/api/v1/users/picture'; + var uri = Uri.parse(uploadUrl); + var request = http.MultipartRequest('PATCH', uri); + request.headers['X-Session-Token'] = authLocalRepository.getToken() ?? '410b860a-de14-4cbe-b5f2-7cff8518a2f7'; + var multipartFile = await http.MultipartFile.fromPath( + 'file', + storyImage.path, + contentType: MediaType('image', 'jpeg'), + ); + request.files.add(multipartFile); + + try { + var response = await request.send(); + return response.statusCode == 201; + } catch (e) { + if (kDebugMode) { + print('Error occurred: $e'); + } + return false; + } + } + Future markStoryAsSeen(String storyId) async { String uploadUrl = 'http://testing.telware.tech:3000/api/v1/stories/:storyId/views'; // var uri = Uri.parse(uploadUrl); diff --git a/lib/features/stories/utils/utils_functions.dart b/lib/features/stories/utils/utils_functions.dart index 47973a12..125868d2 100644 --- a/lib/features/stories/utils/utils_functions.dart +++ b/lib/features/stories/utils/utils_functions.dart @@ -4,7 +4,10 @@ import 'package:flutter/foundation.dart'; import 'package:http/http.dart' as http; import 'package:http_parser/http_parser.dart'; -Future downloadImage(String url) async { +Future downloadImage(String? url) async { + if (url == null || url.isEmpty) { + return null; + } try { final response = await http.get(Uri.parse(url)); if (response.statusCode == 200) { diff --git a/lib/features/stories/view/screens/add_my_story_screen.dart b/lib/features/stories/view/screens/add_my_image_screen.dart similarity index 91% rename from lib/features/stories/view/screens/add_my_story_screen.dart rename to lib/features/stories/view/screens/add_my_image_screen.dart index 8e479100..fa94d16b 100644 --- a/lib/features/stories/view/screens/add_my_story_screen.dart +++ b/lib/features/stories/view/screens/add_my_image_screen.dart @@ -7,20 +7,21 @@ import 'package:flutter/services.dart'; import 'package:go_router/go_router.dart'; import 'package:telware_cross_platform/core/routes/routes.dart'; import 'package:path_provider/path_provider.dart'; -import 'package:telware_cross_platform/features/stories/view/screens/show_taken_story_screen.dart'; +import 'package:telware_cross_platform/features/stories/view/screens/show_taken_image_screen.dart'; import '../widget/take_photo_row.dart'; import '../widget/toggleCameraMode.dart'; -class AddMyStoryScreen extends StatefulWidget { - static const String route = '/add-my-story'; - - const AddMyStoryScreen({super.key}); +class AddMyImageScreen extends StatefulWidget { + static const String route = '/add-my-image'; + final String destination; + const AddMyImageScreen({super.key, this.destination = 'story',}); @override - _AddMyStoryScreenState createState() => _AddMyStoryScreenState(); + _AddMyImageScreenState createState() => _AddMyImageScreenState(); } -class _AddMyStoryScreenState extends State { +class _AddMyImageScreenState extends State { + CameraController? _controller; Future? _initializeControllerFuture; String _selectedMode = 'Photo'; @@ -101,7 +102,7 @@ class _AddMyStoryScreenState extends State { Navigator.push( context, MaterialPageRoute( - builder: (context) => ShowTakenStoryScreen(image: savedFile), + builder: (context) => ShowTakenImageScreen(image: savedFile, destination:widget.destination), ), ); } catch (e) { diff --git a/lib/features/stories/view/screens/show_taken_story_screen.dart b/lib/features/stories/view/screens/show_taken_image_screen.dart similarity index 93% rename from lib/features/stories/view/screens/show_taken_story_screen.dart rename to lib/features/stories/view/screens/show_taken_image_screen.dart index 433334aa..873b987b 100644 --- a/lib/features/stories/view/screens/show_taken_story_screen.dart +++ b/lib/features/stories/view/screens/show_taken_image_screen.dart @@ -16,17 +16,18 @@ import '../widget/bottom_action_buttons_edit_taken_image.dart'; import '../widget/signature_pen.dart'; import '../widget/story_caption_text_field.dart'; -class ShowTakenStoryScreen extends ConsumerStatefulWidget { +class ShowTakenImageScreen extends ConsumerStatefulWidget { static const String route = '/show-taken-story'; final File image; + final String destination; - const ShowTakenStoryScreen({super.key, required this.image}); + const ShowTakenImageScreen({super.key, required this.image, this.destination = 'story',}); @override _ShowTakenStoryScreenState createState() => _ShowTakenStoryScreenState(); } -class _ShowTakenStoryScreenState extends ConsumerState { +class _ShowTakenStoryScreenState extends ConsumerState { final GlobalKey _signatureBoundaryKey = GlobalKey(); File? _imageFile; File? _originalImageFile; @@ -155,13 +156,14 @@ class _ShowTakenStoryScreenState extends ConsumerState { child: Column( mainAxisSize: MainAxisSize.min, children: [ - StoryCaptionField(controller: _captionController), + widget.destination == 'story' ? StoryCaptionField(controller: _captionController) : const SizedBox(), BottomActionButtonsEditTakenImage( cropImage: _cropImage, discardChanges: _discardChanges, saveAndPostStory: _saveCombinedImage, captionController: _captionController, ref: ref, + destination: widget.destination, ), ], ), diff --git a/lib/features/stories/view/widget/bottom_action_buttons_edit_taken_image.dart b/lib/features/stories/view/widget/bottom_action_buttons_edit_taken_image.dart index 6c2690bc..4958c5bf 100644 --- a/lib/features/stories/view/widget/bottom_action_buttons_edit_taken_image.dart +++ b/lib/features/stories/view/widget/bottom_action_buttons_edit_taken_image.dart @@ -10,6 +10,7 @@ class BottomActionButtonsEditTakenImage extends StatelessWidget { final Future Function() saveAndPostStory; final TextEditingController captionController; final WidgetRef ref; + final String destination; const BottomActionButtonsEditTakenImage({ Key? key, @@ -18,6 +19,7 @@ class BottomActionButtonsEditTakenImage extends StatelessWidget { required this.saveAndPostStory, required this.captionController, required this.ref, + this.destination = 'story' }) : super(key: key); @override @@ -41,14 +43,33 @@ class BottomActionButtonsEditTakenImage extends StatelessWidget { FocusScope.of(context).unfocus(); File combinedImageFile = await saveAndPostStory(); String storyCaption = captionController.text; - final contactViewModel = ref.read(usersViewModelProvider.notifier); - bool uploadResult = await contactViewModel.postStory(combinedImageFile, storyCaption); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(uploadResult ? 'Story Posted Successfully' : 'Failed to post Story'), - ), - ); - + bool uploadResult = false; + if(destination == 'story') { + final contactViewModel = ref.read( + usersViewModelProvider.notifier); + uploadResult = await contactViewModel.postStory( + combinedImageFile, storyCaption); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(uploadResult + ? 'Story Posted Successfully' + : 'Failed to post Story'), + ), + ); + } + else{ + final contactViewModel = ref.read( + usersViewModelProvider.notifier); + uploadResult = await contactViewModel.updateProfilePicture( + combinedImageFile); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(uploadResult + ? 'Profile Picture updated' + : 'Failed to post update profile picture'), + ), + ); + } if (uploadResult) { Future.delayed(const Duration(seconds: 2), () { Navigator.of(context).pop(); diff --git a/lib/features/stories/view_model/contact_view_model.dart b/lib/features/stories/view_model/contact_view_model.dart index 70fbe7b3..69381cc0 100644 --- a/lib/features/stories/view_model/contact_view_model.dart +++ b/lib/features/stories/view_model/contact_view_model.dart @@ -150,6 +150,10 @@ class ContactViewModel extends StateNotifier { return _contactsRemoteRepository.postStory(storyImage, storyCaption); } + Future updateProfilePicture(File storyImage) async { + return _contactsRemoteRepository.updateProfilePicture(storyImage); + } + Future deleteStory(String storyId) async { return _contactsRemoteRepository.deleteStory(storyId); } diff --git a/lib/features/user/view/screens/settings_screen.dart b/lib/features/user/view/screens/settings_screen.dart index 80d5f3f3..0ec70abd 100644 --- a/lib/features/user/view/screens/settings_screen.dart +++ b/lib/features/user/view/screens/settings_screen.dart @@ -5,11 +5,15 @@ import 'package:go_router/go_router.dart'; import 'package:telware_cross_platform/core/routes/routes.dart'; import 'package:telware_cross_platform/core/theme/dimensions.dart'; import 'package:telware_cross_platform/core/theme/palette.dart'; +import 'package:telware_cross_platform/core/utils.dart'; +import 'package:telware_cross_platform/features/auth/view_model/auth_state.dart'; import 'package:telware_cross_platform/features/auth/view_model/auth_view_model.dart'; import 'package:telware_cross_platform/features/user/view/widget/profile_header_widget.dart'; import 'package:telware_cross_platform/features/user/view/widget/settings_option_widget.dart'; import 'package:telware_cross_platform/features/user/view/widget/settings_section.dart'; +import '../../../stories/view/screens/add_my_image_screen.dart'; + class SettingsScreen extends ConsumerStatefulWidget { static const String route = '/settings'; @@ -135,84 +139,133 @@ class _SettingsScreen extends ConsumerState { @override Widget build(BuildContext context) { + ref.listen(authViewModelProvider, (_, state) { + if (state.type == AuthStateType.fail) { + showToastMessage(state.message!); + } else if (state.type == AuthStateType.unauthorized) { + context.push(Routes.home); + } + }); + + bool isLoading = + ref.watch(authViewModelProvider).type == AuthStateType.loading; + debugPrint('isLoading: $isLoading'); + return Scaffold( - body: CustomScrollView( - slivers: [ - SliverAppBar( - expandedHeight: 145.0, - toolbarHeight: 80, - floating: false, - pinned: true, - leading: IconButton( - icon: const Icon(Icons.arrow_back), - onPressed: () => context.pop()), - actions: [ - const Icon(Icons.search), - const SizedBox(width: 16), - IconButton(onPressed: () { - ref.read(authViewModelProvider.notifier).logOut(); - context.go(Routes.logIn); - }, icon: const Icon(Icons.more_vert)), - ], - flexibleSpace: LayoutBuilder( - builder: (context, constraints) { - double factor = _calculateFactor(constraints); - return FlexibleSpaceBar( - title: ProfileHeader(fullName: fullName, factor: factor), - centerTitle: true, - background: Container( - alignment: Alignment.topLeft, - color: Palette.trinary, - padding: EdgeInsets.zero, + body: isLoading + ? const Center(child: CircularProgressIndicator.adaptive()) + : CustomScrollView( + slivers: [ + SliverAppBar( + expandedHeight: 145.0, + toolbarHeight: 80, + floating: false, + pinned: true, + leading: IconButton( + icon: const Icon(Icons.arrow_back), + onPressed: () => context.pop()), + actions: [ + const Icon(Icons.search), + const SizedBox(width: 16), + IconButton( + onPressed: () { + ref.read(authViewModelProvider.notifier).logOut(); + }, + icon: const Icon(Icons.more_vert)), + ], + flexibleSpace: LayoutBuilder( + builder: (context, constraints) { + double factor = _calculateFactor(constraints); + return FlexibleSpaceBar( + title: ProfileHeader(factor: factor), + centerTitle: true, + background: Container( + alignment: Alignment.topLeft, + color: Palette.trinary, + padding: EdgeInsets.zero, + ), + ); + }, ), - ); - }, - ), - ), - const SliverToBoxAdapter( - child: Column( - children: [ - SettingsSection( - settingsOptions: [], - actions: [ - SettingsOptionWidget( - key: ValueKey("set-profile-photo-option"), - icon: Icons.camera_alt_outlined, - iconColor: Palette.primary, - text: "Set Profile Photo", - color: Palette.primary, - showDivider: false, - ) - ], - ), - ], - )), - SliverList( - delegate: SliverChildBuilderDelegate( - (context, index) { - final section = profileSections[index]; - final title = section["title"] ?? ""; - final options = section["options"]; - final trailing = section["trailing"] ?? ""; - return Column( + ), + SliverToBoxAdapter( + child: Column( children: [ - const SizedBox(height: Dimensions.sectionGaps), SettingsSection( - title: title, - settingsOptions: options, - trailing: trailing, + settingsOptions: const [], + actions: [ + GestureDetector( + onTap: () { + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => const AddMyImageScreen( + destination: 'profile'), + ), + ); + }, + child: const SettingsOptionWidget( + key: ValueKey("set-profile-photo-option"), + icon: Icons.camera_alt_outlined, + iconColor: Palette.primary, + text: "Set Profile Photo", + color: Palette.primary, + showDivider: false, + ), + ) + ], ), ], - ); - }, - childCount: profileSections.length, + )), + SliverList( + delegate: SliverChildBuilderDelegate((context, index) { + final section = profileSections[index]; + final title = section["title"] ?? ""; + final options = section["options"]; + final trailing = section["trailing"] ?? ""; + return const Column( + children: [ + SettingsSection( + settingsOptions: [], + actions: [ + SettingsOptionWidget( + key: ValueKey("set-profile-photo-option"), + icon: Icons.camera_alt_outlined, + iconColor: Palette.primary, + text: "Set Profile Photo", + color: Palette.primary, + showDivider: false, + ) + ], + ), + ], + ); + })), + SliverList( + delegate: SliverChildBuilderDelegate( + (context, index) { + final section = profileSections[index]; + final title = section["title"] ?? ""; + final options = section["options"]; + final trailing = section["trailing"] ?? ""; + return Column( + children: [ + const SizedBox(height: Dimensions.sectionGaps), + SettingsSection( + title: title, + settingsOptions: options, + trailing: trailing, + ), + ], + ); + }, + childCount: profileSections.length, + ), + ), + const SliverToBoxAdapter( + child: SizedBox(height: Dimensions.sectionGaps), + ), + ], ), - ), - const SliverToBoxAdapter( - child: SizedBox(height: Dimensions.sectionGaps), - ), - ], - ), ); } diff --git a/lib/features/user/view/screens/user_profile_screen.dart b/lib/features/user/view/screens/user_profile_screen.dart index d7f21327..fd73e675 100644 --- a/lib/features/user/view/screens/user_profile_screen.dart +++ b/lib/features/user/view/screens/user_profile_screen.dart @@ -3,6 +3,7 @@ import 'package:go_router/go_router.dart'; import 'package:telware_cross_platform/core/routes/routes.dart'; import 'package:telware_cross_platform/core/theme/dimensions.dart'; import 'package:telware_cross_platform/core/theme/palette.dart'; +import 'package:telware_cross_platform/features/stories/view/screens/add_my_image_screen.dart'; import 'package:telware_cross_platform/features/user/view/widget/profile_header_widget.dart'; import 'package:telware_cross_platform/features/user/view/widget/settings_option_widget.dart'; import 'package:telware_cross_platform/features/user/view/widget/settings_section.dart'; @@ -17,8 +18,6 @@ class UserProfileScreen extends StatefulWidget { } class _UserProfileScreen extends State { - static const String fullName = "Moamen Hefny"; - static var user = { "phoneNumber": "+20 110 5035588", "username": "Moamen", @@ -28,67 +27,92 @@ class _UserProfileScreen extends State { @override Widget build(BuildContext context) { return Scaffold( - body: CustomScrollView( - slivers: [ - SliverAppBar( - expandedHeight: 145.0, - toolbarHeight: 80, - floating: false, - pinned: true, - leading: IconButton( - icon: const Icon(Icons.arrow_back), - onPressed: () => context.pop(), - ), - actions: [ - IconButton(icon: const Icon(Icons.edit), - onPressed: () => context.push(Routes.profileInfo)), - const SizedBox(width: 16), - const Icon(Icons.more_vert), - ], - flexibleSpace: LayoutBuilder( - builder: (context, constraints) { - double factor = _calculateFactor(constraints); - return FlexibleSpaceBar( - title: ProfileHeader(fullName: fullName, factor: factor), - centerTitle: true, - background: Container( - alignment: Alignment.topLeft, - color: Palette.trinary, - padding: EdgeInsets.zero, + body: Stack( + children: [ + CustomScrollView( + slivers: [ + SliverAppBar( + expandedHeight: 145.0, + toolbarHeight: 80, + floating: false, + pinned: true, + leading: IconButton( + icon: const Icon(Icons.arrow_back), + onPressed: () => context.pop(), + ), + actions: [ + IconButton( + icon: const Icon(Icons.edit), + onPressed: () => context.push(Routes.profileInfo), ), - ); - }, - ), - ), - SliverToBoxAdapter( - child: Column( - children: [ - SettingsSection( - title: "Info", - settingsOptions: const [], - actions: [ - SettingsOptionWidget(text: user["phoneNumber"] ?? "", - icon: null, - subtext: "Mobile", + const SizedBox(width: 16), + const Icon(Icons.more_vert), + ], + flexibleSpace: LayoutBuilder( + builder: (context, constraints) { + double factor = _calculateFactor(constraints); + return FlexibleSpaceBar( + title: ProfileHeader(factor: factor), + centerTitle: true, + background: Container( + alignment: Alignment.topLeft, + color: Palette.trinary, + padding: EdgeInsets.zero, ), - if (user["bio"] != null) - SettingsOptionWidget(icon: null, - text: user["bio"] ?? "", - subtext: "Bio" + ); + }, + ), + ), + SliverToBoxAdapter( + child: Column( + children: [ + SettingsSection( + title: "Info", + settingsOptions: const [], + actions: [ + SettingsOptionWidget( + text: user["phoneNumber"] ?? "", + icon: null, + subtext: "Mobile", ), - if (user["username"] != null) - SettingsOptionWidget(icon: null, + if (user["bio"] != null) + SettingsOptionWidget( + icon: null, + text: user["bio"] ?? "", + subtext: "Bio", + ), + if (user["username"] != null) + SettingsOptionWidget( + icon: null, text: "@${user["username"]}", - subtext: "Username" - ), - ], - ), - ], + subtext: "Username", + ), + ], + ), + ], + ), ), + const SliverToBoxAdapter( + child: SizedBox(height: Dimensions.sectionGaps), + ), + ], ), - - const SliverToBoxAdapter( - child: SizedBox(height: Dimensions.sectionGaps), + Positioned( + top: 140, + right: 16.0, + child: FloatingActionButton( + onPressed: () { + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => + const AddMyImageScreen(destination: 'profile'), + ), + ); + }, + shape: const CircleBorder(), + backgroundColor: Palette.primary, + child: const Icon(Icons.add), + ), ), ], ), @@ -98,7 +122,8 @@ class _UserProfileScreen extends State { double _calculateFactor(BoxConstraints constraints) { double maxExtent = 130.0; double scrollOffset = constraints.maxHeight - kToolbarHeight; - double factor = scrollOffset > 0 ? (maxExtent - scrollOffset) / maxExtent * 90.0 : 60.0; + double factor = + scrollOffset > 0 ? (maxExtent - scrollOffset) / maxExtent * 90.0 : 60.0; return factor.clamp(0, 90.0); } } diff --git a/lib/features/user/view/widget/profile_header_widget.dart b/lib/features/user/view/widget/profile_header_widget.dart index 312d8841..3c106e88 100644 --- a/lib/features/user/view/widget/profile_header_widget.dart +++ b/lib/features/user/view/widget/profile_header_widget.dart @@ -1,28 +1,19 @@ import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:telware_cross_platform/core/providers/user_provider.dart'; import 'package:telware_cross_platform/core/theme/palette.dart'; +import 'package:telware_cross_platform/core/utils.dart'; -class ProfileHeader extends StatelessWidget { - final String fullName; - final String? imagePath; +class ProfileHeader extends ConsumerWidget { final double factor; - const ProfileHeader({super.key, required this.fullName, this.imagePath, this.factor = 0}); - - String _getInitials(String name) { - List nameParts = name.split(' '); - String initials = ""; - if (nameParts.isNotEmpty) { - initials = nameParts[0][0]; - if (nameParts.length > 1) { - initials += nameParts[1][0]; - } - } - return initials.toUpperCase(); - } + const ProfileHeader({super.key, this.factor = 0}); @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { + final user = ref.watch(userProvider); + final userImageBytes = user?.photoBytes; return Padding( padding: EdgeInsets.fromLTRB(factor, 0, 0, 0), child: Row( @@ -30,28 +21,28 @@ class ProfileHeader extends StatelessWidget { children: [ const SizedBox(width: 8), CircleAvatar( - radius: 20, - backgroundImage: imagePath != null - ? AssetImage(imagePath!) - : null, - backgroundColor: imagePath == null ? Palette.primary : null, - child: imagePath == null - ? Text( - _getInitials(fullName), - style: const TextStyle( - fontWeight: FontWeight.w500, - color: Palette.primaryText, + radius: 20, + backgroundImage: + userImageBytes != null ? MemoryImage(userImageBytes) : null, + backgroundColor: + userImageBytes == null ? Palette.primary : null, + child: userImageBytes == null + ? Text( + getInitials(user?.screenName ?? 'Moamen Hefny'), + style: const TextStyle( + fontWeight: FontWeight.w500, + color: Palette.primaryText, + ), + ) + : null, ), - ) - : null, - ), const SizedBox(width: 10), Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - fullName, + user?.screenName ?? 'Moamen Hefny', style: TextStyle( fontSize: 14 + 6 * factor / 100, fontWeight: FontWeight.bold, @@ -59,7 +50,7 @@ class ProfileHeader extends StatelessWidget { ), ), Text( - "online", + user?.status ?? 'no status', style: TextStyle( fontSize: 10 + 6 * factor / 100, color: Palette.accentText,