TF-1194 apply new design identity to manage identity screen in mobile

This commit is contained in:
sherlockvn
2022-12-27 14:17:37 +07:00
committed by Dat H. Pham
parent 0144c2cbfa
commit 200e106c19
13 changed files with 435 additions and 176 deletions
@@ -1,22 +1,12 @@
import 'package:core/presentation/extensions/color_extension.dart';
import 'package:core/presentation/resources/image_paths.dart';
import 'package:core/presentation/state/success.dart';
import 'package:core/presentation/utils/responsive_utils.dart';
import 'package:core/presentation/views/button/button_builder.dart';
import 'package:core/presentation/views/responsive/responsive_widget.dart';
import 'package:core/core.dart';
import 'package:flutter/material.dart';
import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:get/get.dart';
import 'package:jmap_dart_client/jmap/identities/identity.dart';
import 'package:tmail_ui_user/features/base/mixin/app_loader_mixin.dart';
import 'package:tmail_ui_user/features/base/mixin/popup_menu_widget_mixin.dart';
import 'package:tmail_ui_user/features/base/widget/drop_down_button_widget.dart';
import 'package:tmail_ui_user/features/manage_account/presentation/profiles/identities/identities_controller.dart';
import 'package:tmail_ui_user/features/manage_account/presentation/profiles/identities/widgets/identity_bottom_sheet_action_tile_builder.dart';
import 'package:tmail_ui_user/features/manage_account/presentation/profiles/identities/widgets/identity_info_tile_builder.dart';
import 'package:tmail_ui_user/features/manage_account/presentation/profiles/identities/widgets/identities_radio_list_builder.dart';
import 'package:tmail_ui_user/main/localizations/app_localizations.dart';
import 'package:tmail_ui_user/features/manage_account/presentation/profiles/identities/widgets/identities_radio_list_builder_web.dart' as identities_listview_web;
class IdentitiesView extends GetWidget<IdentitiesController> with PopupMenuWidgetMixin, AppLoaderMixin {
@@ -27,177 +17,106 @@ class IdentitiesView extends GetWidget<IdentitiesController> with PopupMenuWidge
@override
Widget build(BuildContext context) {
final buttonSelectIdentity = Row(children: [
Expanded(child: Obx(() => DropDownButtonWidget<Identity>(
items: controller.listAllIdentities,
itemSelected: controller.identitySelected.value,
onChanged: (newIdentity) => controller.selectIdentity(newIdentity),
supportSelectionIcon: true))),
if (!_responsiveUtils.isMobile(context)) const SizedBox(width: 12),
if (!_responsiveUtils.isMobile(context))
(ButtonBuilder(_imagePaths.icAddIdentity)
..key(const Key('button_new_identity'))
..decoration(BoxDecoration(
borderRadius: BorderRadius.circular(10),
color: AppColor.colorTextButton))
..paddingIcon(const EdgeInsets.only(right: 8))
..iconColor(Colors.white)
..maxWidth(170)
..size(20)
..radiusSplash(10)
..padding(const EdgeInsets.symmetric(vertical: 12))
..textStyle(const TextStyle(fontSize: 17, color: Colors.white, fontWeight: FontWeight.w500))
..onPressActionClick(() => controller.goToCreateNewIdentity(context))
..text(AppLocalizations.of(context).new_identity, isVertical: false))
.build()
]);
return ResponsiveWidget(
responsiveUtils: _responsiveUtils,
mobile: _buildIdentitiesViewMobile(context),
tablet: _buildIdentitiesViewMobile(context),
desktop: _buildIdentitiesViewWeb(context),
tabletLarge: _buildIdentitiesViewMobile(context),
);
}
return LayoutBuilder(builder: (context, constraints) => ResponsiveWidget(
responsiveUtils: _responsiveUtils,
mobile: Scaffold(
body: Container(
margin: const EdgeInsets.symmetric(vertical: 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
buttonSelectIdentity,
const SizedBox(height: 24),
_buildLoadingView(),
Expanded(child: Obx(() => MasonryGridView.count(
key: const Key('list_identities'),
crossAxisSpacing: 24.0,
mainAxisSpacing: 24.0,
crossAxisCount: _responsiveUtils.isDesktop(context) || _responsiveUtils.isTabletLarge(context)
? 2 : 1,
itemCount: controller.listSelectedIdentities.length,
itemBuilder: (context, index) =>
IdentityInfoTileBuilder(_imagePaths, _responsiveUtils,
controller.listSelectedIdentities[index],
onMenuItemIdentityAction: (identity, position) =>
_openIdentityMenuAction(context, identity, position))
)))
]
)
),
floatingActionButton: _responsiveUtils.isMobile(context)
? FloatingActionButton(
key: const Key('add_new_identity'),
onPressed: () => controller.goToCreateNewIdentity(context),
backgroundColor: AppColor.primaryColor,
child: SvgPicture.asset(_imagePaths.icAddIdentity, width: 24, height: 24))
: null),
desktop: Container(
margin: const EdgeInsets.symmetric(vertical: 24, horizontal: 16),
Widget _buildIdentitiesViewMobile(BuildContext context) {
return SingleChildScrollView(
padding: const EdgeInsets.only(left: 16.0, right: 16.0, top: 24.0, bottom: 24.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
for(Widget item in _buildIdentitiesTitles(context))
item,
_buildCreateIdentityButton(context),
IdentitiesRadioListBuilder(controller: controller),
],
),
);
}
Widget _buildIdentitiesViewWeb(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 24.0, horizontal: 8.0),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
width: 236,
padding: const EdgeInsets.only(right: 12.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: constraints.maxWidth / 2,
child: buttonSelectIdentity),
const SizedBox(height: 24),
_buildLoadingView(),
Expanded(child: Obx(() => MasonryGridView.count(
key: const Key('list_identities'),
crossAxisSpacing: 24.0,
mainAxisSpacing: 24.0,
crossAxisCount: _responsiveUtils.isDesktop(context) || _responsiveUtils.isTabletLarge(context)
? 2 : 1,
itemCount: controller.listSelectedIdentities.length,
itemBuilder: (context, index) =>
IdentityInfoTileBuilder(_imagePaths, _responsiveUtils,
controller.listSelectedIdentities[index],
onMenuItemIdentityAction: (identity, position) =>
_openIdentityMenuAction(context, identity, position))
)))
]
)
)
));
crossAxisAlignment: CrossAxisAlignment.start,
children: [
for(Widget widget in _buildIdentitiesTitles(context))
widget,
_buildCreateIdentityButton(context),
],
),
),
Expanded(
child: identities_listview_web.IdentitiesRadioListBuilder(controller: controller),
),
],
),
);
}
Widget _buildLoadingView() {
return Obx(() => controller.viewState.value.fold(
(failure) => const SizedBox.shrink(),
(success) => success is LoadingState
? Padding(
padding: const EdgeInsets.only(bottom: 24),
child: loadingWidget)
: const SizedBox.shrink()
));
}
void _openIdentityMenuAction(BuildContext context, Identity identity,
RelativeRect? position) {
if (_responsiveUtils.isScreenWithShortestSide(context)) {
controller.openContextMenuAction(context, _bottomSheetIdentityActionTiles(context, identity));
} else {
controller.openPopupMenuAction(context, position, _popupMenuIdentityActionTiles(context, identity));
}
}
List<PopupMenuEntry> _popupMenuIdentityActionTiles(BuildContext context, Identity identity) {
List<Widget> _buildIdentitiesTitles(BuildContext context) {
return [
PopupMenuItem(
padding: EdgeInsets.zero,
child: popupItem(_imagePaths.icEdit,
AppLocalizations.of(context).edit_identity,
styleName: const TextStyle(
fontWeight: FontWeight.normal,
fontSize: 17,
color: Colors.black),
onCallbackAction: () => controller.goToEditIdentity(context, identity))),
if (identity.mayDelete == true)
PopupMenuItem(
padding: EdgeInsets.zero,
child: popupItem(_imagePaths.icDeleteComposer,
AppLocalizations.of(context).delete_identity,
colorIcon: AppColor.colorActionDeleteConfirmDialog,
styleName: const TextStyle(
fontWeight: FontWeight.normal,
fontSize: 17,
color: AppColor.colorActionDeleteConfirmDialog),
onCallbackAction: () => controller.openConfirmationDialogDeleteIdentityAction(context, identity))),
Text(
AppLocalizations.of(context).identities.inCaps,
style: const TextStyle(
fontWeight: FontWeight.w500,
fontSize: 17,
color: Colors.black)),
const SizedBox(height: 4.0),
Text(
AppLocalizations.of(context).identities_description,
style: const TextStyle(
fontSize: 15,
fontWeight: FontWeight.w400,
color: AppColor.colorSettingExplanation),
),
const SizedBox(height: 12.0),
];
}
List<Widget> _bottomSheetIdentityActionTiles(BuildContext context, Identity identity) {
return <Widget>[
_editIdentityActionTile(context, identity),
if (identity.mayDelete == true)
_deleteIdentityActionTile(context, identity),
];
Widget _buildCreateIdentityButton(BuildContext context) {
return Row(
children: [
Expanded(
child: Padding(
padding: const EdgeInsets.only(bottom: 12.0),
child: (ButtonBuilder(_imagePaths.icAddIdentity)
..key(const Key('button_add_identity'))
..decoration(BoxDecoration(
borderRadius: BorderRadius.circular(12),
color: AppColor.colorBorderIdentityInfo))
..paddingIcon(const EdgeInsets.only(right: 12))
..iconColor(AppColor.primaryColor)
..size(28)
..padding(const EdgeInsets.symmetric(vertical: 12))
..textStyle(const TextStyle(
fontSize: 16,
color: AppColor.primaryColor,
fontWeight: FontWeight.w500,
))
..onPressActionClick(() => controller.goToCreateNewIdentity(context))
..text(
AppLocalizations.of(context).create_new_identity,
isVertical: false,
))
.build(),
),
),
],
);
}
Widget _deleteIdentityActionTile(BuildContext context, Identity identity) {
return (IdentityBottomSheetActionTileBuilder(
const Key('delete_identity_action'),
SvgPicture.asset(_imagePaths.icDeleteComposer,
color: AppColor.colorActionDeleteConfirmDialog),
AppLocalizations.of(context).delete_identity,
identity,
iconLeftPadding: _responsiveUtils.isMobile(context)
? const EdgeInsets.only(left: 12, right: 16)
: const EdgeInsets.only(right: 12),
iconRightPadding: _responsiveUtils.isMobile(context)
? const EdgeInsets.only(right: 12)
: EdgeInsets.zero)
..onActionClick((identity) => controller.openConfirmationDialogDeleteIdentityAction(context, identity)))
.build();
}
Widget _editIdentityActionTile(BuildContext context, Identity identity) {
return (IdentityBottomSheetActionTileBuilder(
const Key('edit_identity_action'),
SvgPicture.asset(_imagePaths.icEdit),
AppLocalizations.of(context).edit_identity,
identity,
iconLeftPadding: _responsiveUtils.isMobile(context)
? const EdgeInsets.only(left: 12, right: 16)
: const EdgeInsets.only(right: 12),
iconRightPadding: _responsiveUtils.isMobile(context)
? const EdgeInsets.only(right: 12)
: EdgeInsets.zero)
..onActionClick((identity) => controller.goToEditIdentity(context, identity)))
.build();
}
}
@@ -0,0 +1,79 @@
import 'package:core/presentation/extensions/color_extension.dart';
import 'package:flutter/material.dart';
import 'package:get/get_state_manager/src/rx_flutter/rx_obx_widget.dart';
import 'package:tmail_ui_user/features/manage_account/presentation/profiles/identities/identities_controller.dart';
import 'package:tmail_ui_user/features/manage_account/presentation/profiles/identities/widgets/signature_of_identity_builder.dart';
import 'identity_list_tile_builder.dart';
class IdentitiesRadioListBuilder extends StatefulWidget {
const IdentitiesRadioListBuilder({
Key? key,
required this.controller,
}) : super(key: key);
final IdentitiesController controller;
@override
State<StatefulWidget> createState() => _IdentitiesRadioListBuilderState();
}
class _IdentitiesRadioListBuilderState extends State<IdentitiesRadioListBuilder> {
late final controller = widget.controller;
@override
Widget build(BuildContext context) {
return Obx(() {
if (controller.listAllIdentities.isEmpty) {
return const SizedBox.shrink();
}
return Container(
foregroundDecoration: BoxDecoration(
border: Border.all(color: AppColor.colorBorderIdentityInfo, width: 1),
borderRadius: const BorderRadius.all(Radius.circular(16.0)),
),
child: Obx(() => Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
key: const Key('identities_checkbox_list'),
height: 256,
foregroundDecoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [0, 0.5, 0.55, 0.60, 0.65, 0.70, 0.75, 0.80, 0.85, 0.90, 1].map((e)
=> Colors.white.withOpacity(e.toDouble())).toList(),
stops: const [0.8, 0.82, 0.84, 0.86, 0.88, 0.90, 0.92, 0.94, 0.96, 0.98, 1],// Gradient from https://learnui.design/tools/gradient-generator.html
tileMode: TileMode.mirror,
),
border: controller.isSignatureShow() ? const Border(bottom: BorderSide(color: AppColor.colorBorderIdentityInfo)): null,
),
padding: const EdgeInsets.only(left: 12.0, bottom: 12.0, top: 12.0, right: 4.0),
child: Scrollbar(
child: MediaQuery.removePadding(
context: context,
removeTop: true,
child: ListView.builder(
padding: const EdgeInsets.only(right: 12.0),
itemCount: controller.listAllIdentities.value.length,
itemBuilder: ((context, index) {
return IdentityListTileBuilder(
controller: widget.controller,
index: index,
selected: widget.controller.selectedIndex.value,
);
}),
)
),
),
),
if(controller.isSignatureShow())
Obx(() => SignatureOfIdentityBuilder(identity: controller.identitySelected.value!)),
],
),
));
});
}
}
@@ -0,0 +1,121 @@
import 'package:core/core.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:jmap_dart_client/jmap/identities/identity.dart';
import 'package:tmail_ui_user/features/manage_account/presentation/profiles/identities/identities_controller.dart';
class IdentityListTileBuilder extends StatelessWidget {
const IdentityListTileBuilder({
Key? key,
required this.controller,
required this.index,
required this.selected
}) : super(key: key);
final IdentitiesController controller;
final int index;
final int? selected;
@override
Widget build(BuildContext context) {
final imagePaths = controller.imagePaths;
final listAllIdentities = controller.listAllIdentities.value;
final identity = listAllIdentities[index];
return Material(
color: Colors.transparent,
child: InkWell(
borderRadius: BorderRadius.circular(12.0),
onTap: () => controller.selectIdentity(index),
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12.0),
color: isIdentitySelected(index) ? AppColor.colorItemSelected : Colors.transparent,
),
padding: const EdgeInsets.only(top: 12.0, bottom: 12.0, right: 12.0),
child: Row(
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: BuildUtils.isWeb ? 8.0 : 0.0),
child: Radio<int>(
value: index,
groupValue: selected,
onChanged: (selectedValue) => controller.selectIdentity(index),
),
),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
identity.name ?? '',
overflow: CommonTextStyle.defaultTextOverFlow,
softWrap: CommonTextStyle.defaultSoftWrap,
style: const TextStyle(
fontWeight: FontWeight.w500,
fontSize: 16,
color: Colors.black)),
_buildIconWithTextLine(imagePaths.icEmail, identity.email),
for(final widget in _buildReplyLines(identity, imagePaths))
widget
],
)
),
if(isIdentitySelected(index))...[
buildSVGIconButton(
padding: const EdgeInsets.only(left: 12.0, right: 12.0),
icon: imagePaths.icEdit,
iconSize: 24,
iconColor: AppColor.primaryColor,
onTap: () => controller.goToEditIdentity(context, controller.identitySelected.value!),
),
buildSVGIconButton(
padding: const EdgeInsets.only(right: 0.0),
icon: imagePaths.icDeleteOutline,
iconSize: 24,
iconColor: AppColor.colorActionDeleteConfirmDialog,
onTap: () => controller.openConfirmationDialogDeleteIdentityAction(context, controller.identitySelected.value!),
),
]
],
),
),
),
);
}
List<Widget> _buildReplyLines(Identity? identity, ImagePaths imagePaths) {
return [
for(var identityReplyTo in identity?.replyTo?.toList() ?? [])
_buildIconWithTextLine(imagePaths.icReply, identityReplyTo.email)];
}
Widget _buildIconWithTextLine(String imagePath, String? text) {
return Row(
children: [
Padding(
padding: const EdgeInsets.only(right: 8.0),
child: SvgPicture.asset(
imagePath,
color: AppColor.primaryColor,
width: 16,
fit: BoxFit.fitHeight,
)),
// replacement character after ..., so wrap it inside richtext is workaround solution
Expanded(child: Text(
text ?? '', style: const TextStyle(
color: AppColor.colorEmailAddressFull,
fontWeight: FontWeight.w400,
fontSize: 13,
),
overflow: CommonTextStyle.defaultTextOverFlow,
softWrap: CommonTextStyle.defaultSoftWrap,
)),
]
);
}
bool isIdentitySelected(int index) {
return index == selected;
}
}
@@ -0,0 +1,65 @@
import 'package:core/core.dart';
import 'package:flutter/material.dart';
import 'package:html_unescape/html_unescape_small.dart';
import 'package:jmap_dart_client/jmap/identities/identity.dart';
class SignatureBuilder extends StatelessWidget {
SignatureBuilder({
Key? key,
this.width,
this.height,
this.htmlSignature,
this.textSignature,
}) : super(key: key);
final Signature? htmlSignature;
final Signature? textSignature;
final double? width;
final double? height;
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (context, constraints) {
final signatureWidth = width ?? constraints.biggest.width;
final signatureHeight = height ?? constraints.biggest.height;
return Container(
width: signatureWidth,
padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 16.0),
decoration: const BoxDecoration(
color: Colors.white,
),
child: _buildSignature(signatureWidth, signatureHeight),
);
}
);
}
Widget _buildSignature(double width, double height) {
Widget signature;
if (htmlSignature != null && htmlSignature!.value.isNotEmpty) {
final htmlSignatureDecoded = _decodeHtml(htmlSignature!.value);
if (BuildUtils.isWeb) {
signature = HtmlContentViewerOnWeb(
contentHtml: htmlSignatureDecoded,
widthContent: width,
heightContent: height,
controller: HtmlViewerControllerForWeb(),
allowResizeToDocumentSize: false);
} else {
signature = HtmlContentViewer(contentHtml: htmlSignatureDecoded, heightContent: height);
}
} else if (textSignature != null) {
signature = Text(textSignature!.value);
} else {
signature = SizedBox.fromSize(size: Size(width, height));
}
return signature;
}
String _decodeHtml(String htmlString) {
final unescape = HtmlUnescape();
return unescape.convert(htmlString);
}
}
@@ -0,0 +1,20 @@
import 'package:flutter/cupertino.dart';
import 'package:jmap_dart_client/jmap/identities/identity.dart';
import 'package:tmail_ui_user/features/manage_account/presentation/profiles/identities/widgets/signature_builder.dart';
class SignatureOfIdentityBuilder extends StatelessWidget {
SignatureOfIdentityBuilder({required this.identity});
final Identity identity;
@override
Widget build(BuildContext context) {
return SignatureBuilder(
height: 256,
htmlSignature: identity.htmlSignature,
textSignature: identity.textSignature
);
}
}
@@ -1200,11 +1200,26 @@ class AppLocalizations {
name: 'profiles');
}
String get profiles_description {
return Intl.message('Info about you, and options to manage it',
name: 'profiles_description');
}
String get identities {
return Intl.message('Identities',
name: 'identities');
}
String get identities_description {
return Intl.message('Select the identity or email address you want to use to send an emails',
name: 'identities_description');
}
String get create_new_identity {
return Intl.message('Create new identity',
name: 'create_new_identity');
}
String get new_identity {
return Intl.message('New Identity',
name: 'new_identity');
@@ -2594,4 +2609,11 @@ class AppLocalizations {
name: 'openInNewTab',
);
}
String get regards {
return Intl.message(
'Regards',
name: 'regards',
);
}
}