From 4fa51c775cfd60b49ea47730e589ac466898047f Mon Sep 17 00:00:00 2001 From: sonle Date: Tue, 19 Mar 2024 03:46:11 +0700 Subject: [PATCH 1/3] feat: create groups screen for mobile version --- assets/svg/ic_pencil.svg | 1 + assets/svg/ic_trust_deactivated.svg | 3 + .../widgets/desktop_cover_image_picker.dart | 3 +- .../contact_list_tile.dart} | 0 .../common_widgets/cover_image_picker.dart | 114 +++++++ .../contact_detail_screen.dart | 69 ++-- .../contact_new_version/contact_screen.dart | 4 +- .../create_group_screen.dart | 56 +--- .../group_contacts/group_contacts_screen.dart | 308 ++++++++++++++++++ .../widgets/groups_app_bar.dart | 57 ++++ .../widgets/groups_custom_button.dart | 60 ++++ .../widgets/groups_edit_options_widget.dart | 102 ++++++ .../widgets/groups_manage_members_widget.dart | 259 +++++++++++++++ .../widgets/groups_member_item_widget.dart | 91 ++++++ .../widgets/groups_member_list_view.dart | 43 +++ .../trusted_contacts/trusted_contacts.dart | 2 +- .../widgets/overlapping_contacts.dart | 2 +- lib/utils/text_styles.dart | 55 +++- lib/utils/vectors.dart | 3 + .../group_contact_list_test.dart | 2 +- 20 files changed, 1141 insertions(+), 93 deletions(-) create mode 100644 assets/svg/ic_pencil.svg create mode 100644 assets/svg/ic_trust_deactivated.svg rename lib/screens/{group_contacts_screen/widgets/group_contact_list_tile.dart => common_widgets/contact_list_tile.dart} (100%) create mode 100644 lib/screens/common_widgets/cover_image_picker.dart create mode 100644 lib/screens/group_contacts/group_contacts_screen.dart create mode 100644 lib/screens/group_contacts/widgets/groups_app_bar.dart create mode 100644 lib/screens/group_contacts/widgets/groups_custom_button.dart create mode 100644 lib/screens/group_contacts/widgets/groups_edit_options_widget.dart create mode 100644 lib/screens/group_contacts/widgets/groups_manage_members_widget.dart create mode 100644 lib/screens/group_contacts/widgets/groups_member_item_widget.dart create mode 100644 lib/screens/group_contacts/widgets/groups_member_list_view.dart diff --git a/assets/svg/ic_pencil.svg b/assets/svg/ic_pencil.svg new file mode 100644 index 00000000..d66ed484 --- /dev/null +++ b/assets/svg/ic_pencil.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/svg/ic_trust_deactivated.svg b/assets/svg/ic_trust_deactivated.svg new file mode 100644 index 00000000..b3cef1bc --- /dev/null +++ b/assets/svg/ic_trust_deactivated.svg @@ -0,0 +1,3 @@ + + + diff --git a/lib/desktop_screens_new/groups_screen/widgets/desktop_cover_image_picker.dart b/lib/desktop_screens_new/groups_screen/widgets/desktop_cover_image_picker.dart index 54194a67..3e275f7c 100644 --- a/lib/desktop_screens_new/groups_screen/widgets/desktop_cover_image_picker.dart +++ b/lib/desktop_screens_new/groups_screen/widgets/desktop_cover_image_picker.dart @@ -87,7 +87,7 @@ class DesktopCoverImagePicker extends StatelessWidget { ), ) : Container( - padding: const EdgeInsets.fromLTRB(108, 12, 108, 16), + padding: const EdgeInsets.only(top: 12, bottom: 16), decoration: BoxDecoration( borderRadius: BorderRadius.circular(5), color: ColorConstants.pickerBackgroundColor, @@ -104,7 +104,6 @@ class DesktopCoverImagePicker extends StatelessWidget { AppVectors.icDesktopImage, width: 48, height: 32, - fit: BoxFit.fitWidth, ), ], ), diff --git a/lib/screens/group_contacts_screen/widgets/group_contact_list_tile.dart b/lib/screens/common_widgets/contact_list_tile.dart similarity index 100% rename from lib/screens/group_contacts_screen/widgets/group_contact_list_tile.dart rename to lib/screens/common_widgets/contact_list_tile.dart diff --git a/lib/screens/common_widgets/cover_image_picker.dart b/lib/screens/common_widgets/cover_image_picker.dart new file mode 100644 index 00000000..39ddc5a4 --- /dev/null +++ b/lib/screens/common_widgets/cover_image_picker.dart @@ -0,0 +1,114 @@ +import 'dart:typed_data'; + +import 'package:atsign_atmosphere_pro/utils/colors.dart'; +import 'package:atsign_atmosphere_pro/utils/images.dart'; +import 'package:atsign_atmosphere_pro/utils/text_styles.dart'; +import 'package:atsign_atmosphere_pro/utils/vectors.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; + +class CoverImagePicker extends StatelessWidget { + final Function() onTap; + final Uint8List? groupImage; + final double height; + final EdgeInsetsGeometry margin; + final bool showOptions; + final Function()? onCancel; + + const CoverImagePicker({ + required this.onTap, + required this.groupImage, + required this.height, + this.margin = const EdgeInsets.symmetric(horizontal: 28), + this.showOptions = false, + this.onCancel, + }); + + @override + Widget build(BuildContext context) { + return InkWell( + onTap: onTap, + child: Container( + height: height, + width: double.infinity, + margin: margin, + alignment: Alignment.center, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10), + color: ColorConstants.dividerContextMenuColor, + ), + child: Stack( + fit: StackFit.expand, + children: [ + groupImage != null + ? ClipRRect( + borderRadius: BorderRadius.circular(10), + child: Image.memory( + groupImage!, + fit: BoxFit.cover, + ), + ) + : Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + "Insert Cover Image", + style: CustomTextStyles.greyW50014, + ), + SizedBox(height: 8), + Image.asset( + ImageConstants.icImage, + width: 48, + height: 32, + ), + ], + ), + if (showOptions) + Positioned( + top: 12, + right: 12, + left: 12, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + InkWell( + onTap: onCancel, + child: Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10), + color: Colors.white54.withOpacity(0.5), + ), + child: SvgPicture.asset( + AppVectors.icCancel, + width: 16, + height: 16, + color: Colors.black, + fit: BoxFit.cover, + ), + ), + ), + if (groupImage != null) + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10), + color: Colors.white54.withOpacity(0.5), + ), + child: SvgPicture.asset( + AppVectors.icEdit, + width: 16, + height: 16, + fit: BoxFit.cover, + ), + ), + ], + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/screens/contact_new_version/contact_detail_screen.dart b/lib/screens/contact_new_version/contact_detail_screen.dart index 7ea37aba..2abf445b 100644 --- a/lib/screens/contact_new_version/contact_detail_screen.dart +++ b/lib/screens/contact_new_version/contact_detail_screen.dart @@ -156,46 +156,39 @@ class _ContactDetailScreenState extends State { SizedBox(height: 4), Flexible( child: isEditNickname - ? Row( - children: [ - Flexible( - child: TextField( - maxLines: 1, - decoration: InputDecoration( - contentPadding: EdgeInsets.only( - left: 16, - ), - hintText: 'Enter Nickname', - hintStyle: TextStyle( - fontSize: 14.toFont, - fontWeight: FontWeight.w500, - color: ColorConstants.textBlack, - ), - border: OutlineInputBorder( - borderRadius: - BorderRadius.circular(5), - borderSide: BorderSide.none, - ), - labelStyle: TextStyle( - fontSize: 14.toFont, - ), - fillColor: Colors.white, - filled: true, - suffixIcon: InkWell( - onTap: () { - nicknameController.clear(); - }, - child: Icon( - Icons.clear, - color: Colors.black, - size: 16, - ), - ), - ), - controller: nicknameController, + ? TextField( + maxLines: 1, + decoration: InputDecoration( + contentPadding: EdgeInsets.only( + left: 16, + ), + hintText: 'Enter Nickname', + hintStyle: TextStyle( + fontSize: 14.toFont, + fontWeight: FontWeight.w500, + color: ColorConstants.textBlack, + ), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(5), + borderSide: BorderSide.none, + ), + labelStyle: TextStyle( + fontSize: 14.toFont, + ), + fillColor: Colors.white, + filled: true, + suffixIcon: InkWell( + onTap: () { + nicknameController.clear(); + }, + child: Icon( + Icons.clear, + color: Colors.black, + size: 16, ), ), - ], + ), + controller: nicknameController, ) : Text( widget.contact.tags?['nickname'] ?? diff --git a/lib/screens/contact_new_version/contact_screen.dart b/lib/screens/contact_new_version/contact_screen.dart index 22fc314b..8c9a33c1 100644 --- a/lib/screens/contact_new_version/contact_screen.dart +++ b/lib/screens/contact_new_version/contact_screen.dart @@ -1,5 +1,4 @@ import 'package:at_common_flutter/services/size_config.dart'; -import 'package:at_contacts_group_flutter/screens/group_view/group_view.dart'; import 'package:at_contacts_group_flutter/services/group_service.dart'; import 'package:atsign_atmosphere_pro/data_models/enums/contact_type.dart'; import 'package:atsign_atmosphere_pro/screens/common_widgets/app_bar_custom.dart'; @@ -10,6 +9,7 @@ import 'package:atsign_atmosphere_pro/screens/contact_new_version/contact_detail import 'package:atsign_atmosphere_pro/screens/contact_new_version/create_group_screen.dart'; import 'package:atsign_atmosphere_pro/screens/contact_new_version/widget/contact_skeleton_loading_widget.dart'; import 'package:atsign_atmosphere_pro/screens/contact_new_version/widget/list_contact_widget.dart'; +import 'package:atsign_atmosphere_pro/screens/group_contacts/group_contacts_screen.dart'; import 'package:atsign_atmosphere_pro/utils/colors.dart'; import 'package:atsign_atmosphere_pro/view_models/contact_provider.dart'; import 'package:atsign_atmosphere_pro/view_models/create_group_provider.dart'; @@ -274,7 +274,7 @@ class _ContactScreenState extends State await Navigator.push( context, MaterialPageRoute( - builder: (context) => GroupView( + builder: (context) => GroupContactsScreen( group: group, ), ), diff --git a/lib/screens/contact_new_version/create_group_screen.dart b/lib/screens/contact_new_version/create_group_screen.dart index f289a51f..69a8bffc 100644 --- a/lib/screens/contact_new_version/create_group_screen.dart +++ b/lib/screens/contact_new_version/create_group_screen.dart @@ -2,17 +2,16 @@ import 'package:at_common_flutter/at_common_flutter.dart'; import 'package:at_commons/at_commons.dart'; import 'package:at_contact/at_contact.dart'; import 'package:at_contacts_group_flutter/at_contacts_group_flutter.dart'; +import 'package:atsign_atmosphere_pro/screens/common_widgets/cover_image_picker.dart'; import 'package:atsign_atmosphere_pro/screens/common_widgets/custom_toast.dart'; import 'package:atsign_atmosphere_pro/screens/common_widgets/input_widget.dart'; import 'package:atsign_atmosphere_pro/screens/common_widgets/search_widget.dart'; import 'package:atsign_atmosphere_pro/screens/contact_new_version/widget/list_contact_widget.dart'; import 'package:atsign_atmosphere_pro/utils/colors.dart'; -import 'package:atsign_atmosphere_pro/utils/images.dart'; import 'package:atsign_atmosphere_pro/utils/text_strings.dart'; import 'package:atsign_atmosphere_pro/utils/vectors.dart'; import 'package:atsign_atmosphere_pro/view_models/create_group_provider.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:provider/provider.dart'; @@ -115,7 +114,14 @@ class _CreateGroupScreenState extends State { }, ), ), - _buildImage(value.selectedImageByteData), + SizedBox(height: 16), + CoverImagePicker( + onTap: () async { + await _provider.selectCoverImage(); + }, + groupImage: value.selectedImageByteData, + height: 88, + ), Padding( padding: const EdgeInsets.only( top: 22, @@ -254,48 +260,4 @@ class _CreateGroupScreenState extends State { ); }); } - - Widget _buildImage(Uint8List? selectedImage) { - return InkWell( - onTap: () async { - await _provider.selectCoverImage(); - }, - child: Container( - height: 89, - width: double.infinity, - margin: const EdgeInsets.fromLTRB(27, 14, 27, 0), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(10), - color: Color(0xFFECECEC), - ), - child: selectedImage != null - ? ClipRRect( - borderRadius: BorderRadius.circular(10), - child: Image.memory( - selectedImage, - fit: BoxFit.cover, - ), - ) - : Center( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - "Insert Cover Image", - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w500, - color: ColorConstants.grey, - ), - ), - SizedBox(height: 8), - Image.asset( - ImageConstants.icImage, - ), - ], - ), - ), - ), - ); - } } diff --git a/lib/screens/group_contacts/group_contacts_screen.dart b/lib/screens/group_contacts/group_contacts_screen.dart new file mode 100644 index 00000000..599023b0 --- /dev/null +++ b/lib/screens/group_contacts/group_contacts_screen.dart @@ -0,0 +1,308 @@ +import 'package:at_contact/at_contact.dart'; +import 'package:atsign_atmosphere_pro/screens/common_widgets/cover_image_picker.dart'; +import 'package:atsign_atmosphere_pro/screens/group_contacts/widgets/groups_app_bar.dart'; +import 'package:atsign_atmosphere_pro/screens/group_contacts/widgets/groups_custom_button.dart'; +import 'package:atsign_atmosphere_pro/screens/group_contacts/widgets/groups_edit_options_widget.dart'; +import 'package:atsign_atmosphere_pro/screens/group_contacts/widgets/groups_manage_members_widget.dart'; +import 'package:atsign_atmosphere_pro/screens/group_contacts/widgets/groups_member_list_view.dart'; +import 'package:atsign_atmosphere_pro/utils/colors.dart'; +import 'package:atsign_atmosphere_pro/utils/text_styles.dart'; +import 'package:atsign_atmosphere_pro/utils/vectors.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; + +class GroupContactsScreen extends StatefulWidget { + final AtGroup group; + + const GroupContactsScreen({required this.group}); + + @override + State createState() => _GroupContactsScreenState(); +} + +class _GroupContactsScreenState extends State { + bool showEditOptions = false; + bool isEditingName = false; + bool isEditingImage = false; + late TextEditingController groupNameController = + TextEditingController(text: widget.group.groupName); + + @override + Widget build(BuildContext context) { + return PopScope( + canPop: !isEditingName, + onPopInvoked: (didPop) { + if (isEditingName) { + groupNameController.text = widget.group.groupName ?? ''; + setState(() { + isEditingName = false; + }); + } + }, + child: Scaffold( + backgroundColor: ColorConstants.background, + appBar: GroupsAppBar( + title: isEditingName ? 'Edit' : widget.group.groupName ?? '', + onBack: () { + if (isEditingName) { + groupNameController.text = widget.group.groupName ?? ''; + setState(() { + isEditingName = false; + }); + } else { + Navigator.pop(context); + } + }, + actions: [ + isEditingName || isEditingImage + ? SvgPicture.asset( + AppVectors.icOptions, + width: 16, + fit: BoxFit.cover, + color: Colors.black, + ) + : InkWell( + onTap: () { + setState(() { + showEditOptions = !showEditOptions; + }); + }, + child: SvgPicture.asset( + AppVectors.icPencil, + width: 24, + height: 24, + fit: BoxFit.cover, + color: showEditOptions + ? ColorConstants.orangeColor + : Colors.black, + ), + ), + ], + ), + body: SafeArea( + child: Stack( + children: [ + Column( + children: [ + SizedBox(height: 20), + isEditingName + ? buildEditGroupNameWidget() + : CoverImagePicker( + showOptions: isEditingImage, + onTap: () {}, + groupImage: widget.group.groupPicture, + height: 120, + ), + SizedBox(height: isEditingName || isEditingImage ? 24 : 12), + isEditingName || isEditingImage + ? GroupsCustomButton( + title: 'Save', + onTap: () {}, + borderRadius: 5, + ) + : GroupsCustomButton( + title: 'Transfer File', + suffix: SvgPicture.asset( + AppVectors.icTransfer, + width: 24, + height: 16, + fit: BoxFit.cover, + ), + onTap: () {}, + borderRadius: 7, + ), + Expanded( + child: GroupsMemberListView( + members: widget.group.members!, + ), + ), + ], + ), + if (showEditOptions) ...[ + Positioned( + top: 0, + bottom: 0, + right: 0, + left: 0, + child: InkWell( + onTap: () { + setState(() { + showEditOptions = false; + }); + }, + ), + ), + Container( + padding: EdgeInsets.only(top: 20, left: 20, right: 20), + child: GroupsEditOptionsWidget( + onEditName: () { + setState(() { + showEditOptions = false; + isEditingName = true; + }); + }, + onCoverImage: () { + setState(() { + showEditOptions = false; + isEditingImage = true; + }); + }, + onManageMembers: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => GroupsManageMembersWidget(), + ), + ); + }, + onDelete: () async { + await showDeleteConfirmDialog(); + }, + ), + ), + ] + ], + ), + ), + ), + ); + } + + Widget buildEditGroupNameWidget() { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 28), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text( + 'Group Name', + style: CustomTextStyles.blackW5008, + ), + SizedBox(height: 4), + TextField( + maxLines: 1, + style: CustomTextStyles.blackW50014, + decoration: InputDecoration( + contentPadding: EdgeInsets.only( + left: 28, + top: 16, + bottom: 16, + ), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(5), + borderSide: BorderSide.none, + ), + fillColor: Colors.white, + filled: true, + isDense: true, + suffix: InkWell( + onTap: () { + groupNameController.clear(); + }, + child: Container( + padding: EdgeInsets.symmetric(horizontal: 12), + child: SvgPicture.asset( + AppVectors.icCancel, + color: Colors.black, + height: 8, + width: 8, + fit: BoxFit.cover, + ), + ), + ), + ), + controller: groupNameController, + ) + ], + ), + ); + } + + Future showDeleteConfirmDialog() async { + await showDialog( + context: context, + barrierDismissible: false, + builder: (context) { + return AlertDialog( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(7), + ), + contentPadding: + const EdgeInsets.symmetric(horizontal: 28, vertical: 24), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + 'Delete', + style: CustomTextStyles.blackBold(size: 14), + ), + SizedBox(height: 8), + Text( + 'Are you sure you want to delete ${widget.group.groupName}?', + style: CustomTextStyles.blackW40013, + textAlign: TextAlign.center, + ), + RichText( + text: TextSpan( + children: [ + TextSpan( + text: 'Note', + style: CustomTextStyles.blackItalicW40013, + ), + TextSpan( + text: ': this action cannot be undone.', + style: CustomTextStyles.blackW40013, + ) + ], + ), + ), + SizedBox(height: 8), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 12), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + InkWell( + onTap: () { + Navigator.pop(context); + }, + child: Container( + height: 36, + width: 84, + alignment: Alignment.center, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(4), + border: Border.all(color: Colors.black), + ), + child: Text( + 'Cancel', + style: CustomTextStyles.blackUnderlineW40012, + ), + ), + ), + InkWell( + child: Container( + height: 36, + width: 84, + alignment: Alignment.center, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(4), + color: ColorConstants.bottomBlack, + ), + child: Text( + 'Move', + style: CustomTextStyles.whiteBold12, + ), + ), + ) + ], + ), + ) + ], + ), + ); + }, + ); + } +} diff --git a/lib/screens/group_contacts/widgets/groups_app_bar.dart b/lib/screens/group_contacts/widgets/groups_app_bar.dart new file mode 100644 index 00000000..f038c50a --- /dev/null +++ b/lib/screens/group_contacts/widgets/groups_app_bar.dart @@ -0,0 +1,57 @@ +import 'package:atsign_atmosphere_pro/utils/text_styles.dart'; +import 'package:atsign_atmosphere_pro/utils/vectors.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; + +class GroupsAppBar extends StatelessWidget implements PreferredSizeWidget { + final String title; + final List actions; + final Function()? onBack; + + const GroupsAppBar({ + required this.title, + this.actions = const [], + this.onBack, + }); + + @override + Widget build(BuildContext context) { + return SafeArea( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 24), + child: Row( + children: [ + SizedBox(width: 8), + InkWell( + onTap: onBack ?? + () { + Navigator.pop(context); + }, + child: SizedBox( + width: 24, + height: 24, + child: SvgPicture.asset( + AppVectors.icBack, + height: 20, + width: 8, + fit: BoxFit.cover, + alignment: Alignment.center, + ), + ), + ), + SizedBox(width: 32), + Text( + title, + style: CustomTextStyles.desktopPrimaryW50018, + ), + Spacer(), + ...actions + ], + ), + ), + ); + } + + @override + Size get preferredSize => Size.fromHeight(28); +} diff --git a/lib/screens/group_contacts/widgets/groups_custom_button.dart b/lib/screens/group_contacts/widgets/groups_custom_button.dart new file mode 100644 index 00000000..a6cb4b8c --- /dev/null +++ b/lib/screens/group_contacts/widgets/groups_custom_button.dart @@ -0,0 +1,60 @@ +import 'package:atsign_atmosphere_pro/utils/colors.dart'; +import 'package:atsign_atmosphere_pro/utils/text_styles.dart'; +import 'package:flutter/material.dart'; + +class GroupsCustomButton extends StatelessWidget { + final Function() onTap; + final bool isLoading; + final String title; + final double spaceSize; + final Widget? suffix; + final double borderRadius; + + const GroupsCustomButton({ + required this.onTap, + this.isLoading = false, + required this.title, + this.spaceSize = 28, + this.suffix, + required this.borderRadius, + }); + + @override + Widget build(BuildContext context) { + return Container( + height: 52, + width: double.infinity, + decoration: BoxDecoration( + color: ColorConstants.raisinBlack, + borderRadius: BorderRadius.circular(borderRadius), + ), + margin: EdgeInsets.symmetric(horizontal: 28), + child: InkWell( + onTap: onTap, + child: Center( + child: isLoading + ? SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator( + color: Colors.white, + ), + ) + : Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + title, + style: CustomTextStyles.whiteBold(size: 14), + ), + if (suffix != null) ...[ + SizedBox(width: spaceSize), + suffix!, + ] + ], + ), + ), + ), + ); + } +} diff --git a/lib/screens/group_contacts/widgets/groups_edit_options_widget.dart b/lib/screens/group_contacts/widgets/groups_edit_options_widget.dart new file mode 100644 index 00000000..43f0fbe5 --- /dev/null +++ b/lib/screens/group_contacts/widgets/groups_edit_options_widget.dart @@ -0,0 +1,102 @@ +import 'package:atsign_atmosphere_pro/utils/text_styles.dart'; +import 'package:flutter/material.dart'; + +class GroupsEditOptionsWidget extends StatelessWidget { + final Function() onEditName; + final Function() onCoverImage; + final Function() onManageMembers; + final Function() onDelete; + + const GroupsEditOptionsWidget({ + required this.onEditName, + required this.onCoverImage, + required this.onManageMembers, + required this.onDelete, + }); + + @override + Widget build(BuildContext context) { + return Container( + clipBehavior: Clip.antiAlias, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(7), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.25), + blurRadius: 18, + offset: Offset(0, 4), + ) + ], + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + InkWell( + onTap: onEditName, + child: SizedBox( + height: 56, + child: Center( + child: Text( + 'Edit Name', + style: CustomTextStyles.blackW40017, + ), + ), + ), + ), + Divider( + height: 0, + thickness: 1, + color: Colors.black, + ), + InkWell( + onTap: onCoverImage, + child: SizedBox( + height: 56, + child: Center( + child: Text( + 'Edit Cover Image', + style: CustomTextStyles.blackW40017, + ), + ), + ), + ), + Divider( + height: 0, + thickness: 1, + color: Colors.black, + ), + InkWell( + onTap: onManageMembers, + child: SizedBox( + height: 56, + child: Center( + child: Text( + 'Manage Members', + style: CustomTextStyles.blackW40017, + ), + ), + ), + ), + Divider( + height: 0, + thickness: 1, + color: Colors.black, + ), + InkWell( + onTap: onDelete, + child: SizedBox( + height: 56, + child: Center( + child: Text( + 'Delete', + style: CustomTextStyles.blackW40017, + ), + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/screens/group_contacts/widgets/groups_manage_members_widget.dart b/lib/screens/group_contacts/widgets/groups_manage_members_widget.dart new file mode 100644 index 00000000..31a43c60 --- /dev/null +++ b/lib/screens/group_contacts/widgets/groups_manage_members_widget.dart @@ -0,0 +1,259 @@ +import 'package:at_common_flutter/services/size_config.dart'; +import 'package:at_contact/at_contact.dart'; +import 'package:at_contacts_group_flutter/models/group_contacts_model.dart'; +import 'package:at_contacts_group_flutter/services/group_service.dart'; +import 'package:atsign_atmosphere_pro/screens/group_contacts/widgets/groups_app_bar.dart'; +import 'package:atsign_atmosphere_pro/screens/group_contacts/widgets/groups_member_item_widget.dart'; +import 'package:atsign_atmosphere_pro/utils/colors.dart'; +import 'package:atsign_atmosphere_pro/utils/text_styles.dart'; +import 'package:atsign_atmosphere_pro/utils/vectors.dart'; +import 'package:atsign_atmosphere_pro/view_models/trusted_sender_view_model.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:provider/provider.dart'; + +class GroupsManageMembersWidget extends StatefulWidget { + const GroupsManageMembersWidget(); + + @override + State createState() => + _GroupsManageMembersWidgetState(); +} + +class _GroupsManageMembersWidgetState extends State { + TextEditingController searchController = TextEditingController(); + late TrustedContactProvider trustedContactProvider = + context.read(); + final _groupService = GroupService(); + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_) async { + await _groupService.fetchGroupsAndContacts(); + }); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: ColorConstants.background, + appBar: GroupsAppBar(title: 'Manage Members'), + body: SafeArea( + child: Stack( + children: [ + Column( + children: [ + SizedBox(height: 28), + Padding( + padding: EdgeInsets.only(left: 36, right: 20), + child: buildSearchWidget(), + ), + SizedBox(height: 12), + Expanded( + child: StreamBuilder>( + stream: _groupService.allContactsStream, + initialData: _groupService.allContacts, + builder: (context, snapshot) { + final contactList = (snapshot.data ?? []) + .where((e) => e?.contact != null) + .map((e) => e!.contact!) + .toList(); + return ListView.separated( + physics: ClampingScrollPhysics(), + padding: EdgeInsets.only( + left: 32, + right: 32, + bottom: 112, + ), + itemBuilder: (context, index) { + return buildContactList(contactList)[index]; + }, + separatorBuilder: (context, index) { + return SizedBox(height: 12); + }, + itemCount: buildContactList(contactList).length, + ); + }, + ), + ), + ], + ), + Positioned( + bottom: 28, + left: 48, + right: 48, + child: buildSelectContactButton(), + ), + ], + ), + ), + ); + } + + Widget buildSearchWidget() { + return Row( + children: [ + Expanded( + child: TextField( + maxLines: 1, + style: CustomTextStyles.blackW50014, + decoration: InputDecoration( + contentPadding: EdgeInsets.only( + left: 20, + top: 20, + bottom: 20, + right: 24, + ), + hintText: 'Search', + hintStyle: CustomTextStyles.greyW50014, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(10), + borderSide: BorderSide.none, + ), + fillColor: Colors.white, + filled: true, + isDense: true, + suffixIcon: Icon( + Icons.search, + size: 20, + color: ColorConstants.grey, + ), + ), + controller: searchController, + ), + ), + SizedBox(width: 12), + InkWell( + child: Container( + width: 40, + height: 40, + alignment: Alignment.center, + decoration: BoxDecoration( + color: ColorConstants.iconButtonColor, + shape: BoxShape.circle, + ), + child: SvgPicture.asset( + AppVectors.icTrust, + width: 24, + height: 20, + color: Colors.black, + fit: BoxFit.cover, + ), + ), + ), + ], + ); + } + + List buildContactList(List list) { + final List result = []; + if (list.isNotEmpty) { + final List sortedList = sortGroupAlphabetical(list); + bool isSameCharWithPrev(int i) => + (((sortedList[i].atSign ?? '').isNotEmpty + ? sortedList[i].atSign![1] + : ' ') != + ((sortedList[i - 1].atSign ?? '').isNotEmpty + ? sortedList[i - 1].atSign![1] + : ' ')); + + bool isPrevCharacter(int i) => RegExp(r'^[a-z]+$').hasMatch( + (((sortedList[i - 1].atSign ?? '').isNotEmpty + ? sortedList[i - 1].atSign![1] + : ' '))[0] + .toLowerCase()); + + for (int i = 0; i < sortedList.length; i++) { + if (i == 0 || (isSameCharWithPrev(i) && isPrevCharacter(i))) { + result.add(buildAlphabeticalTitle( + (sortedList[i].atSign ?? '').isNotEmpty + ? sortedList[i].atSign![1] + : '')); + } + result.add( + Padding( + padding: const EdgeInsets.symmetric(horizontal: 4), + child: GroupsMemberItemWidget( + member: sortedList[i], + isTrusted: trustedContactProvider.trustedContacts.any( + (element) => element.atSign == sortedList[i].atSign, + ), + ), + ), + ); + } + } + return result; + } + + Widget buildAlphabeticalTitle(String char) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: Row( + children: [ + char.isNotEmpty && RegExp(r'^[a-z]+$').hasMatch(char.toLowerCase()) + ? Text( + char.toUpperCase(), + style: CustomTextStyles.darkSliverBold20, + ) + : Text( + 'Others', + style: CustomTextStyles.darkSliverBold20, + ), + SizedBox(width: 20), + Divider( + height: 1.toHeight, + color: ColorConstants.dividerColor, + ) + ], + ), + ); + } + + List sortGroupAlphabetical(List list) { + final List emptyTitleContact = list + .where((e) => + (e.atSign ?? '').isEmpty || + !RegExp(r'^[a-z]+$').hasMatch( + (e.atSign ?? '')[1].toLowerCase(), + )) + .toList(); + final List nonEmptyTitleContact = list + .where((e) => + (e.atSign ?? '').isNotEmpty && + RegExp(r'^[a-z]+$').hasMatch( + (e.atSign ?? '')[1].toLowerCase(), + )) + .toList(); + nonEmptyTitleContact.sort( + (a, b) => (a.atSign?[1] ?? '').compareTo( + (b.atSign?[1] ?? ''), + ), + ); + return [...nonEmptyTitleContact, ...emptyTitleContact]; + } + + Widget buildSelectContactButton() { + return Container( + padding: EdgeInsets.symmetric(vertical: 16), + alignment: Alignment.center, + decoration: BoxDecoration( + color: Colors.black, + borderRadius: BorderRadius.circular(7), + ), + child: RichText( + text: TextSpan( + text: 'Selects Contact ', + style: CustomTextStyles.whiteBold16, + children: [ + TextSpan( + text: '(3)', + style: CustomTextStyles.whiteW40016, + ), + ], + ), + ), + ); + } +} diff --git a/lib/screens/group_contacts/widgets/groups_member_item_widget.dart b/lib/screens/group_contacts/widgets/groups_member_item_widget.dart new file mode 100644 index 00000000..81fbd08d --- /dev/null +++ b/lib/screens/group_contacts/widgets/groups_member_item_widget.dart @@ -0,0 +1,91 @@ +import 'dart:typed_data'; + +import 'package:at_contact/at_contact.dart'; +import 'package:atsign_atmosphere_pro/screens/common_widgets/contact_initial.dart'; +import 'package:atsign_atmosphere_pro/utils/colors.dart'; +import 'package:atsign_atmosphere_pro/utils/text_styles.dart'; +import 'package:atsign_atmosphere_pro/utils/vectors.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; + +class GroupsMemberItemWidget extends StatelessWidget { + final AtContact member; + final bool isTrusted; + final bool isSelected; + + const GroupsMemberItemWidget({ + required this.member, + required this.isTrusted, + this.isSelected = false, + }); + + @override + Widget build(BuildContext context) { + return Container( + padding: EdgeInsets.fromLTRB(16, 16, 24, 16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(10), + border: isSelected + ? Border.all( + color: ColorConstants.orange, + width: 2, + ) + : null, + ), + child: Row( + children: [ + member.tags?['image'] != null + ? ClipRRect( + borderRadius: BorderRadius.circular(18), + child: Image.memory( + Uint8List.fromList(member.tags?['image'].cast()), + width: 40, + height: 40, + fit: BoxFit.cover, + ), + ) + : ContactInitial( + initials: member.atSign?.substring(1), + borderRadius: 18, + ), + SizedBox(width: 16), + Expanded( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + member.atSign ?? '', + style: CustomTextStyles.blackW60013, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + if (member.tags?['nickname'] != null) + Text( + member.tags?['nickname'], + style: CustomTextStyles.blackW60013, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ], + )), + SizedBox(width: 16), + SizedBox( + width: 28, + height: 28, + child: SvgPicture.asset( + isTrusted + ? AppVectors.icTrustActivated + : AppVectors.icTrustDeactivated, + width: 24, + height: 20, + fit: BoxFit.cover, + alignment: Alignment.center, + ), + ) + ], + ), + ); + } +} diff --git a/lib/screens/group_contacts/widgets/groups_member_list_view.dart b/lib/screens/group_contacts/widgets/groups_member_list_view.dart new file mode 100644 index 00000000..0aac6fe0 --- /dev/null +++ b/lib/screens/group_contacts/widgets/groups_member_list_view.dart @@ -0,0 +1,43 @@ +import 'package:at_contact/at_contact.dart'; +import 'package:atsign_atmosphere_pro/screens/group_contacts/widgets/groups_member_item_widget.dart'; +import 'package:atsign_atmosphere_pro/view_models/trusted_sender_view_model.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +class GroupsMemberListView extends StatefulWidget { + final Set members; + + const GroupsMemberListView({ + required this.members, + }); + + @override + State createState() => _GroupsMemberListViewState(); +} + +class _GroupsMemberListViewState extends State { + late TrustedContactProvider trustedContactProvider = + context.read(); + + @override + Widget build(BuildContext context) { + return ListView.separated( + padding: EdgeInsets.symmetric(horizontal: 36, vertical: 28), + shrinkWrap: true, + physics: NeverScrollableScrollPhysics(), + itemBuilder: (context, index) { + return GroupsMemberItemWidget( + member: widget.members.elementAt(index), + isTrusted: trustedContactProvider.trustedContacts.any( + (element) => + element.atSign == widget.members.elementAt(index).atSign, + ), + ); + }, + separatorBuilder: (context, index) { + return SizedBox(height: 16); + }, + itemCount: widget.members.length, + ); + } +} diff --git a/lib/screens/trusted_contacts/trusted_contacts.dart b/lib/screens/trusted_contacts/trusted_contacts.dart index 5717d159..855cae04 100644 --- a/lib/screens/trusted_contacts/trusted_contacts.dart +++ b/lib/screens/trusted_contacts/trusted_contacts.dart @@ -9,7 +9,7 @@ import 'package:atsign_atmosphere_pro/screens/common_widgets/contact_initial.dar import 'package:atsign_atmosphere_pro/screens/common_widgets/custom_button.dart'; import 'package:atsign_atmosphere_pro/screens/common_widgets/custom_circle_avatar.dart'; import 'package:atsign_atmosphere_pro/screens/common_widgets/provider_handler.dart'; -import 'package:atsign_atmosphere_pro/screens/group_contacts_screen/widgets/group_contact_list_tile.dart'; +import 'package:atsign_atmosphere_pro/screens/common_widgets/contact_list_tile.dart'; import 'package:atsign_atmosphere_pro/screens/trusted_contacts/widgets/remove_trusted_contact_dialog.dart'; import 'package:atsign_atmosphere_pro/utils/images.dart'; import 'package:atsign_atmosphere_pro/utils/text_strings.dart'; diff --git a/lib/screens/welcome_screen/widgets/overlapping_contacts.dart b/lib/screens/welcome_screen/widgets/overlapping_contacts.dart index 17b6294e..7f87541b 100644 --- a/lib/screens/welcome_screen/widgets/overlapping_contacts.dart +++ b/lib/screens/welcome_screen/widgets/overlapping_contacts.dart @@ -7,7 +7,7 @@ import 'package:at_contact/at_contact.dart'; import 'package:at_contacts_group_flutter/models/group_contacts_model.dart'; import 'package:atsign_atmosphere_pro/screens/common_widgets/contact_initial.dart'; import 'package:atsign_atmosphere_pro/screens/common_widgets/custom_circle_avatar.dart'; -import 'package:atsign_atmosphere_pro/screens/group_contacts_screen/widgets/group_contact_list_tile.dart'; +import 'package:atsign_atmosphere_pro/screens/common_widgets/contact_list_tile.dart'; import 'package:atsign_atmosphere_pro/screens/welcome_screen/widgets/contact_card.dart'; import 'package:atsign_atmosphere_pro/services/common_utility_functions.dart'; import 'package:atsign_atmosphere_pro/view_models/trusted_sender_view_model.dart'; diff --git a/lib/utils/text_styles.dart b/lib/utils/text_styles.dart index 753f9e36..212e3ea9 100644 --- a/lib/utils/text_styles.dart +++ b/lib/utils/text_styles.dart @@ -379,6 +379,12 @@ class CustomTextStyles { letterSpacing: 0.1, fontWeight: FontWeight.normal); + static TextStyle desktopPrimaryW50018 = TextStyle( + color: Colors.black, + fontSize: 18, + fontWeight: FontWeight.w500, + ); + static TextStyle desktopPrimaryRegular16 = TextStyle( color: Colors.black, fontSize: 16, @@ -411,6 +417,12 @@ class CustomTextStyles { fontWeight: FontWeight.w500, ); + static TextStyle whiteW40016 = TextStyle( + color: Colors.white, + fontSize: 16, + fontWeight: FontWeight.w400, + ); + static TextStyle blackW50020 = const TextStyle( color: Colors.black, fontSize: 20, @@ -418,7 +430,10 @@ class CustomTextStyles { ); static TextStyle orangeW50014 = TextStyle( - color: ColorConstants.orange, fontSize: 14, fontWeight: FontWeight.w500); + color: ColorConstants.orange, + fontSize: 14, + fontWeight: FontWeight.w500, + ); static TextStyle whiteBold12 = const TextStyle( color: Colors.white, @@ -438,6 +453,26 @@ class CustomTextStyles { fontWeight: FontWeight.w400, ); + static TextStyle blackUnderlineW40012 = const TextStyle( + color: Colors.black, + fontSize: 12, + fontWeight: FontWeight.w400, + decoration: TextDecoration.underline, + ); + + static TextStyle blackW40013 = const TextStyle( + color: Colors.black, + fontSize: 13, + fontWeight: FontWeight.w400, + ); + + static TextStyle blackItalicW40013 = const TextStyle( + color: Colors.black, + fontSize: 13, + fontWeight: FontWeight.w400, + fontStyle: FontStyle.italic, + ); + static TextStyle darkSliverBold20 = TextStyle( color: ColorConstants.darkSliver, fontSize: 20, @@ -504,6 +539,12 @@ class CustomTextStyles { fontSize: 13, ); + static TextStyle blackW40017 = TextStyle( + color: Colors.black, + fontWeight: FontWeight.w400, + fontSize: 17, + ); + static TextStyle raisinBlackW40010 = TextStyle( color: ColorConstants.raisinBlack, fontSize: 10, @@ -528,6 +569,12 @@ class CustomTextStyles { fontSize: 11, ); + static TextStyle blackW5008 = TextStyle( + color: Colors.black, + fontWeight: FontWeight.w500, + fontSize: 8, + ); + static TextStyle blackW50014 = TextStyle( color: Colors.black, fontWeight: FontWeight.w500, @@ -581,4 +628,10 @@ class CustomTextStyles { fontWeight: FontWeight.w500, color: ColorConstants.portlandOrange, ); + + static TextStyle greyW50014 = TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: ColorConstants.grey, + ); } diff --git a/lib/utils/vectors.dart b/lib/utils/vectors.dart index cb88e5b2..ecae254a 100644 --- a/lib/utils/vectors.dart +++ b/lib/utils/vectors.dart @@ -98,6 +98,9 @@ class AppVectors { static String icDownloadDisable = '$_basePath/ic_download_disable.svg'; static String icSendDisable = '$_basePath/ic_send_disable.svg'; static String icDeleteDisable = '$_basePath/ic_delete_disable.svg'; + static String icPencil = '$_basePath/ic_pencil.svg'; + static String icMobileImage = '$_basePath/ic_mobile_image.svg'; + static String icTrustDeactivated = '$_basePath/ic_trust_deactivated.svg'; static String icDownloadOutline = '$_basePath/ic_download_outline.svg'; static String icDeliveredCheck = '$_basePath/ic_delivered_check.svg'; static String icDownloadedCheck = '$_basePath/ic_downloaded_check.svg'; diff --git a/test/widget_tests/group_contact_screen_test/group_contact_list_test.dart b/test/widget_tests/group_contact_screen_test/group_contact_list_test.dart index 3a9a6b08..28a364c3 100644 --- a/test/widget_tests/group_contact_screen_test/group_contact_list_test.dart +++ b/test/widget_tests/group_contact_screen_test/group_contact_list_test.dart @@ -1,6 +1,6 @@ import 'package:at_common_flutter/at_common_flutter.dart'; import 'package:atsign_atmosphere_pro/screens/common_widgets/contact_initial.dart'; -import 'package:atsign_atmosphere_pro/screens/group_contacts_screen/widgets/group_contact_list_tile.dart'; +import 'package:atsign_atmosphere_pro/screens/common_widgets/contact_list_tile.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; From faafe13833fdf79cd1b4f97b7d66a0d3a2030f4a Mon Sep 17 00:00:00 2001 From: sonle Date: Tue, 19 Mar 2024 20:43:19 +0700 Subject: [PATCH 2/3] feat: add functions --- lib/app.dart | 4 + .../groups_screen/desktop_groups_screen.dart | 12 +- .../widgets/desktop_groups_detail.dart | 10 +- .../common_widgets/cover_image_picker.dart | 58 +-- .../contact_new_version/contact_screen.dart | 8 +- .../group_contacts/group_contacts_screen.dart | 447 ++++++++++-------- .../widgets/groups_app_bar.dart | 8 +- .../widgets/groups_manage_members_widget.dart | 165 +++++-- .../widgets/groups_member_item_widget.dart | 121 ++--- lib/services/picker_service.dart | 6 +- .../desktop_groups_screen_provider.dart | 32 +- lib/view_models/groups_provider.dart | 326 +++++++++++++ pubspec.lock | 8 + pubspec.yaml | 1 + 14 files changed, 849 insertions(+), 357 deletions(-) create mode 100644 lib/view_models/groups_provider.dart diff --git a/lib/app.dart b/lib/app.dart index a8963946..b2d16f34 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -9,6 +9,7 @@ import 'package:atsign_atmosphere_pro/view_models/desktop_groups_screen_provider import 'package:atsign_atmosphere_pro/view_models/file_download_checker.dart'; import 'package:atsign_atmosphere_pro/desktop_routes/desktop_routes.dart'; import 'package:atsign_atmosphere_pro/view_models/file_progress_provider.dart'; +import 'package:atsign_atmosphere_pro/view_models/groups_provider.dart'; import 'package:atsign_atmosphere_pro/view_models/internet_connectivity_checker.dart'; import 'package:atsign_atmosphere_pro/view_models/my_files_provider.dart'; import 'package:atsign_atmosphere_pro/view_models/side_bar_provider.dart'; @@ -72,6 +73,9 @@ class _MyAppState extends State { ChangeNotifierProvider(create: (context) => DesktopAddGroupProvider()), ChangeNotifierProvider(create: (context) => ContactProvider()), ChangeNotifierProvider(create: (context) => NotificationService()), + ChangeNotifierProvider( + create: (context) => GroupsProvider(), + ), ], child: MaterialApp( builder: (BuildContext context, Widget? child) { diff --git a/lib/desktop_screens_new/groups_screen/desktop_groups_screen.dart b/lib/desktop_screens_new/groups_screen/desktop_groups_screen.dart index 39d975f1..08a2bb86 100644 --- a/lib/desktop_screens_new/groups_screen/desktop_groups_screen.dart +++ b/lib/desktop_screens_new/groups_screen/desktop_groups_screen.dart @@ -84,8 +84,8 @@ class _DesktopGroupsScreenState extends State { snapshot.data!, key: UniqueKey(), expandIndex: GroupService().expandIndex ?? 0, - onExpand: (value) { - provider.setSelectedAtGroup(value); + onExpand: (value) async { + await provider.setSelectedAtGroup(value); provider.setGroupCardState(GroupCardState.expanded); }, onAdd: () { @@ -111,8 +111,8 @@ class _DesktopGroupsScreenState extends State { children: [ Expanded( child: InkWell( - onTap: () { - groupsProvider.setSelectedAtGroup(null); + onTap: () async { + await groupsProvider.setSelectedAtGroup(null); groupsProvider .setGroupCardState(GroupCardState.disable); }, @@ -135,7 +135,7 @@ class _DesktopGroupsScreenState extends State { return DesktopAddGroup( onDoneTap: (value) async { if (value) await GroupService().getAllGroupsDetails(); - groupsProvider.setSelectedAtGroup(null); + await groupsProvider.setSelectedAtGroup(null); groupsProvider.setGroupCardState(GroupCardState.disable); }, ); @@ -143,7 +143,7 @@ class _DesktopGroupsScreenState extends State { return DesktopGroupsDetail( onBackArrowTap: (value) async { if (value) await GroupService().getAllGroupsDetails(); - groupsProvider.setSelectedAtGroup(null); + await groupsProvider.setSelectedAtGroup(null); groupsProvider.setGroupCardState(GroupCardState.disable); }, ); diff --git a/lib/desktop_screens_new/groups_screen/widgets/desktop_groups_detail.dart b/lib/desktop_screens_new/groups_screen/widgets/desktop_groups_detail.dart index 86cfe7f4..8cbb8b56 100644 --- a/lib/desktop_screens_new/groups_screen/widgets/desktop_groups_detail.dart +++ b/lib/desktop_screens_new/groups_screen/widgets/desktop_groups_detail.dart @@ -92,8 +92,8 @@ class _DesktopGroupsDetailState extends State { titleTextStyle: CustomTextStyles.blackW50020, leadingIcon: InkWell( onTap: provider.isEditing - ? () { - provider.setIsEditing(false); + ? () async { + await provider.setIsEditing(false); if (provider.isAddingContacts) { provider.setIsAddingContact(); } @@ -155,7 +155,7 @@ class _DesktopGroupsDetailState extends State { bgColor: ColorConstants.redAlert, ); } else { - provider.setSelectedGroupImage( + await provider.setSelectedGroupImage( await File(details.files.first.path) .readAsBytes(), ); @@ -292,7 +292,7 @@ class _DesktopGroupsDetailState extends State { IconButtonWidget( icon: AppVectors.icEdit, backgroundColor: ColorConstants.iconButtonColor, - onTap: () => groupProvider.setIsEditing(true), + onTap: () async => await groupProvider.setIsEditing(true), ), const SizedBox(width: 24), IconButtonWidget( @@ -330,7 +330,7 @@ class _DesktopGroupsDetailState extends State { if (!mounted) return; if (result != null && result) { Navigator.of(context).pop(); - groupProvider.setSelectedAtGroup(null); + await groupProvider.setSelectedAtGroup(null); groupProvider.setGroupCardState(GroupCardState.disable); } else { CustomToast().show(TextConstants().SERVICE_ERROR, context); diff --git a/lib/screens/common_widgets/cover_image_picker.dart b/lib/screens/common_widgets/cover_image_picker.dart index 39ddc5a4..0869e1e4 100644 --- a/lib/screens/common_widgets/cover_image_picker.dart +++ b/lib/screens/common_widgets/cover_image_picker.dart @@ -70,39 +70,39 @@ class CoverImagePicker extends StatelessWidget { right: 12, left: 12, child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - InkWell( - onTap: onCancel, - child: Container( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(10), - color: Colors.white54.withOpacity(0.5), - ), - child: SvgPicture.asset( - AppVectors.icCancel, - width: 16, - height: 16, - color: Colors.black, - fit: BoxFit.cover, - ), - ), - ), if (groupImage != null) - Container( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(10), - color: Colors.white54.withOpacity(0.5), - ), - child: SvgPicture.asset( - AppVectors.icEdit, - width: 16, - height: 16, - fit: BoxFit.cover, + InkWell( + onTap: onCancel, + child: Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10), + color: Colors.white54.withOpacity(0.5), + ), + child: SvgPicture.asset( + AppVectors.icCancel, + width: 16, + height: 16, + color: Colors.black, + fit: BoxFit.cover, + ), ), ), + Spacer(), + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10), + color: Colors.white54.withOpacity(0.5), + ), + child: SvgPicture.asset( + AppVectors.icEdit, + width: 16, + height: 16, + fit: BoxFit.cover, + ), + ), ], ), ), diff --git a/lib/screens/contact_new_version/contact_screen.dart b/lib/screens/contact_new_version/contact_screen.dart index 8c9a33c1..39857f80 100644 --- a/lib/screens/contact_new_version/contact_screen.dart +++ b/lib/screens/contact_new_version/contact_screen.dart @@ -271,7 +271,7 @@ class _ContactScreenState extends State .addPostFrameCallback((_) async { _groupService.groupViewSink.add(group); }); - await Navigator.push( + final result = await Navigator.push( context, MaterialPageRoute( builder: (context) => GroupContactsScreen( @@ -279,6 +279,12 @@ class _ContactScreenState extends State ), ), ); + + if (result ?? false) { + setState(() => isReloading = !isReloading); + await _groupService.fetchGroupsAndContacts(); + setState(() => isReloading = !isReloading); + } }, onTapAddButton: () async { final result = await showModalBottomSheet( diff --git a/lib/screens/group_contacts/group_contacts_screen.dart b/lib/screens/group_contacts/group_contacts_screen.dart index 599023b0..f67bb1de 100644 --- a/lib/screens/group_contacts/group_contacts_screen.dart +++ b/lib/screens/group_contacts/group_contacts_screen.dart @@ -1,15 +1,22 @@ import 'package:at_contact/at_contact.dart'; +import 'package:at_contacts_group_flutter/models/group_contacts_model.dart'; import 'package:atsign_atmosphere_pro/screens/common_widgets/cover_image_picker.dart'; import 'package:atsign_atmosphere_pro/screens/group_contacts/widgets/groups_app_bar.dart'; import 'package:atsign_atmosphere_pro/screens/group_contacts/widgets/groups_custom_button.dart'; import 'package:atsign_atmosphere_pro/screens/group_contacts/widgets/groups_edit_options_widget.dart'; import 'package:atsign_atmosphere_pro/screens/group_contacts/widgets/groups_manage_members_widget.dart'; import 'package:atsign_atmosphere_pro/screens/group_contacts/widgets/groups_member_list_view.dart'; +import 'package:atsign_atmosphere_pro/services/picker_service.dart'; +import 'package:atsign_atmosphere_pro/utils/app_utils.dart'; import 'package:atsign_atmosphere_pro/utils/colors.dart'; import 'package:atsign_atmosphere_pro/utils/text_styles.dart'; import 'package:atsign_atmosphere_pro/utils/vectors.dart'; +import 'package:atsign_atmosphere_pro/view_models/groups_provider.dart'; +import 'package:atsign_atmosphere_pro/view_models/welcome_screen_view_model.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; +import 'package:provider/provider.dart'; +import 'package:tuple/tuple.dart'; class GroupContactsScreen extends StatefulWidget { final AtGroup group; @@ -21,62 +28,96 @@ class GroupContactsScreen extends StatefulWidget { } class _GroupContactsScreenState extends State { - bool showEditOptions = false; - bool isEditingName = false; - bool isEditingImage = false; late TextEditingController groupNameController = - TextEditingController(text: widget.group.groupName); + TextEditingController(text: _groupsProvider.atGroup?.groupName ?? ''); + late final _groupsProvider = context.read(); + late final _welcomeProvider = context.read(); + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_) async { + await _groupsProvider.init(widget.group); + }); + } @override Widget build(BuildContext context) { return PopScope( - canPop: !isEditingName, - onPopInvoked: (didPop) { - if (isEditingName) { - groupNameController.text = widget.group.groupName ?? ''; - setState(() { - isEditingName = false; - }); + canPop: false, + onPopInvoked: (didPop) async { + if (didPop) { + return; + } + if (_groupsProvider.isEditingName) { + groupNameController.text = _groupsProvider.atGroup?.groupName ?? ''; + _groupsProvider.setIsEditingName(); + } else if (_groupsProvider.isEditingImage) { + await _groupsProvider + .setGroupPicture(_groupsProvider.atGroup?.groupPicture); + _groupsProvider.setIsEditingImage(); + } else { + Navigator.pop( + context, + _groupsProvider.needRefresh, + ); + _groupsProvider.reset(); } }, child: Scaffold( backgroundColor: ColorConstants.background, appBar: GroupsAppBar( - title: isEditingName ? 'Edit' : widget.group.groupName ?? '', - onBack: () { - if (isEditingName) { - groupNameController.text = widget.group.groupName ?? ''; - setState(() { - isEditingName = false; - }); + title: context.watch().isEditingName + ? 'Edit' + : _groupsProvider.atGroup?.groupName ?? '', + onBack: () async { + if (_groupsProvider.isEditingName) { + groupNameController.text = + _groupsProvider.atGroup?.groupName ?? ''; + _groupsProvider.setIsEditingName(); + } else if (_groupsProvider.isEditingImage) { + await _groupsProvider + .setGroupPicture(_groupsProvider.atGroup?.groupPicture); + _groupsProvider.setIsEditingImage(); } else { - Navigator.pop(context); + Navigator.pop( + context, + _groupsProvider.needRefresh, + ); + _groupsProvider.reset(); } }, actions: [ - isEditingName || isEditingImage - ? SvgPicture.asset( - AppVectors.icOptions, - width: 16, - fit: BoxFit.cover, - color: Colors.black, - ) - : InkWell( - onTap: () { - setState(() { - showEditOptions = !showEditOptions; - }); - }, - child: SvgPicture.asset( - AppVectors.icPencil, - width: 24, - height: 24, - fit: BoxFit.cover, - color: showEditOptions - ? ColorConstants.orangeColor - : Colors.black, - ), - ), + Selector>( + builder: (context, value, child) { + return value.item1 || value.item2 + ? SvgPicture.asset( + AppVectors.icOptions, + width: 16, + fit: BoxFit.cover, + color: Colors.black, + ) + : InkWell( + onTap: () { + _groupsProvider.setShowEditOptions(); + }, + child: SvgPicture.asset( + AppVectors.icPencil, + width: 24, + height: 24, + fit: BoxFit.cover, + color: value.item3 + ? ColorConstants.orangeColor + : Colors.black, + ), + ); + }, + selector: (_, p) => Tuple3( + p.isEditingName, + p.isEditingImage, + p.showEditOptions, + ), + ), ], ), body: SafeArea( @@ -85,82 +126,177 @@ class _GroupContactsScreenState extends State { Column( children: [ SizedBox(height: 20), - isEditingName - ? buildEditGroupNameWidget() - : CoverImagePicker( - showOptions: isEditingImage, - onTap: () {}, - groupImage: widget.group.groupPicture, - height: 120, - ), - SizedBox(height: isEditingName || isEditingImage ? 24 : 12), - isEditingName || isEditingImage - ? GroupsCustomButton( - title: 'Save', - onTap: () {}, - borderRadius: 5, - ) - : GroupsCustomButton( - title: 'Transfer File', - suffix: SvgPicture.asset( - AppVectors.icTransfer, - width: 24, - height: 16, - fit: BoxFit.cover, + Selector>( + builder: (context, value, child) { + return value.item1 + ? buildEditGroupNameWidget() + : CoverImagePicker( + showOptions: value.item2, + onTap: () async { + if (value.item2) { + await PickerService.pickImage( + onPickedImage: (result) async { + if (result.isNotEmpty) { + await AppUtils.checkGroupImageSize( + image: result, + onSatisfy: (value) { + _groupsProvider + .setGroupPicture(result); + }, + ); + } + }, + ); + } + }, + groupImage: value.item3, + height: 120, + onCancel: () async { + await _groupsProvider.setGroupPicture(null); + }, + ); + }, + selector: (_, p) => Tuple3( + p.isEditingName, + p.isEditingImage, + p.groupPicture, + ), + ), + Selector>( + builder: (context, value, child) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox( + height: value.item1 || value.item2 ? 24 : 12, ), - onTap: () {}, - borderRadius: 7, - ), + value.item1 || value.item2 + ? Selector>( + builder: (context, value, child) { + return GroupsCustomButton( + title: 'Save', + onTap: () async { + if (value.item2) { + await _groupsProvider.updateGroupName( + context, + name: groupNameController.text, + ); + return; + } + if (value.item3) { + await _groupsProvider + .updateGroupPicture(context); + return; + } + }, + borderRadius: 5, + isLoading: value.item1, + ); + }, + selector: (_, p) => Tuple3( + p.isLoading, + p.isEditingName, + p.isEditingImage, + ), + ) + : GroupsCustomButton( + title: 'Transfer File', + suffix: SvgPicture.asset( + AppVectors.icTransfer, + width: 24, + height: 16, + fit: BoxFit.cover, + ), + onTap: () { + _welcomeProvider.selectedContacts = [ + GroupContactsModel( + group: _groupsProvider.atGroup, + contactType: ContactsType.GROUP, + ), + ]; + Navigator.pop( + context, + _groupsProvider.needRefresh, + ); + _welcomeProvider + .changeBottomNavigationIndex(0); + }, + borderRadius: 7, + ), + ], + ); + }, + selector: (_, p) => Tuple2( + p.isEditingName, + p.isEditingImage, + ), + ), Expanded( - child: GroupsMemberListView( - members: widget.group.members!, + child: Selector>( + builder: (context, value, child) { + return GroupsMemberListView( + members: value, + ); + }, + selector: (_, p) => p.atGroup?.members ?? {}, ), ), ], ), - if (showEditOptions) ...[ - Positioned( - top: 0, - bottom: 0, - right: 0, - left: 0, - child: InkWell( - onTap: () { - setState(() { - showEditOptions = false; - }); - }, - ), - ), - Container( - padding: EdgeInsets.only(top: 20, left: 20, right: 20), - child: GroupsEditOptionsWidget( - onEditName: () { - setState(() { - showEditOptions = false; - isEditingName = true; - }); - }, - onCoverImage: () { - setState(() { - showEditOptions = false; - isEditingImage = true; - }); - }, - onManageMembers: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => GroupsManageMembersWidget(), - ), - ); - }, - onDelete: () async { - await showDeleteConfirmDialog(); - }, - ), - ), - ] + Selector( + builder: (context, value, child) { + return Visibility( + visible: value, + child: Positioned( + top: 0, + bottom: 0, + right: 0, + left: 0, + child: InkWell( + onTap: () { + _groupsProvider.setShowEditOptions(); + }, + ), + ), + ); + }, + selector: (_, p) => p.showEditOptions, + ), + Selector( + builder: (context, value, child) { + return Visibility( + visible: value, + child: Container( + padding: EdgeInsets.only(top: 20, left: 20, right: 20), + child: GroupsEditOptionsWidget( + onEditName: () { + _groupsProvider.setShowEditOptions(); + _groupsProvider.setIsEditingName(); + }, + onCoverImage: () { + _groupsProvider.setShowEditOptions(); + _groupsProvider.setIsEditingImage(); + }, + onManageMembers: () { + _groupsProvider.setShowEditOptions(); + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => GroupsManageMembersWidget(), + ), + ); + }, + onDelete: () async { + await _groupsProvider + .showDeleteConfirmDialog(context); + }, + ), + ), + ); + }, + selector: (_, p) => p.showEditOptions, + ) ], ), ), @@ -218,91 +354,4 @@ class _GroupContactsScreenState extends State { ), ); } - - Future showDeleteConfirmDialog() async { - await showDialog( - context: context, - barrierDismissible: false, - builder: (context) { - return AlertDialog( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(7), - ), - contentPadding: - const EdgeInsets.symmetric(horizontal: 28, vertical: 24), - content: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - 'Delete', - style: CustomTextStyles.blackBold(size: 14), - ), - SizedBox(height: 8), - Text( - 'Are you sure you want to delete ${widget.group.groupName}?', - style: CustomTextStyles.blackW40013, - textAlign: TextAlign.center, - ), - RichText( - text: TextSpan( - children: [ - TextSpan( - text: 'Note', - style: CustomTextStyles.blackItalicW40013, - ), - TextSpan( - text: ': this action cannot be undone.', - style: CustomTextStyles.blackW40013, - ) - ], - ), - ), - SizedBox(height: 8), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 12), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - InkWell( - onTap: () { - Navigator.pop(context); - }, - child: Container( - height: 36, - width: 84, - alignment: Alignment.center, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(4), - border: Border.all(color: Colors.black), - ), - child: Text( - 'Cancel', - style: CustomTextStyles.blackUnderlineW40012, - ), - ), - ), - InkWell( - child: Container( - height: 36, - width: 84, - alignment: Alignment.center, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(4), - color: ColorConstants.bottomBlack, - ), - child: Text( - 'Move', - style: CustomTextStyles.whiteBold12, - ), - ), - ) - ], - ), - ) - ], - ), - ); - }, - ); - } } diff --git a/lib/screens/group_contacts/widgets/groups_app_bar.dart b/lib/screens/group_contacts/widgets/groups_app_bar.dart index f038c50a..dcad88ee 100644 --- a/lib/screens/group_contacts/widgets/groups_app_bar.dart +++ b/lib/screens/group_contacts/widgets/groups_app_bar.dart @@ -18,7 +18,11 @@ class GroupsAppBar extends StatelessWidget implements PreferredSizeWidget { Widget build(BuildContext context) { return SafeArea( child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 24), + padding: const EdgeInsets.only( + left: 24, + right: 24, + top: 36, + ), child: Row( children: [ SizedBox(width: 8), @@ -53,5 +57,5 @@ class GroupsAppBar extends StatelessWidget implements PreferredSizeWidget { } @override - Size get preferredSize => Size.fromHeight(28); + Size get preferredSize => Size.fromHeight(64); } diff --git a/lib/screens/group_contacts/widgets/groups_manage_members_widget.dart b/lib/screens/group_contacts/widgets/groups_manage_members_widget.dart index 31a43c60..65e86ca4 100644 --- a/lib/screens/group_contacts/widgets/groups_manage_members_widget.dart +++ b/lib/screens/group_contacts/widgets/groups_manage_members_widget.dart @@ -1,16 +1,17 @@ import 'package:at_common_flutter/services/size_config.dart'; import 'package:at_contact/at_contact.dart'; -import 'package:at_contacts_group_flutter/models/group_contacts_model.dart'; import 'package:at_contacts_group_flutter/services/group_service.dart'; import 'package:atsign_atmosphere_pro/screens/group_contacts/widgets/groups_app_bar.dart'; import 'package:atsign_atmosphere_pro/screens/group_contacts/widgets/groups_member_item_widget.dart'; import 'package:atsign_atmosphere_pro/utils/colors.dart'; import 'package:atsign_atmosphere_pro/utils/text_styles.dart'; import 'package:atsign_atmosphere_pro/utils/vectors.dart'; +import 'package:atsign_atmosphere_pro/view_models/groups_provider.dart'; import 'package:atsign_atmosphere_pro/view_models/trusted_sender_view_model.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:provider/provider.dart'; +import 'package:tuple/tuple.dart'; class GroupsManageMembersWidget extends StatefulWidget { const GroupsManageMembersWidget(); @@ -24,6 +25,8 @@ class _GroupsManageMembersWidgetState extends State { TextEditingController searchController = TextEditingController(); late TrustedContactProvider trustedContactProvider = context.read(); + late GroupsProvider groupsProvider = context.read(); + final _groupService = GroupService(); @override @@ -51,14 +54,8 @@ class _GroupsManageMembersWidgetState extends State { ), SizedBox(height: 12), Expanded( - child: StreamBuilder>( - stream: _groupService.allContactsStream, - initialData: _groupService.allContacts, - builder: (context, snapshot) { - final contactList = (snapshot.data ?? []) - .where((e) => e?.contact != null) - .map((e) => e!.contact!) - .toList(); + child: Selector>( + builder: (context, value, child) { return ListView.separated( physics: ClampingScrollPhysics(), padding: EdgeInsets.only( @@ -67,14 +64,36 @@ class _GroupsManageMembersWidgetState extends State { bottom: 112, ), itemBuilder: (context, index) { - return buildContactList(contactList)[index]; + return buildContactList( + filterContacts( + data: _groupService.allContacts + .where((e) => e?.contact != null) + .map((e) => e!.contact!) + .toList(), + searchKeyword: value.item1, + showTrustedMembers: value.item2, + ), + )[index]; }, separatorBuilder: (context, index) { return SizedBox(height: 12); }, - itemCount: buildContactList(contactList).length, + itemCount: buildContactList( + filterContacts( + data: _groupService.allContacts + .where((e) => e?.contact != null) + .map((e) => e!.contact!) + .toList(), + searchKeyword: value.item1, + showTrustedMembers: value.item2, + ), + ).length, ); }, + selector: (_, p) => Tuple2( + p.searchKeyword, + p.showTrustedMembers, + ), ), ), ], @@ -91,12 +110,36 @@ class _GroupsManageMembersWidgetState extends State { ); } + List filterContacts( + {required List data, + required bool showTrustedMembers, + required String searchKeyword}) { + List sampleList = data; + + if (showTrustedMembers) { + sampleList = sampleList + .where((element) => trustedContactProvider.trustedContacts + .any((e) => e.atSign == element.atSign)) + .map((e) => e) + .toList(); + } else if (searchKeyword.isNotEmpty) { + sampleList = sampleList + .where((element) => (element.atSign ?? '').contains(searchKeyword)) + .map((e) => e) + .toList(); + } + return sampleList; + } + Widget buildSearchWidget() { return Row( children: [ Expanded( child: TextField( maxLines: 1, + onChanged: (value) { + groupsProvider.changeSearchKeyword(value); + }, style: CustomTextStyles.blackW50014, decoration: InputDecoration( contentPadding: EdgeInsets.only( @@ -125,6 +168,9 @@ class _GroupsManageMembersWidgetState extends State { ), SizedBox(width: 12), InkWell( + onTap: () { + groupsProvider.setShowTrustedMembersStatus(); + }, child: Container( width: 40, height: 40, @@ -133,12 +179,17 @@ class _GroupsManageMembersWidgetState extends State { color: ColorConstants.iconButtonColor, shape: BoxShape.circle, ), - child: SvgPicture.asset( - AppVectors.icTrust, - width: 24, - height: 20, - color: Colors.black, - fit: BoxFit.cover, + child: Selector( + builder: (context, value, child) { + return SvgPicture.asset( + AppVectors.icTrust, + width: 24, + height: 20, + color: value ? ColorConstants.orange : Colors.black, + fit: BoxFit.cover, + ); + }, + selector: (_, p) => p.showTrustedMembers, ), ), ), @@ -172,13 +223,24 @@ class _GroupsManageMembersWidgetState extends State { : '')); } result.add( - Padding( - padding: const EdgeInsets.symmetric(horizontal: 4), - child: GroupsMemberItemWidget( - member: sortedList[i], - isTrusted: trustedContactProvider.trustedContacts.any( - (element) => element.atSign == sortedList[i].atSign, - ), + Selector( + builder: (context, value, child) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 4), + child: GroupsMemberItemWidget( + member: sortedList[i], + isTrusted: trustedContactProvider.trustedContacts.any( + (element) => element.atSign == sortedList[i].atSign, + ), + isSelected: value, + onTap: () { + groupsProvider.addOrRemoveMember(sortedList[i]); + }, + ), + ); + }, + selector: (_, p) => p.members.any( + (element) => element.atSign == sortedList[i].atSign, ), ), ); @@ -235,23 +297,44 @@ class _GroupsManageMembersWidgetState extends State { } Widget buildSelectContactButton() { - return Container( - padding: EdgeInsets.symmetric(vertical: 16), - alignment: Alignment.center, - decoration: BoxDecoration( - color: Colors.black, - borderRadius: BorderRadius.circular(7), - ), - child: RichText( - text: TextSpan( - text: 'Selects Contact ', - style: CustomTextStyles.whiteBold16, - children: [ - TextSpan( - text: '(3)', - style: CustomTextStyles.whiteW40016, - ), - ], + return InkWell( + onTap: () async { + await groupsProvider.updateMembers(context); + }, + child: Container( + padding: EdgeInsets.symmetric(vertical: 16), + alignment: Alignment.center, + decoration: BoxDecoration( + color: Colors.black, + borderRadius: BorderRadius.circular(7), + ), + child: Selector>( + builder: (context, value, child) { + return value.item2 + ? SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator( + color: Colors.white, + ), + ) + : RichText( + text: TextSpan( + text: 'Selects Contact ', + style: CustomTextStyles.whiteBold16, + children: [ + TextSpan( + text: '(${value.item1})', + style: CustomTextStyles.whiteW40016, + ), + ], + ), + ); + }, + selector: (_, p) => Tuple2( + p.members.length, + p.isLoading, + ), ), ), ); diff --git a/lib/screens/group_contacts/widgets/groups_member_item_widget.dart b/lib/screens/group_contacts/widgets/groups_member_item_widget.dart index 81fbd08d..ee03da7f 100644 --- a/lib/screens/group_contacts/widgets/groups_member_item_widget.dart +++ b/lib/screens/group_contacts/widgets/groups_member_item_widget.dart @@ -12,79 +12,84 @@ class GroupsMemberItemWidget extends StatelessWidget { final AtContact member; final bool isTrusted; final bool isSelected; + final Function()? onTap; const GroupsMemberItemWidget({ required this.member, required this.isTrusted, this.isSelected = false, + this.onTap, }); @override Widget build(BuildContext context) { - return Container( - padding: EdgeInsets.fromLTRB(16, 16, 24, 16), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(10), - border: isSelected - ? Border.all( - color: ColorConstants.orange, - width: 2, - ) - : null, - ), - child: Row( - children: [ - member.tags?['image'] != null - ? ClipRRect( - borderRadius: BorderRadius.circular(18), - child: Image.memory( - Uint8List.fromList(member.tags?['image'].cast()), - width: 40, - height: 40, - fit: BoxFit.cover, - ), + return InkWell( + onTap: onTap, + child: Container( + padding: EdgeInsets.fromLTRB(16, 16, 24, 16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(10), + border: isSelected + ? Border.all( + color: ColorConstants.orange, + width: 2, ) - : ContactInitial( - initials: member.atSign?.substring(1), - borderRadius: 18, - ), - SizedBox(width: 16), - Expanded( - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - member.atSign ?? '', - style: CustomTextStyles.blackW60013, - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - if (member.tags?['nickname'] != null) + : null, + ), + child: Row( + children: [ + member.tags?['image'] != null + ? ClipRRect( + borderRadius: BorderRadius.circular(18), + child: Image.memory( + Uint8List.fromList(member.tags?['image'].cast()), + width: 40, + height: 40, + fit: BoxFit.cover, + ), + ) + : ContactInitial( + initials: member.atSign?.substring(1), + borderRadius: 18, + ), + SizedBox(width: 16), + Expanded( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ Text( - member.tags?['nickname'], + member.atSign ?? '', style: CustomTextStyles.blackW60013, maxLines: 1, overflow: TextOverflow.ellipsis, ), - ], - )), - SizedBox(width: 16), - SizedBox( - width: 28, - height: 28, - child: SvgPicture.asset( - isTrusted - ? AppVectors.icTrustActivated - : AppVectors.icTrustDeactivated, - width: 24, - height: 20, - fit: BoxFit.cover, - alignment: Alignment.center, - ), - ) - ], + if (member.tags?['nickname'] != null) + Text( + member.tags?['nickname'], + style: CustomTextStyles.blackW60013, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ], + )), + SizedBox(width: 16), + SizedBox( + width: 28, + height: 28, + child: SvgPicture.asset( + isTrusted + ? AppVectors.icTrustActivated + : AppVectors.icTrustDeactivated, + width: 24, + height: 20, + fit: BoxFit.cover, + alignment: Alignment.center, + ), + ) + ], + ), ), ); } diff --git a/lib/services/picker_service.dart b/lib/services/picker_service.dart index 292b8e96..e37bb13a 100644 --- a/lib/services/picker_service.dart +++ b/lib/services/picker_service.dart @@ -15,8 +15,10 @@ class PickerService { source: ImageSource.gallery, ); if (image != null) { - final result = await File(image.path).readAsBytes(); - onPickedImage.call(result); + final result = await File(image.path).readAsBytes(); + onPickedImage.call(result); + } else { + onPickedImage.call(Uint8List(0)); } } } diff --git a/lib/view_models/desktop_groups_screen_provider.dart b/lib/view_models/desktop_groups_screen_provider.dart index 5d7923d0..11da5bad 100644 --- a/lib/view_models/desktop_groups_screen_provider.dart +++ b/lib/view_models/desktop_groups_screen_provider.dart @@ -36,15 +36,15 @@ class DesktopGroupsScreenProvider extends ChangeNotifier { notifyListeners(); } - void setSelectedAtGroup(AtGroup? atGroup) { + Future setSelectedAtGroup(AtGroup? atGroup) async { selectedAtGroup = atGroup; setSelectedGroupName(selectedAtGroup?.groupName ?? ''); if (selectedAtGroup?.groupPicture != null) { - setSelectedGroupImage( + await setSelectedGroupImage( Uint8List.fromList(selectedAtGroup?.groupPicture.cast()), ); } else { - setSelectedGroupImage(Uint8List(0)); + await setSelectedGroupImage(Uint8List(0)); } notifyListeners(); } @@ -64,13 +64,13 @@ class DesktopGroupsScreenProvider extends ChangeNotifier { notifyListeners(); } - void setIsEditing(bool status) { + Future setIsEditing(bool status) async { isEditing = status; if (selectedAtGroup?.groupPicture != null) { - setSelectedGroupImage( + await setSelectedGroupImage( Uint8List.fromList(selectedAtGroup?.groupPicture.cast())); } else { - setSelectedGroupImage(Uint8List(0)); + await setSelectedGroupImage(Uint8List(0)); } notifyListeners(); } @@ -80,14 +80,18 @@ class DesktopGroupsScreenProvider extends ChangeNotifier { notifyListeners(); } - void setSelectedGroupImage(Uint8List data) { - AppUtils.checkGroupImageSize( - image: data, - onSatisfy: (value) { - selectedGroupImage = value; - notifyListeners(); - }, - ); + Future setSelectedGroupImage(Uint8List data) async { + if (data.isNotEmpty) { + await AppUtils.checkGroupImageSize( + image: data, + onSatisfy: (value) { + selectedGroupImage = value; + }, + ); + } else { + selectedGroupImage = data; + } + notifyListeners(); } void setSelectedGroupName(String name) { diff --git a/lib/view_models/groups_provider.dart b/lib/view_models/groups_provider.dart new file mode 100644 index 00000000..51ab3345 --- /dev/null +++ b/lib/view_models/groups_provider.dart @@ -0,0 +1,326 @@ +import 'package:at_commons/at_commons.dart'; +import 'package:at_contact/at_contact.dart'; +import 'package:at_contacts_group_flutter/services/group_service.dart'; +import 'package:at_contacts_group_flutter/utils/text_constants.dart'; +import 'package:atsign_atmosphere_pro/routes/route_names.dart'; +import 'package:atsign_atmosphere_pro/screens/common_widgets/custom_toast.dart'; +import 'package:atsign_atmosphere_pro/utils/app_utils.dart'; +import 'package:atsign_atmosphere_pro/utils/colors.dart'; +import 'package:atsign_atmosphere_pro/utils/text_styles.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +class GroupsProvider extends ChangeNotifier { + AtGroup? atGroup; + bool showEditOptions = false; + bool isEditingName = false; + bool isEditingImage = false; + dynamic groupPicture; + Set members = {}; + bool isLoading = false; + bool needRefresh = false; + bool showTrustedMembers = false; + String searchKeyword = ''; + + Future init(AtGroup data) async { + atGroup = data; + members = atGroup?.members ?? {}; + if (atGroup?.groupPicture != null) { + await setGroupPicture( + Uint8List.fromList(atGroup?.groupPicture.cast()), + ); + } + notifyListeners(); + } + + void reset() { + showEditOptions = false; + isEditingName = false; + isEditingImage = false; + groupPicture = null; + members.clear(); + atGroup = null; + isLoading = false; + needRefresh = false; + notifyListeners(); + } + + void resetManageMembers() { + showTrustedMembers = false; + searchKeyword = ''; + notifyListeners(); + } + + void setShowEditOptions() { + showEditOptions = !showEditOptions; + notifyListeners(); + } + + void setIsEditingName() { + isEditingName = !isEditingName; + notifyListeners(); + } + + void setIsEditingImage() { + isEditingImage = !isEditingImage; + notifyListeners(); + } + + Future setGroupPicture(Uint8List? data) async { + if (data != null) { + await AppUtils.checkGroupImageSize( + image: data, + onSatisfy: (value) { + groupPicture = value; + }, + ); + } else { + groupPicture = null; + } + notifyListeners(); + } + + Future updateGroupName( + BuildContext context, { + required String name, + }) async { + if (isLoading) return; + isLoading = true; + notifyListeners(); + + if (name.trim().isEmpty) { + ScaffoldMessenger.of(context) + .showSnackBar(SnackBar(content: Text(TextConstants().EMPTY_NAME))); + } else { + AtGroup group = atGroup!; + group.groupName = name.trim(); + group.displayName = name.trim(); + final result = await GroupService().updateGroup(group); + if (result is AtGroup) { + atGroup = result; + + isEditingName = false; + } else if (result != null) { + if (result.runtimeType == AlreadyExistsException) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(TextConstants().GROUP_ALREADY_EXISTS))); + } else if (result.runtimeType == InvalidAtSignException) { + ScaffoldMessenger.of(context) + .showSnackBar(SnackBar(content: Text(result.content))); + } else { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(TextConstants().SERVICE_ERROR))); + } + } else { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(TextConstants().SERVICE_ERROR))); + } + } + isLoading = false; + notifyListeners(); + } + + Future updateGroupPicture(BuildContext context) async { + if (isLoading) return; + isLoading = true; + notifyListeners(); + + AtGroup group = atGroup!; + group.groupPicture = groupPicture; + + final result = await GroupService().updateGroup(group); + if (result is AtGroup) { + atGroup = result; + isEditingImage = false; + if (!needRefresh) { + needRefresh = true; + } + } else if (result != null) { + if (result.runtimeType == AlreadyExistsException) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(TextConstants().GROUP_ALREADY_EXISTS))); + } else if (result.runtimeType == InvalidAtSignException) { + ScaffoldMessenger.of(context) + .showSnackBar(SnackBar(content: Text(result.content))); + } else { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(TextConstants().SERVICE_ERROR))); + } + } else { + ScaffoldMessenger.of(context) + .showSnackBar(SnackBar(content: Text(TextConstants().SERVICE_ERROR))); + } + isLoading = false; + notifyListeners(); + } + + Future updateMembers(BuildContext context) async { + if (isLoading) return; + isLoading = true; + notifyListeners(); + + if (members.isEmpty) { + await showDeleteConfirmDialog(context); + } else { + AtGroup group = atGroup!; + group.members = members; + final result = await GroupService().updateGroup(group); + if (result is AtGroup) { + atGroup = result; + resetManageMembers(); + Navigator.pop(context); + } else if (result != null) { + if (result.runtimeType == AlreadyExistsException) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(TextConstants().GROUP_ALREADY_EXISTS))); + } else if (result.runtimeType == InvalidAtSignException) { + ScaffoldMessenger.of(context) + .showSnackBar(SnackBar(content: Text(result.content))); + } else { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(TextConstants().SERVICE_ERROR))); + } + } else { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(TextConstants().SERVICE_ERROR))); + } + } + isLoading = false; + notifyListeners(); + } + + void addOrRemoveMember(AtContact member) { + final membersList = members; + if (membersList.any((element) => element.atSign == member.atSign)) { + print('remove'); + membersList.removeWhere( + (element) => element.atSign == member.atSign, + ); + print(membersList); + } else { + print('add'); + membersList.add(member); + } + setMembers(membersList); + } + + void setMembers(Set data) { + members = data; + notifyListeners(); + } + + void setShowTrustedMembersStatus() { + showTrustedMembers = !showTrustedMembers; + notifyListeners(); + } + + void changeSearchKeyword(String text) { + searchKeyword = text; + notifyListeners(); + } + + Future showDeleteConfirmDialog(BuildContext context) async { + await showDialog( + context: context, + barrierDismissible: false, + builder: (context) { + return AlertDialog( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(7), + ), + contentPadding: + const EdgeInsets.symmetric(horizontal: 28, vertical: 24), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + 'Delete', + style: CustomTextStyles.blackBold(size: 14), + ), + SizedBox(height: 8), + Text( + 'Are you sure you want to delete ${atGroup?.groupName ?? ''}?', + style: CustomTextStyles.blackW40013, + textAlign: TextAlign.center, + ), + RichText( + text: TextSpan( + children: [ + TextSpan( + text: 'Note', + style: CustomTextStyles.blackItalicW40013, + ), + TextSpan( + text: ': this action cannot be undone.', + style: CustomTextStyles.blackW40013, + ) + ], + ), + ), + SizedBox(height: 8), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 12), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + InkWell( + onTap: () { + Navigator.pop(context); + }, + child: Container( + height: 36, + width: 84, + alignment: Alignment.center, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(4), + border: Border.all(color: Colors.black), + ), + child: Text( + 'Cancel', + style: CustomTextStyles.blackUnderlineW40012, + ), + ), + ), + InkWell( + onTap: () async { + final result = + await GroupService().deleteGroup(atGroup!); + if (result != null && result) { + reset(); + resetManageMembers(); + await GroupService().fetchGroupsAndContacts(); + Navigator.popUntil( + context, + ModalRoute.withName(Routes.WELCOME_SCREEN), + ); + } else { + CustomToast().show( + TextConstants().SERVICE_ERROR, + context, + ); + } + }, + child: Container( + height: 36, + width: 84, + alignment: Alignment.center, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(4), + color: ColorConstants.bottomBlack, + ), + child: Text( + 'Move', + style: CustomTextStyles.whiteBold12, + ), + ), + ) + ], + ), + ) + ], + ), + ); + }, + ); + } +} diff --git a/pubspec.lock b/pubspec.lock index dfa79fe0..6c745edd 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1381,6 +1381,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.9.2" + tuple: + dependency: "direct main" + description: + name: tuple + sha256: a97ce2013f240b2f3807bcbaf218765b6f301c3eff91092bcfa23a039e7dd151 + url: "https://pub.dev" + source: hosted + version: "2.0.2" tutorial_coach_mark: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 4e5f7e18..c344e593 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -87,6 +87,7 @@ dependencies: open_store: ^0.5.0 flutter_image_compress: ^2.2.0 smooth_page_indicator: ^1.1.0 + tuple: ^2.0.2 dev_dependencies: flutter_test: From 8d5d63460542d97d8cd71cac6796bfbf56ca7a42 Mon Sep 17 00:00:00 2001 From: sonle Date: Thu, 21 Mar 2024 17:33:17 +0700 Subject: [PATCH 3/3] feat: renew group screen for desktop version --- assets/svg/ic_delete_group.svg | 3 + lib/desktop_routes/desktop_route_names.dart | 2 + lib/desktop_routes/desktop_routes.dart | 4 + ...desktop_add_or_remove_contacts_screen.dart | 309 +++++++++++++++ .../widgets/desktop_cover_image_picker.dart | 65 ++-- .../widgets/desktop_custom_list_tile.dart | 196 +++++----- .../widgets/desktop_group_contacts_list.dart | 316 ++++++++++------ .../widgets/desktop_groups_detail.dart | 354 +++++++++++------- lib/utils/colors.dart | 2 + lib/utils/text_styles.dart | 16 +- lib/utils/vectors.dart | 1 + .../desktop_groups_screen_provider.dart | 7 + 12 files changed, 885 insertions(+), 390 deletions(-) create mode 100644 assets/svg/ic_delete_group.svg create mode 100644 lib/desktop_screens_new/groups_screen/desktop_add_or_remove_contacts_screen.dart diff --git a/assets/svg/ic_delete_group.svg b/assets/svg/ic_delete_group.svg new file mode 100644 index 00000000..4187677c --- /dev/null +++ b/assets/svg/ic_delete_group.svg @@ -0,0 +1,3 @@ + + + diff --git a/lib/desktop_routes/desktop_route_names.dart b/lib/desktop_routes/desktop_route_names.dart index 4d0fd7b2..deb0294a 100644 --- a/lib/desktop_routes/desktop_route_names.dart +++ b/lib/desktop_routes/desktop_route_names.dart @@ -27,4 +27,6 @@ class DesktopRoutes { 'desktop_group_right_initial'; static const String DESKTOP_NEW_GROUP = 'desktop_new_group'; static const String DESKTOP_GROUP_DETAIL = 'desktop_group_detail'; + static const String DESKTOP_ADD_OR_REMOVE_CONTACTS = + 'desktop_add_or_remove_contacts'; } diff --git a/lib/desktop_routes/desktop_routes.dart b/lib/desktop_routes/desktop_routes.dart index c7327718..8f736176 100644 --- a/lib/desktop_routes/desktop_routes.dart +++ b/lib/desktop_routes/desktop_routes.dart @@ -5,6 +5,7 @@ import 'package:atsign_atmosphere_pro/desktop_screens/desktop_download_all_files import 'package:atsign_atmosphere_pro/desktop_screens/desktop_home/desktop_home.dart'; import 'package:atsign_atmosphere_pro/desktop_screens/trusted_sender/desktop_empty_trusted_sender.dart'; import 'package:atsign_atmosphere_pro/desktop_screens_new/contacts_screen/desktop_contact_screen.dart'; +import 'package:atsign_atmosphere_pro/desktop_screens_new/groups_screen/desktop_add_or_remove_contacts_screen.dart'; import 'package:atsign_atmosphere_pro/desktop_screens_new/groups_screen/desktop_groups_screen.dart'; import 'package:atsign_atmosphere_pro/desktop_screens_new/settings_screen/blocked_contacts.dart'; import 'package:atsign_atmosphere_pro/desktop_screens_new/settings_screen/settings_desktop.dart'; @@ -71,6 +72,9 @@ class DesktopSetupRoutes { DesktopRoutes.DEKSTOP_CONTACTS_SCREEN: (context) { return DesktopContactScreen(); }, + DesktopRoutes.DESKTOP_ADD_OR_REMOVE_CONTACTS: (context) { + return DesktopAddOrRemoveContactsScreen(); + }, DesktopRoutes.DESKTOP_DOWNLOAD_ALL: (context) { return DesktopDownloadAllFiles(); }, diff --git a/lib/desktop_screens_new/groups_screen/desktop_add_or_remove_contacts_screen.dart b/lib/desktop_screens_new/groups_screen/desktop_add_or_remove_contacts_screen.dart new file mode 100644 index 00000000..2b6e34ba --- /dev/null +++ b/lib/desktop_screens_new/groups_screen/desktop_add_or_remove_contacts_screen.dart @@ -0,0 +1,309 @@ +import 'dart:typed_data'; + +import 'package:at_contact/at_contact.dart'; +import 'package:at_contacts_group_flutter/models/group_contacts_model.dart'; +import 'package:at_contacts_group_flutter/services/group_service.dart'; +import 'package:at_contacts_group_flutter/utils/text_constants.dart'; +import 'package:at_contacts_group_flutter/widgets/confirmation_dialog.dart'; +import 'package:at_onboarding_flutter/at_onboarding_flutter.dart'; +import 'package:atsign_atmosphere_pro/data_models/enums/group_card_state.dart'; +import 'package:atsign_atmosphere_pro/desktop_routes/desktop_route_names.dart'; +import 'package:atsign_atmosphere_pro/desktop_routes/desktop_routes.dart'; +import 'package:atsign_atmosphere_pro/desktop_screens_new/groups_screen/widgets/desktop_cover_image_picker.dart'; +import 'package:atsign_atmosphere_pro/desktop_screens_new/groups_screen/widgets/desktop_group_contacts_list.dart'; +import 'package:atsign_atmosphere_pro/screens/common_widgets/custom_toast.dart'; +import 'package:atsign_atmosphere_pro/utils/colors.dart'; +import 'package:atsign_atmosphere_pro/utils/text_styles.dart'; +import 'package:atsign_atmosphere_pro/utils/vectors.dart'; +import 'package:atsign_atmosphere_pro/view_models/desktop_groups_screen_provider.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:provider/provider.dart'; + +class DesktopAddOrRemoveContactsScreen extends StatefulWidget { + const DesktopAddOrRemoveContactsScreen(); + + @override + State createState() => + _DesktopAddOrRemoveContactsScreenState(); +} + +class _DesktopAddOrRemoveContactsScreenState + extends State { + final _groupService = GroupService(); + late DesktopGroupsScreenProvider _groupsProvider = + context.read(); + bool isLoading = false; + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_) async { + await _groupService.fetchGroupsAndContacts(isDesktop: true); + + _groupService.selectedGroupContacts = + _groupsProvider.selectedAtGroup?.members + ?.map( + (e) => GroupContactsModel( + contact: e, + contactType: ContactsType.CONTACT, + ), + ) + .toList() ?? + []; + _groupService.selectedContactsSink + .add(_groupService.selectedGroupContacts); + }); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.white, + body: Row( + children: [ + Expanded( + flex: 572, + child: buildHalfLeftWidget(), + ), + Expanded( + flex: 441, + child: buildHalfRightWidget(), + ), + ], + ), + ); + } + + Widget buildHalfLeftWidget() { + return Container( + decoration: BoxDecoration( + color: ColorConstants.background, + borderRadius: BorderRadius.horizontal( + right: Radius.circular(47), + ), + ), + child: ListView( + children: [ + SizedBox(height: 40), + Padding( + padding: const EdgeInsets.only(left: 60), + child: buildAppbar(), + ), + SizedBox(height: 20), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 72), + child: DesktopGroupContactsList( + asSelectionScreen: true, + initialData: _groupsProvider.selectedAtGroup?.members + ?.map( + (e) => GroupContactsModel( + contact: e, + contactType: ContactsType.CONTACT, + ), + ) + .toList(), + ), + ), + ], + ), + ); + } + + Widget buildAppbar() { + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + InkWell( + onTap: () async { + await DesktopSetupRoutes.nested_push( + DesktopRoutes.DESKTOP_GROUP, + ); + }, + child: SizedBox( + width: 24, + height: 24, + child: Center( + child: SvgPicture.asset( + AppVectors.icBack, + width: 8, + height: 16, + fit: BoxFit.cover, + ), + ), + ), + ), + SizedBox(width: 28), + Text( + 'Add or Remove Contacts', + style: CustomTextStyles.blackW50020, + ) + ], + ); + } + + Widget buildHalfRightWidget() { + return Stack( + children: [ + ListView( + padding: const EdgeInsets.fromLTRB(56, 40, 24, 0), + children: [ + Text( + _groupsProvider.selectedGroupName ?? '', + style: CustomTextStyles.blackW50020, + ), + SizedBox(height: 28), + DesktopCoverImagePicker( + selectedImage: _groupsProvider.selectedGroupImage, + isEdit: false, + ), + SizedBox(height: 52), + StreamBuilder>( + stream: _groupService.selectedContactsStream, + initialData: _groupsProvider.selectedAtGroup?.members + ?.map( + (e) => GroupContactsModel( + contact: e, + contactType: ContactsType.CONTACT, + ), + ) + .toList(), + builder: (context, snapshot) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + Center( + child: Text( + '${snapshot.data?.length} ${(snapshot.data?.length ?? 0) > 1 ? 'Members' : 'Member'}', + style: CustomTextStyles.raisinBlackW50015, + ), + ), + const SizedBox(height: 28), + DesktopGroupContactsList( + showMembersOnly: true, + asSelectionScreen: false, + initialData: snapshot.data, + showSelectedBorder: false, + ), + ], + ); + }, + ), + ], + ), + Positioned( + bottom: 0, + left: 0, + right: 0, + child: Container( + color: Colors.white, + padding: const EdgeInsets.fromLTRB(56, 16, 24, 52), + child: InkWell( + onTap: () async { + await updateMembers(context); + }, + child: Container( + alignment: Alignment.center, + padding: EdgeInsets.symmetric(vertical: 16), + decoration: BoxDecoration( + color: ColorConstants.orange, + borderRadius: BorderRadius.circular(135), + ), + child: isLoading + ? SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator( + color: Colors.white, + ), + ) + : Text( + 'Update Group', + style: CustomTextStyles.whiteBold16, + ), + ), + ), + ), + ), + ], + ); + } + + Future updateMembers(BuildContext context) async { + if (isLoading) return; + setState(() { + isLoading = true; + }); + + if (_groupService.selectedGroupContacts.isEmpty) { + await showMyDialog(context, _groupsProvider.selectedAtGroup!); + } else { + AtGroup group = _groupsProvider.selectedAtGroup!; + group.members = _groupService.selectedGroupContacts + .where((e) => + e!.contactType == ContactsType.CONTACT && e.contact != null) + .map((e) => e!.contact!) + .toSet(); + final result = await GroupService().updateGroup(group); + if (result is AtGroup) { + _groupService.selectedGroupContacts = []; + await _groupsProvider.setSelectedAtGroup(result); + await _groupService.fetchGroupsAndContacts(isDesktop: true); + await DesktopSetupRoutes.nested_push( + DesktopRoutes.DESKTOP_GROUP, + ); + } else if (result != null) { + if (result.runtimeType == AlreadyExistsException) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(TextConstants().GROUP_ALREADY_EXISTS))); + } else if (result.runtimeType == InvalidAtSignException) { + ScaffoldMessenger.of(context) + .showSnackBar(SnackBar(content: Text(result.content))); + } else { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(TextConstants().SERVICE_ERROR))); + } + } else { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(TextConstants().SERVICE_ERROR))); + } + } + setState(() { + isLoading = false; + }); + } + + Future showMyDialog(BuildContext context, AtGroup group) async { + return showDialog( + context: context, + barrierDismissible: true, + builder: (BuildContext context) { + Uint8List? groupPicture; + if (group.groupPicture != null) { + List intList = group.groupPicture.cast(); + groupPicture = Uint8List.fromList(intList); + } + return ConfirmationDialog( + title: '${group.displayName}', + heading: 'Are you sure you want to delete this group?', + onYesPressed: () async { + var result = await GroupService().deleteGroup(group); + + if (!mounted) return; + if (result != null && result) { + Navigator.pop(context); + await _groupsProvider.setSelectedAtGroup(null); + _groupsProvider.setGroupCardState(GroupCardState.disable); + await DesktopSetupRoutes.nested_push( + DesktopRoutes.DESKTOP_GROUP, + ); + } else { + CustomToast().show(TextConstants().SERVICE_ERROR, context); + } + }, + image: groupPicture, + ); + }, + ); + } +} diff --git a/lib/desktop_screens_new/groups_screen/widgets/desktop_cover_image_picker.dart b/lib/desktop_screens_new/groups_screen/widgets/desktop_cover_image_picker.dart index 3e275f7c..963c80f5 100644 --- a/lib/desktop_screens_new/groups_screen/widgets/desktop_cover_image_picker.dart +++ b/lib/desktop_screens_new/groups_screen/widgets/desktop_cover_image_picker.dart @@ -7,16 +7,16 @@ import 'package:flutter_svg/flutter_svg.dart'; class DesktopCoverImagePicker extends StatelessWidget { final Uint8List? selectedImage; - final Function() onPickImage; + final Function()? onPickImage; final bool isEdit; - final Function() onCancel; + final Function()? onCancel; const DesktopCoverImagePicker({ Key? key, this.selectedImage, - required this.onPickImage, + this.onPickImage, required this.isEdit, - required this.onCancel, + this.onCancel, }) : super(key: key); @override @@ -24,14 +24,13 @@ class DesktopCoverImagePicker extends StatelessWidget { return InkWell( onTap: () async { if (isEdit) { - onPickImage.call(); + onPickImage?.call(); } }, - child: selectedImage != null && selectedImage!.isNotEmpty - ? SizedBox( - width: 360, - height: 88, - child: Stack( + child: SizedBox( + height: 120, + child: selectedImage != null && selectedImage!.isNotEmpty + ? Stack( alignment: Alignment.center, fit: StackFit.expand, children: [ @@ -84,30 +83,30 @@ class DesktopCoverImagePicker extends StatelessWidget { ), ), ], + ) + : Container( + alignment: Alignment.center, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(5), + color: ColorConstants.pickerBackgroundColor, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + 'Insert Cover Image', + style: CustomTextStyles.orangeW50014, + ), + const SizedBox(height: 8), + SvgPicture.asset( + AppVectors.icDesktopImage, + width: 48, + height: 32, + ), + ], + ), ), - ) - : Container( - padding: const EdgeInsets.only(top: 12, bottom: 16), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(5), - color: ColorConstants.pickerBackgroundColor, - ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - 'Insert Cover Image', - style: CustomTextStyles.orangeW50014, - ), - const SizedBox(height: 8), - SvgPicture.asset( - AppVectors.icDesktopImage, - width: 48, - height: 32, - ), - ], - ), - ), + ), ); } } diff --git a/lib/desktop_screens_new/groups_screen/widgets/desktop_custom_list_tile.dart b/lib/desktop_screens_new/groups_screen/widgets/desktop_custom_list_tile.dart index 9ca9f6fe..351cd38a 100644 --- a/lib/desktop_screens_new/groups_screen/widgets/desktop_custom_list_tile.dart +++ b/lib/desktop_screens_new/groups_screen/widgets/desktop_custom_list_tile.dart @@ -32,6 +32,8 @@ class DesktopCustomListTile extends StatefulWidget { final bool selectSingle; final ValueChanged>? selectedList; final bool isTrusted; + final List selectedContact; + final bool showSelectedBorder; const DesktopCustomListTile({ Key? key, @@ -42,6 +44,8 @@ class DesktopCustomListTile extends StatefulWidget { this.selectSingle = false, this.selectedList, required this.isTrusted, + this.selectedContact = const [], + required this.showSelectedBorder, }) : super(key: key); @override @@ -63,7 +67,7 @@ class _DesktopCustomListTileState extends State { _groupService = GroupService(); // ignore: omit_local_variable_types - getIsSelectedValue(_groupService.selectedGroupContacts); + getIsSelectedValue(widget.selectedContact); super.initState(); } @@ -113,110 +117,104 @@ class _DesktopCustomListTileState extends State { Widget build(BuildContext context) { getNameAndImage(); - return StreamBuilder>( - initialData: _groupService.selectedGroupContacts, - stream: _groupService.selectedContactsStream, - builder: (context, snapshot) { - getIsSelectedValue(_groupService.selectedGroupContacts); - - return InkWell( - onTap: () { - if (widget.asSelectionTile) { - if (widget.selectSingle) { - _groupService.selectedGroupContacts = []; + return InkWell( + onTap: () { + if (widget.asSelectionTile) { + if (widget.selectSingle) { + _groupService.selectedGroupContacts = []; + _groupService.addGroupContact(widget.item); + widget.selectedList!([widget.item]); + Navigator.pop(context); + } else if (!widget.selectSingle) { + if (mounted) { + setState(() { + if (isSelected) { + _groupService.removeGroupContact(widget.item); + } else { _groupService.addGroupContact(widget.item); - widget.selectedList!([widget.item]); - Navigator.pop(context); - } else if (!widget.selectSingle) { - if (mounted) { - setState(() { - if (isSelected) { - _groupService.removeGroupContact(widget.item); - } else { - _groupService.addGroupContact(widget.item); - } - isSelected = !isSelected; - }); - } } - } else { - widget.onTap!(); - } - }, - child: Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(12), - border: Border.all( - color: - isSelected ? ColorConstants.orange : Colors.transparent, - width: 2, - ), - ), - child: Row( - children: [ - (isLoading) - ? const CircularProgressIndicator() - : ClipRRect( - borderRadius: const BorderRadius.only( - topLeft: Radius.circular(10), - bottomLeft: Radius.circular(10), - ), - child: (image != null) - ? Image.memory( - image!, - width: 72, - height: 72, - fit: BoxFit.cover, - ) - : ContactInitial( - size: 72, - borderRadius: 0, - initials: (initials ?? 'UG'), - ), - ), - const SizedBox(width: 16), - Expanded( - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 20), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - widget.item?.contact?.atSign ?? - '${widget.item?.group?.members?.length} Members', - style: TextStyle( - color: Colors.black, - fontSize: 13.toFont, - fontWeight: FontWeight.w600, - ), - ), - if ((widget.item?.contact?.tags?["nickname"] ?? '') - .isNotEmpty) - Text( - widget.item?.contact?.tags?["nickname"], - style: TextStyle( - color: Colors.black, - fontSize: 10.toFont, - fontWeight: FontWeight.w400, - ), - ), - ], - ), + isSelected = !isSelected; + }); + } + } + } else { + widget.onTap!(); + } + }, + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: isSelected && widget.showSelectedBorder + ? ColorConstants.orange + : Colors.transparent, + width: 2, + ), + ), + child: Row( + children: [ + (isLoading) + ? const CircularProgressIndicator() + : ClipRRect( + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(10), + bottomLeft: Radius.circular(10), ), + child: (image != null) + ? Image.memory( + image!, + width: 72, + height: 72, + fit: BoxFit.cover, + ) + : ContactInitial( + size: 72, + borderRadius: 0, + initials: (initials ?? 'UG'), + ), ), - if (widget.isTrusted) - SvgPicture.asset( - AppVectors.icTrust, - width: 24, - height: 20, - color: ColorConstants.orange, + const SizedBox(width: 16), + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 20), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + widget.item?.contact?.atSign ?? + '${widget.item?.group?.members?.length} Members', + style: TextStyle( + color: Colors.black, + fontSize: 13.toFont, + fontWeight: FontWeight.w600, + ), ), - const SizedBox(width: 24), - ], + if ((widget.item?.contact?.tags?["nickname"] ?? '') + .isNotEmpty) + Text( + widget.item?.contact?.tags?["nickname"], + style: TextStyle( + color: Colors.black, + fontSize: 10.toFont, + fontWeight: FontWeight.w400, + ), + ), + ], + ), ), ), - ); - }); + if (widget.isTrusted) + SvgPicture.asset( + AppVectors.icTrust, + width: 24, + height: 20, + color: ColorConstants.orange, + ), + const SizedBox(width: 24), + ], + ), + ), + ); } } diff --git a/lib/desktop_screens_new/groups_screen/widgets/desktop_group_contacts_list.dart b/lib/desktop_screens_new/groups_screen/widgets/desktop_group_contacts_list.dart index e5c97509..efbfabe3 100644 --- a/lib/desktop_screens_new/groups_screen/widgets/desktop_group_contacts_list.dart +++ b/lib/desktop_screens_new/groups_screen/widgets/desktop_group_contacts_list.dart @@ -5,7 +5,6 @@ import 'package:at_contacts_flutter/utils/text_strings.dart'; import 'package:at_contacts_group_flutter/models/group_contacts_model.dart'; import 'package:at_contacts_group_flutter/services/group_service.dart'; import 'package:at_contacts_group_flutter/widgets/add_contacts_group_dialog.dart'; -import 'package:at_contacts_group_flutter/widgets/horizontal_circular_list.dart'; import 'package:atsign_atmosphere_pro/desktop_screens_new/groups_screen/widgets/desktop_custom_list_tile.dart'; import 'package:atsign_atmosphere_pro/desktop_screens_new/groups_screen/widgets/icon_button_widget.dart'; import 'package:atsign_atmosphere_pro/utils/colors.dart'; @@ -13,6 +12,7 @@ import 'package:atsign_atmosphere_pro/utils/text_styles.dart'; import 'package:atsign_atmosphere_pro/utils/vectors.dart'; import 'package:atsign_atmosphere_pro/view_models/desktop_groups_screen_provider.dart'; import 'package:atsign_atmosphere_pro/view_models/trusted_sender_view_model.dart'; +import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_slidable/flutter_slidable.dart'; import 'package:flutter_svg/flutter_svg.dart'; @@ -20,16 +20,16 @@ import 'package:provider/provider.dart'; class DesktopGroupContactsList extends StatefulWidget { final bool asSelectionScreen; - final bool singleSelection; - final bool showContacts; final List? initialData; + final bool showMembersOnly; + final bool showSelectedBorder; const DesktopGroupContactsList({ Key? key, this.asSelectionScreen = false, - this.singleSelection = false, - this.showContacts = true, this.initialData, + this.showMembersOnly = false, + this.showSelectedBorder = true, }) : super(key: key); @override @@ -51,11 +51,12 @@ class _DesktopGroupContactsListState extends State { groupProvider = context.read(); trustedProvider = context.read(); searchController = TextEditingController(); - WidgetsBinding.instance.addPostFrameCallback((_) { + WidgetsBinding.instance.addPostFrameCallback((_) async { groupProvider.setSearchContactText(''); if (groupProvider.showTrustedContacts) { groupProvider.setShowTrustedContacts(); } + _groupService.selectedContactsSink.add(widget.initialData ?? []); }); super.initState(); } @@ -66,8 +67,6 @@ class _DesktopGroupContactsListState extends State { @override void dispose() { - _groupService.selectedGroupContacts = []; - _groupService.selectedContactsSink.add(_groupService.selectedGroupContacts); super.dispose(); } @@ -77,33 +76,29 @@ class _DesktopGroupContactsListState extends State { builder: (context, provider, child) { return Column( children: [ - Row( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Expanded( - child: buildSearchField(), - ), - const SizedBox(width: 12), - IconButtonWidget( - icon: AppVectors.icTrust, - backgroundColor: ColorConstants.iconButtonColor, - isSelected: provider.showTrustedContacts, - onTap: () { - provider.setShowTrustedContacts(); - }, - ) - ], - ), - const SizedBox(height: 20), - widget.asSelectionScreen ? HorizontalCircularList() : Container(), - (widget.initialData ?? []).isNotEmpty - ? buildContactsList(provider.showTrustedContacts - ? widget.initialData - ?.where((e) => trustedProvider.trustedContacts.any( - (element) => element.atSign == e?.contact?.atSign)) - .toList() - : widget.initialData) + if (!widget.showMembersOnly) ...[ + Row( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Expanded( + child: buildSearchField(), + ), + const SizedBox(width: 12), + IconButtonWidget( + icon: AppVectors.icTrust, + backgroundColor: ColorConstants.iconButtonColor, + isSelected: provider.showTrustedContacts, + onTap: () { + provider.setShowTrustedContacts(); + }, + ) + ], + ), + const SizedBox(height: 20), + ], + !widget.asSelectionScreen + ? buildContactsList(widget.initialData) : StreamBuilder>( stream: _groupService.allContactsStream, initialData: _groupService.allContacts, @@ -140,14 +135,16 @@ class _DesktopGroupContactsListState extends State { ], ); } else { - return buildContactsList(provider.showTrustedContacts - ? trustedProvider.trustedContacts - .map((e) => GroupContactsModel( - contact: e, - contactType: ContactsType.CONTACT, - )) - .toList() - : snapshot.data); + return buildContactsList( + provider.showTrustedContacts + ? trustedProvider.trustedContacts + .map((e) => GroupContactsModel( + contact: e, + contactType: ContactsType.CONTACT, + )) + .toList() + : snapshot.data, + ); } } }) @@ -160,6 +157,7 @@ class _DesktopGroupContactsListState extends State { // filtering contacts and groups var _filteredList = []; _filteredList = getAllContactList(data ?? []); + bool isFirst = true; if (_filteredList.isEmpty) { return Center( @@ -175,7 +173,6 @@ class _DesktopGroupContactsListState extends State { // renders contacts according to the initial alphabet return ListView.builder( - padding: EdgeInsets.only(bottom: 80.toHeight), itemCount: 27, shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), @@ -200,31 +197,91 @@ class _DesktopGroupContactsListState extends State { } if (contactsForAlphabet.isEmpty) { - return Container(); + Widget separator = SizedBox(); + if (widget.showMembersOnly && + getContactsForAlphabets( + _filteredList, + String.fromCharCode(alphabetIndex + 1 + 65).toUpperCase(), + alphabetIndex + 1, + ).isNotEmpty) { + if (isFirst && + getContactsForAlphabets( + _filteredList, + String.fromCharCode(alphabetIndex -1 + 65).toUpperCase(), + alphabetIndex - 1, + ).isNotEmpty) { + isFirst = false; + } else { + separator = Padding( + padding: const EdgeInsets.symmetric(vertical: 12), + child: Divider( + color: ColorConstants.boxGrey, + thickness: 1.toHeight, + height: 0, + ), + ); + } + } else if (!widget.showMembersOnly && + getContactsForAlphabets( + _filteredList, + String.fromCharCode(alphabetIndex + 1 + 65).toUpperCase(), + alphabetIndex + 1, + ).isNotEmpty) { + if (isFirst && + getContactsForAlphabets( + _filteredList, + String.fromCharCode(alphabetIndex -1 + 65).toUpperCase(), + alphabetIndex - 1, + ).isNotEmpty) { + isFirst = false; + } else { + separator = SizedBox(height: 16); + } + } + return separator; } return Column( + mainAxisSize: MainAxisSize.min, children: [ - Row( - children: [ - Text( - currentChar, - style: TextStyle( - color: ColorConstants.darkSliver, - fontSize: 20.toFont, - fontWeight: FontWeight.bold, + if (!widget.showMembersOnly) ...[ + Row( + children: [ + Text( + currentChar, + style: TextStyle( + color: ColorConstants.darkSliver, + fontSize: 20.toFont, + fontWeight: FontWeight.bold, + ), ), - ), - SizedBox(width: 16.toWidth), - Expanded( - child: Divider( - color: ColorConstants.boxGrey, - height: 1.toHeight, + SizedBox(width: 16.toWidth), + Expanded( + child: Divider( + color: ColorConstants.boxGrey, + thickness: 1.toHeight, + height: 0, + ), ), + ], + ), + SizedBox(height: 16), + ], + contactListBuilder(contactsForAlphabet), + if (widget.showMembersOnly && + getContactsForAlphabets( + _filteredList, + String.fromCharCode(alphabetIndex + 1 + 65).toUpperCase(), + alphabetIndex + 1, + ).isNotEmpty) + Padding( + padding: const EdgeInsets.symmetric(vertical: 12), + child: Divider( + color: ColorConstants.boxGrey, + thickness: 1.toHeight, + height: 0, ), - ], - ), - contactListBuilder(contactsForAlphabet) + ), ], ); }, @@ -324,47 +381,83 @@ class _DesktopGroupContactsListState extends State { List contactsForAlphabet, ) { return ListView.separated( - padding: const EdgeInsets.symmetric(horizontal: 28), + padding: EdgeInsets.symmetric( + horizontal: widget.showMembersOnly ? 0 : 28, + ), itemCount: contactsForAlphabet.length, physics: const NeverScrollableScrollPhysics(), shrinkWrap: true, - separatorBuilder: (context, _) => Divider( - color: ColorConstants.dividerColor, - height: 1.toHeight, + separatorBuilder: (context, _) => Padding( + padding: const EdgeInsets.symmetric(vertical: 12), + child: Divider( + color: ColorConstants.boxGrey, + thickness: 1.toHeight, + height: 0, + ), ), itemBuilder: (context, index) { - return Padding( - padding: const EdgeInsets.only( - right: 24, - bottom: 12, - top: 12, - ), - child: (contactsForAlphabet[index]!.contact != null) - ? Slidable( - endActionPane: ActionPane( - motion: const ScrollMotion(), - extentRatio: 0.25, - children: [ - SlidableAction( - label: TextStrings().block, - backgroundColor: ColorConstants.inputFieldColor, - icon: Icons.block, - onPressed: (context) async { - blockUnblockContact( - contactsForAlphabet[index]!.contact!); + return (contactsForAlphabet[index]!.contact != null) + ? Slidable( + endActionPane: ActionPane( + motion: const ScrollMotion(), + extentRatio: 0.25, + children: [ + SlidableAction( + label: TextStrings().block, + backgroundColor: ColorConstants.inputFieldColor, + icon: Icons.block, + onPressed: (context) async { + blockUnblockContact( + contactsForAlphabet[index]!.contact!); + }, + ), + SlidableAction( + label: TextStrings().delete, + backgroundColor: Colors.red, + icon: Icons.delete, + onPressed: (context) async { + deleteAtSign(contactsForAlphabet[index]!.contact!); + }, + ), + ], + ), + child: StreamBuilder>( + initialData: widget.initialData, + stream: _groupService.selectedContactsStream, + builder: (context, snapshot) { + return DesktopCustomListTile( + key: UniqueKey(), + onTap: () {}, + asSelectionTile: widget.asSelectionScreen, + selectSingle: false, + item: contactsForAlphabet[index], + selectedList: (s) { + setSelectedContactsList(s); }, - ), - SlidableAction( - label: TextStrings().delete, - backgroundColor: Colors.red, - icon: Icons.delete, - onPressed: (context) async { - deleteAtSign(contactsForAlphabet[index]!.contact!); + selectedContact: snapshot.data ?? [], + onTrailingPressed: () { + if (contactsForAlphabet[index]!.contact != null) { + Navigator.pop(context); + + _groupService + .addGroupContact(contactsForAlphabet[index]); + setSelectedContactsList( + _groupService.selectedGroupContacts); + } }, - ), - ], - ), - child: DesktopCustomListTile( + isTrusted: trustedProvider.trustedContacts.any( + (element) => + element.atSign == + contactsForAlphabet[index]?.contact?.atSign), + showSelectedBorder: widget.showSelectedBorder, + ); + }), + ) + : StreamBuilder>( + initialData: widget.initialData, + stream: _groupService.selectedContactsStream, + builder: (context, snapshot) { + return DesktopCustomListTile( key: UniqueKey(), onTap: () {}, asSelectionTile: widget.asSelectionScreen, @@ -373,8 +466,9 @@ class _DesktopGroupContactsListState extends State { selectedList: (s) { setSelectedContactsList(s); }, + selectedContact: snapshot.data ?? [], onTrailingPressed: () { - if (contactsForAlphabet[index]!.contact != null) { + if (contactsForAlphabet[index]!.group != null) { Navigator.pop(context); _groupService @@ -386,31 +480,9 @@ class _DesktopGroupContactsListState extends State { isTrusted: trustedProvider.trustedContacts.any((element) => element.atSign == contactsForAlphabet[index]?.contact?.atSign), - ), - ) - : DesktopCustomListTile( - key: UniqueKey(), - onTap: () {}, - asSelectionTile: widget.asSelectionScreen, - selectSingle: false, - item: contactsForAlphabet[index], - selectedList: (s) { - setSelectedContactsList(s); - }, - onTrailingPressed: () { - if (contactsForAlphabet[index]!.group != null) { - Navigator.pop(context); - - _groupService.addGroupContact(contactsForAlphabet[index]); - setSelectedContactsList( - _groupService.selectedGroupContacts); - } - }, - isTrusted: trustedProvider.trustedContacts.any((element) => - element.atSign == - contactsForAlphabet[index]?.contact?.atSign), - ), - ); + showSelectedBorder: widget.showSelectedBorder, + ); + }); }, ); } diff --git a/lib/desktop_screens_new/groups_screen/widgets/desktop_groups_detail.dart b/lib/desktop_screens_new/groups_screen/widgets/desktop_groups_detail.dart index 8cbb8b56..f82d0d6c 100644 --- a/lib/desktop_screens_new/groups_screen/widgets/desktop_groups_detail.dart +++ b/lib/desktop_screens_new/groups_screen/widgets/desktop_groups_detail.dart @@ -10,13 +10,12 @@ import 'package:at_contacts_group_flutter/utils/text_constants.dart'; import 'package:at_contacts_group_flutter/widgets/confirmation_dialog.dart'; import 'package:at_contacts_group_flutter/widgets/custom_toast.dart'; import 'package:atsign_atmosphere_pro/data_models/enums/group_card_state.dart'; +import 'package:atsign_atmosphere_pro/desktop_routes/desktop_route_names.dart'; import 'package:atsign_atmosphere_pro/desktop_routes/desktop_routes.dart'; import 'package:atsign_atmosphere_pro/desktop_screens_new/groups_screen/widgets/desktop_cover_image_picker.dart'; import 'package:atsign_atmosphere_pro/desktop_screens_new/groups_screen/widgets/desktop_custom_app_bar.dart'; -import 'package:atsign_atmosphere_pro/desktop_screens_new/groups_screen/widgets/desktop_floating_add_contact_button.dart'; import 'package:atsign_atmosphere_pro/desktop_screens_new/groups_screen/widgets/desktop_group_contacts_list.dart'; import 'package:atsign_atmosphere_pro/desktop_screens_new/groups_screen/widgets/desktop_group_name_text_field.dart'; -import 'package:atsign_atmosphere_pro/desktop_screens_new/groups_screen/widgets/icon_button_widget.dart'; import 'package:atsign_atmosphere_pro/services/picker_service.dart'; import 'package:atsign_atmosphere_pro/services/snackbar_service.dart'; import 'package:atsign_atmosphere_pro/utils/colors.dart'; @@ -100,41 +99,55 @@ class _DesktopGroupsDetailState extends State { resetGroupName(); } : () => widget.onBackArrowTap(false), - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 20), - child: SvgPicture.asset( - AppVectors.icBack, - width: 8, - height: 20, + child: SizedBox( + height: 24, + width: 24, + child: Center( + child: SvgPicture.asset( + AppVectors.icBack, + width: 8, + height: 20, + fit: BoxFit.cover, + ), ), ), ), showLeadingIcon: true, - showTrailingIcon: provider.isEditing, - trailingIcon: InkWell( - onTap: () async { - await updateGroup(); - }, - child: Container( - padding: - const EdgeInsets.symmetric(horizontal: 52, vertical: 8), - margin: const EdgeInsets.only(right: 28), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(46), - color: ColorConstants.orange, - ), - child: Text( - 'Save', - style: CustomTextStyles.whiteW50015, - ), - ), - ), + showTrailingIcon: true, + onTrailingIconPressed: provider.isEditing + ? () async { + await updateGroup(); + } + : () { + groupProvider.setShowEditOptionsStatus(); + }, + trailingIcon: provider.isEditing + ? Container( + padding: + const EdgeInsets.symmetric(horizontal: 52, vertical: 8), + margin: const EdgeInsets.only(right: 28), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(46), + color: ColorConstants.orange, + ), + child: Text( + 'Save', + style: CustomTextStyles.whiteW50015, + ), + ) + : SvgPicture.asset( + AppVectors.icOptions, + width: 16, + fit: BoxFit.fitWidth, + color: provider.showEditOptions + ? ColorConstants.orange + : Colors.black, + ), ), body: Stack( children: [ ListView( - padding: - const EdgeInsets.symmetric(horizontal: 24, vertical: 16), + padding: const EdgeInsets.fromLTRB(20, 16, 20, 132), children: [ if (provider.isEditing) ...[ DesktopGroupNameTextField( @@ -178,13 +191,21 @@ class _DesktopGroupsDetailState extends State { ), ), SizedBox(height: provider.isEditing ? 16 : 20), - buildDetailOptions( - isAddingContacts: provider.isAddingContacts, - isEditing: provider.isEditing, - ), - const SizedBox(height: 12), + if (!provider.isEditing) buildTransferFileButton(), + const SizedBox(height: 36), + provider.isEditing + ? SizedBox(height: 8) + : Center( + child: Text( + '${provider.selectedAtGroup?.members?.length} ${(provider.selectedAtGroup?.members?.length ?? 0) > 1 ? 'Members' : 'Member'}', + style: CustomTextStyles.raisinBlackW50015, + ), + ), + const SizedBox(height: 28), DesktopGroupContactsList( - asSelectionScreen: provider.isAddingContacts, + asSelectionScreen: false, + showMembersOnly: !provider.isEditing, + showSelectedBorder: provider.isEditing, initialData: provider.isAddingContacts ? [] : provider.selectedAtGroup?.members @@ -195,8 +216,32 @@ class _DesktopGroupsDetailState extends State { ), ], ), - if (provider.isAddingContacts) - const DesktopFloatingAddContactButton(), + if (!provider.isEditing) + Positioned( + bottom: 0, + left: 0, + right: 0, + child: Container( + color: Colors.white, + padding: const EdgeInsets.fromLTRB(28, 16, 28, 52), + child: buildEditMembers(), + ), + ), + if (provider.showEditOptions) ...[ + Positioned.fill( + child: InkWell( + onTap: () { + groupProvider.setShowEditOptionsStatus(); + }, + ), + ), + Positioned( + top: 8, + left: 20, + right: 20, + child: buildEditOptionsDialog(), + ), + ], ], ), ), @@ -204,111 +249,152 @@ class _DesktopGroupsDetailState extends State { }); } - Widget buildDetailOptions({ - required bool isEditing, - required bool isAddingContacts, - }) { - return isEditing - ? Row( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - IconButtonWidget( - icon: AppVectors.icDesktopAdd, - isSelected: isAddingContacts, - onTap: () { - groupProvider.setIsAddingContact(); - }, - backgroundColor: ColorConstants.iconButtonColor, - ), - const SizedBox(width: 20), - IconButtonWidget( - icon: AppVectors.icShare, - padding: - const EdgeInsets.symmetric(vertical: 12, horizontal: 8), - onTap: () {}, - backgroundColor: ColorConstants.iconButtonColor, - ), - const SizedBox(width: 20), - IconButtonWidget( - icon: AppVectors.icDelete, - onTap: () async { - await showMyDialog( - context, - groupProvider.selectedAtGroup!, - ); - }, - backgroundColor: ColorConstants.iconButtonColor, - ), - ], - ) - : Column( - mainAxisSize: MainAxisSize.min, - children: [ - InkWell( - onTap: () async { - Provider.of(context, listen: false) - .selectedContacts = [ - GroupContactsModel( - group: groupProvider.selectedAtGroup, - contactType: ContactsType.GROUP, - ), - ]; - Provider.of(context, listen: false) - .notify(); - await DesktopSetupRoutes.nested_pop(); - }, - child: Container( - padding: const EdgeInsets.symmetric(vertical: 12), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(5), - color: Colors.black, - ), - child: Center( - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - "Transfer File", - style: CustomTextStyles.whiteBold12, - ), - const SizedBox(width: 12), - SvgPicture.asset( - AppVectors.icTransfer, - width: 16, - height: 12, - fit: BoxFit.cover, - ), - ], - ), + Widget buildEditMembers() { + return InkWell( + onTap: () async { + await DesktopSetupRoutes.nested_push( + DesktopRoutes.DESKTOP_ADD_OR_REMOVE_CONTACTS, + ); + }, + child: Container( + alignment: Alignment.center, + padding: EdgeInsets.symmetric(vertical: 20), + decoration: BoxDecoration( + color: ColorConstants.editMembersButtonColor, + borderRadius: BorderRadius.circular(10), + ), + child: Text( + 'Edit Members', + style: CustomTextStyles.dimGrayW50015, + ), + ), + ); + } + + Widget buildEditOptionsDialog() { + return Container( + clipBehavior: Clip.antiAlias, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(7), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.25), + offset: Offset(0, 4), + blurRadius: 18, + ), + ], + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + InkWell( + onTap: () async { + await groupProvider.setIsEditing(true); + groupProvider.setShowEditOptionsStatus(); + }, + child: Padding( + padding: EdgeInsets.symmetric(vertical: 12), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + 'Edit Group', + style: CustomTextStyles.blackW40015, ), - ), + SizedBox(width: 12), + SvgPicture.asset( + AppVectors.icPencil, + width: 16, + height: 16, + fit: BoxFit.cover, + ) + ], ), - const SizedBox(height: 20), - Row( - mainAxisSize: MainAxisSize.min, + ), + ), + Divider( + height: 0, + color: Colors.black, + thickness: 1, + ), + InkWell( + onTap: () async { + groupProvider.setShowEditOptionsStatus(); + await showMyDialog( + context, + groupProvider.selectedAtGroup!, + ); + }, + child: Padding( + padding: EdgeInsets.symmetric(vertical: 12), + child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - IconButtonWidget( - icon: AppVectors.icEdit, - backgroundColor: ColorConstants.iconButtonColor, - onTap: () async => await groupProvider.setIsEditing(true), - ), - const SizedBox(width: 24), - IconButtonWidget( - icon: AppVectors.icDelete, - backgroundColor: ColorConstants.iconButtonColor, - onTap: () async { - await showMyDialog( - context, - groupProvider.selectedAtGroup!, - ); - }, + Text( + 'Delete Group', + style: CustomTextStyles.blackW40015, ), + SizedBox(width: 12), + SvgPicture.asset( + AppVectors.icDeleteGroup, + width: 16, + height: 16, + fit: BoxFit.cover, + ) ], ), + ), + ) + ], + ), + ); + } + + Widget buildTransferFileButton() { + return InkWell( + onTap: () async { + Provider.of(context, listen: false) + .selectedContacts = [ + GroupContactsModel( + group: groupProvider.selectedAtGroup, + contactType: ContactsType.GROUP, + ), + ]; + Provider.of(context, listen: false).notify(); + await DesktopSetupRoutes.nested_pop(); + }, + child: Container( + padding: const EdgeInsets.symmetric(vertical: 16), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10), + color: Colors.black, + ), + child: Center( + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + "Transfer File", + style: CustomTextStyles.whiteBold12, + ), + const SizedBox(width: 12), + SizedBox( + width: 20, + height: 20, + child: SvgPicture.asset( + AppVectors.icSendGroup, + width: 16, + height: 16, + fit: BoxFit.cover, + color: Colors.white, + ), + ), ], - ); + ), + ), + ), + ); } Future showMyDialog(BuildContext context, AtGroup group) async { diff --git a/lib/utils/colors.dart b/lib/utils/colors.dart index 3a3031ce..948f0d8e 100644 --- a/lib/utils/colors.dart +++ b/lib/utils/colors.dart @@ -61,6 +61,7 @@ class ColorConstants { static const Color gray2 = Color(0xFFB9B9B9); static const Color lightGray2 = Color(0xFFE3E3E3); static const Color darkGray2 = Color(0xFF474747); + static const Color dimGray = Color(0xFF696969); static const Color sidebarTextUnselected = Color(0xFFA4A4A5); static const Color sidebarTextSelected = Color(0xFF000000); @@ -106,6 +107,7 @@ class ColorConstants { static const Color disableTooltipColor = Color(0xFFC4C4C4); static const Color inactiveIconColor = Color(0xFFC5C5C5); static const Color closeButtonColor = Color(0xFFE6E6E6); + static const Color editMembersButtonColor = Color(0xFFF0F0F0); static const Color listFileShadowColor = Color(0xFFD7D7D7); static const Color spanishGray = Color(0xFF969393); } diff --git a/lib/utils/text_styles.dart b/lib/utils/text_styles.dart index 212e3ea9..506d454b 100644 --- a/lib/utils/text_styles.dart +++ b/lib/utils/text_styles.dart @@ -533,10 +533,10 @@ class CustomTextStyles { fontSize: 12, ); - static TextStyle blackW40013 = TextStyle( + static TextStyle blackW40015 = TextStyle( color: Colors.black, fontWeight: FontWeight.w400, - fontSize: 13, + fontSize: 15, ); static TextStyle blackW40017 = TextStyle( @@ -557,6 +557,12 @@ class CustomTextStyles { fontWeight: FontWeight.w400, ); + static TextStyle raisinBlackW50015 = TextStyle( + color: ColorConstants.raisinBlack, + fontSize: 15, + fontWeight: FontWeight.w500, + ); + static TextStyle blackW60010 = TextStyle( color: Colors.black, fontWeight: FontWeight.w600, @@ -634,4 +640,10 @@ class CustomTextStyles { fontWeight: FontWeight.w500, color: ColorConstants.grey, ); + + static TextStyle dimGrayW50015 = TextStyle( + fontSize: 15, + fontWeight: FontWeight.w500, + color: ColorConstants.dimGray, + ); } diff --git a/lib/utils/vectors.dart b/lib/utils/vectors.dart index ecae254a..b35eba3b 100644 --- a/lib/utils/vectors.dart +++ b/lib/utils/vectors.dart @@ -106,4 +106,5 @@ class AppVectors { static String icDownloadedCheck = '$_basePath/ic_downloaded_check.svg'; static String icDone = '$_basePath/ic_done.svg'; static String icUndone = '$_basePath/ic_undone.svg'; + static String icDeleteGroup = '$_basePath/ic_delete_group.svg'; } diff --git a/lib/view_models/desktop_groups_screen_provider.dart b/lib/view_models/desktop_groups_screen_provider.dart index 11da5bad..7a929123 100644 --- a/lib/view_models/desktop_groups_screen_provider.dart +++ b/lib/view_models/desktop_groups_screen_provider.dart @@ -15,6 +15,7 @@ class DesktopGroupsScreenProvider extends ChangeNotifier { bool isAddingContacts = false; Uint8List? selectedGroupImage; String? selectedGroupName; + bool showEditOptions = false; void reset() { searchGroupText = ''; @@ -23,6 +24,7 @@ class DesktopGroupsScreenProvider extends ChangeNotifier { isEditing = false; isAddingContacts = false; groupCardState = GroupCardState.disable; + showEditOptions = false; notifyListeners(); } @@ -100,4 +102,9 @@ class DesktopGroupsScreenProvider extends ChangeNotifier { notifyListeners(); } } + + void setShowEditOptionsStatus() { + showEditOptions = !showEditOptions; + notifyListeners(); + } }