TF-4308 Add NoLabelYetWidget for choose label modal

Signed-off-by: dab246 <tdvu@linagora.com>
This commit is contained in:
dab246
2026-04-16 19:33:35 +07:00
committed by Dat H. Pham
parent c5eb4a011d
commit 35b46ff21b
9 changed files with 288 additions and 89 deletions
+5
View File
@@ -0,0 +1,5 @@
<svg width="98" height="98" viewBox="0 0 98 98" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="49" cy="49" r="49" fill="#E0EDFF"/>
<path d="M49.9441 28.2701C49.1313 27.4571 48.0289 27.0002 46.8794 27H31.3348C30.1851 27 29.0826 27.4567 28.2696 28.2696C27.4567 29.0826 27 30.1851 27 31.3348V46.8794C27.0002 48.0289 27.4571 49.1313 28.2701 49.9441L47.1351 68.8091C48.1202 69.788 49.4526 70.3374 50.8414 70.3374C52.2301 70.3374 53.5625 69.788 54.5476 68.8091L68.8091 54.5476C69.788 53.5625 70.3374 52.2301 70.3374 50.8414C70.3374 49.4526 69.788 48.1202 68.8091 47.1351L49.9441 28.2701Z" stroke="#9EBFEB" stroke-width="4.33479" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M38.9206 40.0043C39.5191 40.0043 40.0043 39.5191 40.0043 38.9206C40.0043 38.3221 39.5191 37.8369 38.9206 37.8369C38.3221 37.8369 37.8369 38.3221 37.8369 38.9206C37.8369 39.5191 38.3221 40.0043 38.9206 40.0043Z" fill="#9EBFEB" stroke="#9EBFEB" stroke-width="4.33479" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1016 B

@@ -282,6 +282,7 @@ class ImagePaths {
String get icColorPicker => _getImagePath('ic_color_picker.svg'); String get icColorPicker => _getImagePath('ic_color_picker.svg');
String get icThumbsUp => _getImagePath('ic_thumbs_up.svg'); String get icThumbsUp => _getImagePath('ic_thumbs_up.svg');
String get icAllEmail => _getImagePath('ic_all_email.svg'); String get icAllEmail => _getImagePath('ic_all_email.svg');
String get icNoTag => _getImagePath('ic_no_tag.svg');
String _getImagePath(String imageName) { String _getImagePath(String imageName) {
return AssetsPaths.images + imageName; return AssetsPaths.images + imageName;
@@ -4,10 +4,10 @@ import 'package:core/presentation/extensions/color_extension.dart';
import 'package:core/presentation/resources/image_paths.dart'; import 'package:core/presentation/resources/image_paths.dart';
import 'package:core/presentation/utils/theme_utils.dart'; import 'package:core/presentation/utils/theme_utils.dart';
import 'package:core/presentation/views/button/default_close_button_widget.dart'; import 'package:core/presentation/views/button/default_close_button_widget.dart';
import 'package:core/presentation/views/dialog/modal_list_action_button_widget.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:labels/labels.dart'; import 'package:labels/labels.dart';
import 'package:tmail_ui_user/features/mailbox/presentation/widgets/labels/label_list_item.dart'; import 'package:tmail_ui_user/features/labels/presentation/widgets/list_label_with_action_modal.dart';
import 'package:tmail_ui_user/features/labels/presentation/widgets/no_label_yet_widget.dart';
import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart';
import 'package:tmail_ui_user/main/routes/route_navigation.dart'; import 'package:tmail_ui_user/main/routes/route_navigation.dart';
@@ -30,10 +30,6 @@ class ChooseLabelModal extends StatefulWidget {
} }
class _ChooseLabelModalState extends State<ChooseLabelModal> { class _ChooseLabelModalState extends State<ChooseLabelModal> {
final ValueNotifier<bool> _addLabelStateNotifier = ValueNotifier(false);
final ValueNotifier<List<Label>> _selectedLabelStateNotifier =
ValueNotifier([]);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final appLocalizations = AppLocalizations.of(context); final appLocalizations = AppLocalizations.of(context);
@@ -41,6 +37,8 @@ class _ChooseLabelModalState extends State<ChooseLabelModal> {
return LayoutBuilder(builder: (_, constraints) { return LayoutBuilder(builder: (_, constraints) {
final currentScreenWidth = constraints.maxWidth; final currentScreenWidth = constraints.maxWidth;
final currentScreenHeight = constraints.maxHeight; final currentScreenHeight = constraints.maxHeight;
double height = math.min(currentScreenHeight - 100, 645);
double width = math.min(currentScreenWidth - 32, 536);
Widget bodyWidget = Container( Widget bodyWidget = Container(
decoration: BoxDecoration( decoration: BoxDecoration(
@@ -58,11 +56,8 @@ class _ChooseLabelModalState extends State<ChooseLabelModal> {
), ),
], ],
), ),
width: math.min( width: width,
currentScreenWidth - 32, height: height,
554,
),
constraints: BoxConstraints(maxHeight: currentScreenHeight - 100),
clipBehavior: Clip.antiAlias, clipBehavior: Clip.antiAlias,
child: Stack( child: Stack(
children: [ children: [
@@ -73,23 +68,21 @@ class _ChooseLabelModalState extends State<ChooseLabelModal> {
_buildTitle(appLocalizations), _buildTitle(appLocalizations),
const Divider(height: 1, color: Colors.black12), const Divider(height: 1, color: Colors.black12),
_buildSubtitle(appLocalizations), _buildSubtitle(appLocalizations),
Flexible(child: _buildLabelListView()), if (widget.labels.isNotEmpty)
ValueListenableBuilder( Expanded(
valueListenable: _addLabelStateNotifier, child: ListLabelWithActionModal(
builder: (_, value, __) { labels: widget.labels,
return ModalListActionButtonWidget( imagePaths: widget.imagePaths,
positiveLabel: appLocalizations.addLabel, onLabelAsToEmailsAction: widget.onLabelAsToEmailsAction,
negativeLabel: appLocalizations.cancel, onCloseModal: _onCloseModal,
padding: const EdgeInsets.symmetric( ),
vertical: 25, )
horizontal: 25, else
), Expanded(
isPositiveActionEnabled: value, child: Center(
onPositiveAction: _onAddLabel, child: NoLabelYetWidget(imagePaths: widget.imagePaths),
onNegativeAction: _onCloseModal, ),
); ),
},
),
], ],
), ),
DefaultCloseButtonWidget( DefaultCloseButtonWidget(
@@ -137,63 +130,7 @@ class _ChooseLabelModalState extends State<ChooseLabelModal> {
); );
} }
Widget _buildLabelListView() {
final labelList = widget.labels;
return ListView.builder(
shrinkWrap: true,
padding: EdgeInsets.zero,
itemCount: labelList.length,
itemBuilder: (context, index) {
final label = labelList[index];
return ValueListenableBuilder(
valueListenable: _selectedLabelStateNotifier,
builder: (_, selectedLabels, __) {
final isSelected = selectedLabels.contains(label);
return LabelListItem(
label: label,
imagePaths: widget.imagePaths,
padding: const EdgeInsetsDirectional.only(
start: 4,
end: 16,
),
enableSelectedIcon: true,
isSelected: isSelected,
onOpenLabelCallback: _onToggleLabel,
);
},
);
},
);
}
void _onToggleLabel(Label selectedLabel) {
final currentLabels = _selectedLabelStateNotifier.value;
final isAlreadySelected = currentLabels.any((label) => label.id == selectedLabel.id);
if (isAlreadySelected) {
_selectedLabelStateNotifier.value =
currentLabels.where((label) => label.id != selectedLabel.id).toList();
} else {
_selectedLabelStateNotifier.value = [...currentLabels, selectedLabel];
}
_addLabelStateNotifier.value = _selectedLabelStateNotifier.value.isNotEmpty;
}
void _onAddLabel() {
widget.onLabelAsToEmailsAction(_selectedLabelStateNotifier.value);
popBack();
}
void _onCloseModal() { void _onCloseModal() {
popBack(); popBack();
} }
@override
void dispose() {
_addLabelStateNotifier.dispose();
_selectedLabelStateNotifier.dispose();
super.dispose();
}
} }
@@ -0,0 +1,112 @@
import 'package:core/presentation/resources/image_paths.dart';
import 'package:core/presentation/views/dialog/modal_list_action_button_widget.dart';
import 'package:flutter/material.dart';
import 'package:labels/model/label.dart';
import 'package:tmail_ui_user/features/labels/presentation/widgets/choose_label_modal.dart';
import 'package:tmail_ui_user/features/mailbox/presentation/widgets/labels/label_list_item.dart';
import 'package:tmail_ui_user/main/localizations/app_localizations.dart';
class ListLabelWithActionModal extends StatefulWidget {
final List<Label> labels;
final ImagePaths imagePaths;
final OnLabelAsToEmailsAction onLabelAsToEmailsAction;
final VoidCallback onCloseModal;
const ListLabelWithActionModal({
super.key,
required this.labels,
required this.imagePaths,
required this.onLabelAsToEmailsAction,
required this.onCloseModal,
});
@override
State<ListLabelWithActionModal> createState() =>
_ListLabelWithActionModalState();
}
class _ListLabelWithActionModalState extends State<ListLabelWithActionModal> {
final ValueNotifier<bool> _addLabelStateNotifier = ValueNotifier(false);
final ValueNotifier<List<Label>> _selectedLabelStateNotifier =
ValueNotifier([]);
@override
Widget build(BuildContext context) {
final labelList = widget.labels;
final appLocalizations = AppLocalizations.of(context);
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: ListView.builder(
padding: EdgeInsets.zero,
itemCount: labelList.length,
itemBuilder: (context, index) {
final label = labelList[index];
return ValueListenableBuilder(
valueListenable: _selectedLabelStateNotifier,
builder: (_, selectedLabels, __) {
final isSelected = selectedLabels.contains(label);
return LabelListItem(
label: label,
imagePaths: widget.imagePaths,
padding: const EdgeInsetsDirectional.only(
start: 4,
end: 16,
),
enableSelectedIcon: true,
isSelected: isSelected,
onOpenLabelCallback: _onToggleLabel,
);
},
);
},
),
),
ValueListenableBuilder(
valueListenable: _addLabelStateNotifier,
builder: (_, value, __) {
return ModalListActionButtonWidget(
positiveLabel: appLocalizations.addLabel,
negativeLabel: appLocalizations.cancel,
padding: const EdgeInsets.symmetric(
vertical: 25,
horizontal: 25,
),
isPositiveActionEnabled: value,
onPositiveAction: _onAddLabel,
onNegativeAction: widget.onCloseModal,
);
},
),
],
);
}
void _onToggleLabel(Label selectedLabel) {
final currentLabels = _selectedLabelStateNotifier.value;
if (currentLabels.contains(selectedLabel)) {
_selectedLabelStateNotifier.value =
currentLabels.where((label) => label.id != selectedLabel.id).toList();
} else {
_selectedLabelStateNotifier.value = [...currentLabels, selectedLabel];
}
_addLabelStateNotifier.value = _selectedLabelStateNotifier.value.isNotEmpty;
}
void _onAddLabel() {
widget.onLabelAsToEmailsAction(_selectedLabelStateNotifier.value);
widget.onCloseModal();
}
@override
void dispose() {
_addLabelStateNotifier.dispose();
_selectedLabelStateNotifier.dispose();
super.dispose();
}
}
@@ -0,0 +1,109 @@
import 'package:core/presentation/extensions/color_extension.dart';
import 'package:core/presentation/resources/image_paths.dart';
import 'package:core/presentation/utils/theme_utils.dart';
import 'package:core/presentation/views/dialog/confirm_dialog_button.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:tmail_ui_user/main/localizations/app_localizations.dart';
class NoLabelYetWidget extends StatelessWidget {
final ImagePaths imagePaths;
final VoidCallback? onCreateLabel;
const NoLabelYetWidget({
super.key,
required this.imagePaths,
this.onCreateLabel,
});
@override
Widget build(BuildContext context) {
final appLocalizations = AppLocalizations.of(context);
return Container(
margin: const EdgeInsets.all(16),
constraints: const BoxConstraints(maxWidth: 352),
child: SingleChildScrollView(
physics: const ClampingScrollPhysics(),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
_IconNoTag(icon: imagePaths.icNoTag),
_LabelNoTag(
text: appLocalizations.noLabelsYet,
textStyle: ThemeUtils.textStyleInter600(),
padding: const EdgeInsetsDirectional.only(top: 16),
),
Flexible(
child: _LabelNoTag(
text: appLocalizations.noLabelsYetMessageDescriptions,
textStyle: ThemeUtils.textStyleInter400.copyWith(
fontSize: 16,
height: 21.01 / 16,
letterSpacing: -0.15,
color: AppColor.gray424244.withValues(alpha: 0.64),
),
padding: const EdgeInsetsDirectional.only(top: 36),
),
),
Container(
constraints: const BoxConstraints(minWidth: 186),
height: 48,
margin: const EdgeInsetsDirectional.only(top: 16),
child: ConfirmDialogButton(
label: appLocalizations.createALabel,
backgroundColor: Colors.white,
textColor: AppColor.primaryMain,
borderColor: AppColor.primaryMain,
icon: imagePaths.icAddIdentity,
onTapAction: onCreateLabel ?? () {},
),
),
],
),
),
);
}
}
class _IconNoTag extends StatelessWidget {
final String icon;
const _IconNoTag({required this.icon});
@override
Widget build(BuildContext context) {
return SvgPicture.asset(
icon,
width: 98,
height: 98,
fit: BoxFit.fill,
);
}
}
class _LabelNoTag extends StatelessWidget {
final String text;
final TextStyle textStyle;
final EdgeInsetsGeometry? padding;
const _LabelNoTag({
required this.text,
required this.textStyle,
this.padding,
});
@override
Widget build(BuildContext context) {
final textWidget = Text(
text,
style: textStyle,
textAlign: TextAlign.center,
);
if (padding != null) {
return Padding(padding: padding!, child: textWidget);
} else {
return textWidget;
}
}
}
@@ -396,7 +396,6 @@ class MailboxDashBoardView extends BaseMailboxDashBoardView {
controller.mapMailboxById, controller.mapMailboxById,
controller.imagePaths, controller.imagePaths,
isLabelAvailable: controller.isLabelAvailable, isLabelAvailable: controller.isLabelAvailable,
labels: controller.labelController.labels,
onCancelSelection: () => onCancelSelection: () =>
controller.dispatchAction(CancelSelectionAllEmailAction()), controller.dispatchAction(CancelSelectionAllEmailAction()),
onEmailActionTypeAction: (listEmails, actionType) => onEmailActionTypeAction: (listEmails, actionType) =>
@@ -4,7 +4,6 @@ import 'package:core/presentation/utils/theme_utils.dart';
import 'package:core/presentation/views/button/tmail_button_widget.dart'; import 'package:core/presentation/views/button/tmail_button_widget.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart';
import 'package:labels/model/label.dart';
import 'package:model/email/email_action_type.dart'; import 'package:model/email/email_action_type.dart';
import 'package:model/email/presentation_email.dart'; import 'package:model/email/presentation_email.dart';
import 'package:model/extensions/list_presentation_email_extension.dart'; import 'package:model/extensions/list_presentation_email_extension.dart';
@@ -22,7 +21,6 @@ class TopBarThreadSelection extends StatelessWidget{
final VoidCallback? onCancelSelection; final VoidCallback? onCancelSelection;
final ImagePaths imagePaths; final ImagePaths imagePaths;
final bool isLabelAvailable; final bool isLabelAvailable;
final List<Label> labels;
const TopBarThreadSelection ( const TopBarThreadSelection (
this.listEmail, this.listEmail,
@@ -31,7 +29,6 @@ class TopBarThreadSelection extends StatelessWidget{
{ {
super.key, super.key,
required this.isLabelAvailable, required this.isLabelAvailable,
required this.labels,
this.onEmailActionTypeAction, this.onEmailActionTypeAction,
this.onCancelSelection, this.onCancelSelection,
} }
@@ -109,7 +106,7 @@ class TopBarThreadSelection extends StatelessWidget{
EmailActionType.moveToMailbox, EmailActionType.moveToMailbox,
), ),
), ),
if (isLabelAvailable && labels.isNotEmpty) if (isLabelAvailable)
TMailButtonWidget.fromIcon( TMailButtonWidget.fromIcon(
icon: imagePaths.icTag, icon: imagePaths.icTag,
iconSize: TopBarThreadSelectionStyle.iconSize, iconSize: TopBarThreadSelectionStyle.iconSize,
+18
View File
@@ -5513,5 +5513,23 @@
"type": "text", "type": "text",
"placeholders_order": [], "placeholders_order": [],
"placeholders": {} "placeholders": {}
},
"noLabelsYet": "No Labels yet",
"@noLabelsYet": {
"type": "text",
"placeholders_order": [],
"placeholders": {}
},
"noLabelsYetMessageDescriptions": "Create your first label to organize your emails more efficiently.",
"@noLabelsYetMessageDescriptions": {
"type": "text",
"placeholders_order": [],
"placeholders": {}
},
"createALabel": "Create a label",
"@createALabel": {
"type": "text",
"placeholders_order": [],
"placeholders": {}
} }
} }
@@ -5839,4 +5839,25 @@ class AppLocalizations {
name: 'addListLabelToListEmailFailureMessage', name: 'addListLabelToListEmailFailureMessage',
); );
} }
String get noLabelsYet {
return Intl.message(
'No Labels yet',
name: 'noLabelsYet',
);
}
String get noLabelsYetMessageDescriptions {
return Intl.message(
'Create your first label to organize your emails more efficiently.',
name: 'noLabelsYetMessageDescriptions',
);
}
String get createALabel {
return Intl.message(
'Create a label',
name: 'createALabel',
);
}
} }