From 5fb18a13d2442f651209e1fa83d1250edc798ff2 Mon Sep 17 00:00:00 2001 From: Vaibhav Surve Date: Mon, 7 Jul 2025 14:11:15 +0530 Subject: [PATCH] feat(directory): refactor DirectoryView layout for improved structure and readability --- lib/view/directory/directory_view.dart | 628 ++++++++++++------------- 1 file changed, 307 insertions(+), 321 deletions(-) diff --git a/lib/view/directory/directory_view.dart b/lib/view/directory/directory_view.dart index 0f8430d..0b41133 100644 --- a/lib/view/directory/directory_view.dart +++ b/lib/view/directory/directory_view.dart @@ -27,346 +27,332 @@ class DirectoryView extends StatelessWidget { @override Widget build(BuildContext context) { - return Column( - children: [ - // Search + Filter + Toggle - Padding( - padding: MySpacing.xy(8, 8), - child: Row( - children: [ - // Search Field - Expanded( - child: SizedBox( - height: 35, - child: TextField( - controller: searchController, - onChanged: (value) { - controller.searchQuery.value = value; - controller.applyFilters(); - }, - decoration: InputDecoration( - contentPadding: - const EdgeInsets.symmetric(horizontal: 12), - prefixIcon: const Icon(Icons.search, - size: 20, color: Colors.grey), - hintText: 'Search contacts...', - filled: true, - fillColor: Colors.white, - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(10), - borderSide: BorderSide(color: Colors.grey.shade300), - ), - enabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(10), - borderSide: BorderSide(color: Colors.grey.shade300), + return Scaffold( + backgroundColor: Colors.white, + floatingActionButton: FloatingActionButton( + backgroundColor: Colors.red, + onPressed: () async { + final result = await Get.bottomSheet( + AddContactBottomSheet(), + isScrollControlled: true, + backgroundColor: Colors.transparent, + ); + if (result == true) { + controller.fetchContacts(); + } + }, + child: const Icon(Icons.add, color: Colors.white), + ), + body: Column( + children: [ + Padding( + padding: MySpacing.xy(8, 8), + child: Row( + children: [ + Expanded( + child: SizedBox( + height: 35, + child: TextField( + controller: searchController, + onChanged: (value) { + controller.searchQuery.value = value; + controller.applyFilters(); + }, + decoration: InputDecoration( + contentPadding: + const EdgeInsets.symmetric(horizontal: 12), + prefixIcon: const Icon(Icons.search, + size: 20, color: Colors.grey), + hintText: 'Search contacts...', + filled: true, + fillColor: Colors.white, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(10), + borderSide: BorderSide(color: Colors.grey.shade300), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(10), + borderSide: BorderSide(color: Colors.grey.shade300), + ), ), ), ), ), - ), - MySpacing.width(8), - Tooltip( - message: 'Refresh Data', - child: InkWell( - borderRadius: BorderRadius.circular(24), - onTap: _refreshDirectory, - child: MouseRegion( - cursor: SystemMouseCursors.click, + MySpacing.width(8), + Tooltip( + message: 'Refresh Data', + child: InkWell( + borderRadius: BorderRadius.circular(24), + onTap: _refreshDirectory, child: const Padding( padding: EdgeInsets.all(0), child: Icon(Icons.refresh, color: Colors.green, size: 28), ), ), ), - ), - MySpacing.width(8), - // Filter Icon with red dot - Obx(() { - final isFilterActive = controller.hasActiveFilters(); - return Stack( - children: [ - Container( - height: 35, - width: 35, - decoration: BoxDecoration( - color: Colors.white, - border: Border.all(color: Colors.grey.shade300), - borderRadius: BorderRadius.circular(10), - ), - child: IconButton( - icon: Icon(Icons.filter_alt_outlined, - size: 20, - color: isFilterActive - ? Colors.indigo - : Colors.black87), - onPressed: () { - showModalBottomSheet( - context: context, - isScrollControlled: true, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.vertical( - top: Radius.circular(20)), - ), - builder: (_) => const DirectoryFilterBottomSheet(), - ); - }, - ), - ), - if (isFilterActive) - Positioned( - top: 6, - right: 6, - child: Container( - height: 8, - width: 8, - decoration: const BoxDecoration( - color: Colors.red, - shape: BoxShape.circle, - ), + MySpacing.width(8), + Obx(() { + final isFilterActive = controller.hasActiveFilters(); + return Stack( + children: [ + Container( + height: 35, + width: 35, + decoration: BoxDecoration( + color: Colors.white, + border: Border.all(color: Colors.grey.shade300), + borderRadius: BorderRadius.circular(10), + ), + child: IconButton( + icon: Icon(Icons.filter_alt_outlined, + size: 20, + color: + isFilterActive ? Colors.indigo : Colors.black87), + onPressed: () { + showModalBottomSheet( + context: context, + isScrollControlled: true, + shape: const RoundedRectangleBorder( + borderRadius: + BorderRadius.vertical(top: Radius.circular(20)), + ), + builder: (_) => const DirectoryFilterBottomSheet(), + ); + }, ), ), - ], - ); - }), - MySpacing.width(10), - // 3-dot Popup with toggle - Container( - height: 35, - width: 35, - decoration: BoxDecoration( - color: Colors.white, - border: Border.all(color: Colors.grey.shade300), - borderRadius: BorderRadius.circular(10), - ), - child: PopupMenuButton( - padding: EdgeInsets.zero, - icon: const Icon(Icons.more_vert, - size: 20, color: Colors.black87), - shape: RoundedRectangleBorder( + if (isFilterActive) + Positioned( + top: 6, + right: 6, + child: Container( + height: 8, + width: 8, + decoration: const BoxDecoration( + color: Colors.red, + shape: BoxShape.circle, + ), + ), + ), + ], + ); + }), + MySpacing.width(10), + Container( + height: 35, + width: 35, + decoration: BoxDecoration( + color: Colors.white, + border: Border.all(color: Colors.grey.shade300), borderRadius: BorderRadius.circular(10), ), - itemBuilder: (context) => [ - PopupMenuItem( - value: 0, - enabled: false, - child: Obx(() => Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - MyText.bodySmall('Show Inactive', - fontWeight: 600), - Switch.adaptive( - value: !controller.isActive.value, - activeColor: Colors.indigo, - onChanged: (val) { - controller.isActive.value = !val; - controller.fetchContacts(active: !val); - Navigator.pop(context); - }, - ), - ], - )), + child: PopupMenuButton( + padding: EdgeInsets.zero, + icon: const Icon(Icons.more_vert, + size: 20, color: Colors.black87), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), ), - ], - ), - ), - ], - ), - ), - - // Contact List - Expanded( - child: Obx(() { - if (controller.isLoading.value) { - return ListView.separated( - itemCount: 10, - separatorBuilder: (_, __) => MySpacing.height(12), - itemBuilder: (_, __) => SkeletonLoaders.contactSkeletonCard(), - ); - } - - if (controller.filteredContacts.isEmpty) { - return Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Icon(Icons.contact_page_outlined, - size: 60, color: Colors.grey), - const SizedBox(height: 12), - MyText.bodyMedium('No contacts found.', fontWeight: 500), - ], - ), - ); - } - - return ListView.separated( - padding: MySpacing.only(left: 8, right: 8, top: 4, bottom: 80), - itemCount: controller.filteredContacts.length, - separatorBuilder: (_, __) => MySpacing.height(12), - itemBuilder: (_, index) { - final contact = controller.filteredContacts[index]; - final phone = contact.contactPhones.isNotEmpty - ? contact.contactPhones.first.phoneNumber - : '-'; - final nameParts = contact.name.trim().split(" "); - final firstName = nameParts.first; - final lastName = nameParts.length > 1 ? nameParts.last : ""; - final tags = contact.tags.map((tag) => tag.name).toList(); - - return InkWell( - onTap: () { - Get.to(() => ContactDetailScreen(contact: contact)); - }, - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 12.0, vertical: 10), - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Avatar( - firstName: firstName, lastName: lastName, size: 45), - MySpacing.width(12), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - MyText.titleSmall(contact.name, - fontWeight: 600, - overflow: TextOverflow.ellipsis), - MyText.bodySmall(contact.organization, - color: Colors.grey[700], - overflow: TextOverflow.ellipsis), - MySpacing.height(6), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - ...contact.contactEmails.map((e) => - GestureDetector( - onTap: () => LauncherUtils.launchEmail( - e.emailAddress), - onLongPress: () => - LauncherUtils.copyToClipboard( - e.emailAddress, - typeLabel: 'Email'), - child: Padding( - padding: - const EdgeInsets.only(bottom: 4), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - const Icon(Icons.email_outlined, - size: 16, - color: Colors.indigo), - MySpacing.width(4), - ConstrainedBox( - constraints: - const BoxConstraints( - maxWidth: 180), - child: MyText.labelSmall( - e.emailAddress, - overflow: - TextOverflow.ellipsis, - color: Colors.indigo, - decoration: - TextDecoration.underline, - ), - ), - ], - ), - ), - )), - ...contact.contactPhones.map((p) => - GestureDetector( - onTap: () => LauncherUtils.launchPhone( - p.phoneNumber), - onLongPress: () => - LauncherUtils.copyToClipboard( - p.phoneNumber, - typeLabel: 'Phone number'), - child: Padding( - padding: - const EdgeInsets.only(bottom: 4), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - const Icon(Icons.phone_outlined, - size: 16, - color: Colors.indigo), - MySpacing.width(4), - ConstrainedBox( - constraints: - const BoxConstraints( - maxWidth: 160), - child: MyText.labelSmall( - p.phoneNumber, - overflow: - TextOverflow.ellipsis, - color: Colors.indigo, - decoration: - TextDecoration.underline, - ), - ), - ], - ), - ), - )), - ], - ), - if (tags.isNotEmpty) ...[ - MySpacing.height(4), - MyText.labelSmall(tags.join(', '), - color: Colors.grey[500], - maxLines: 1, - overflow: TextOverflow.ellipsis), + itemBuilder: (context) => [ + PopupMenuItem( + value: 0, + enabled: false, + child: Obx(() => Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + MyText.bodySmall('Show Inactive', fontWeight: 600), + Switch.adaptive( + value: !controller.isActive.value, + activeColor: Colors.indigo, + onChanged: (val) { + controller.isActive.value = !val; + controller.fetchContacts(active: !val); + Navigator.pop(context); + }, + ), ], - ], - ), - ), - Column( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - const Icon(Icons.arrow_forward_ios, - color: Colors.grey, size: 16), - MySpacing.height(12), - if (phone != '-') - GestureDetector( - onTap: () => - LauncherUtils.launchWhatsApp(phone), - child: const FaIcon(FontAwesomeIcons.whatsapp, - color: Colors.green, size: 20), - ), - ], - ), - ], - ), + )), + ), + ], ), - ); - }, - ); - }), - ), - - // Floating action button (moved here so it doesn't appear in NotesView) - Padding( - padding: const EdgeInsets.only(right: 16.0, bottom: 16.0), - child: Align( - alignment: Alignment.bottomRight, - child: FloatingActionButton( - backgroundColor: Colors.red, - onPressed: () async { - final result = await Get.bottomSheet( - AddContactBottomSheet(), - isScrollControlled: true, - backgroundColor: Colors.transparent, - ); - if (result == true) { - controller.fetchContacts(); - } - }, - child: const Icon(Icons.add, color: Colors.white), + ), + ], ), ), - ), - ], + Expanded( + child: Obx(() { + if (controller.isLoading.value) { + return ListView.separated( + itemCount: 10, + separatorBuilder: (_, __) => MySpacing.height(12), + itemBuilder: (_, __) => SkeletonLoaders.contactSkeletonCard(), + ); + } + + if (controller.filteredContacts.isEmpty) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon(Icons.contact_page_outlined, + size: 60, color: Colors.grey), + const SizedBox(height: 12), + MyText.bodyMedium('No contacts found.', fontWeight: 500), + ], + ), + ); + } + + return ListView.separated( + padding: MySpacing.only(left: 8, right: 8, top: 4, bottom: 80), + itemCount: controller.filteredContacts.length, + separatorBuilder: (_, __) => MySpacing.height(12), + itemBuilder: (_, index) { + final contact = controller.filteredContacts[index]; + final phone = contact.contactPhones.isNotEmpty + ? contact.contactPhones.first.phoneNumber + : '-'; + final nameParts = contact.name.trim().split(" "); + final firstName = nameParts.first; + final lastName = + nameParts.length > 1 ? nameParts.last : ""; + final tags = contact.tags.map((tag) => tag.name).toList(); + + return InkWell( + onTap: () { + Get.to(() => ContactDetailScreen(contact: contact)); + }, + child: Padding( + padding: const EdgeInsets.fromLTRB(12, 10, 12, 0), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Avatar( + firstName: firstName, + lastName: lastName, + size: 45), + MySpacing.width(12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + MyText.titleSmall(contact.name, + fontWeight: 600, + overflow: TextOverflow.ellipsis), + MyText.bodySmall(contact.organization, + color: Colors.grey[700], + overflow: TextOverflow.ellipsis), + MySpacing.height(6), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ...contact.contactEmails.map((e) => + GestureDetector( + onTap: () => LauncherUtils + .launchEmail(e.emailAddress), + onLongPress: () => + LauncherUtils.copyToClipboard( + e.emailAddress, + typeLabel: 'Email'), + child: Padding( + padding: + const EdgeInsets.only(bottom: 4), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.email_outlined, + size: 16, + color: Colors.indigo), + MySpacing.width(4), + ConstrainedBox( + constraints: + const BoxConstraints( + maxWidth: 180), + child: MyText.labelSmall( + e.emailAddress, + overflow: + TextOverflow.ellipsis, + color: Colors.indigo, + decoration: TextDecoration + .underline, + ), + ), + ], + ), + ), + )), + ...contact.contactPhones.map((p) => + GestureDetector( + onTap: () => LauncherUtils + .launchPhone(p.phoneNumber), + onLongPress: () => + LauncherUtils.copyToClipboard( + p.phoneNumber, + typeLabel: 'Phone number'), + child: Padding( + padding: + const EdgeInsets.only(bottom: 4), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.phone_outlined, + size: 16, + color: Colors.indigo), + MySpacing.width(4), + ConstrainedBox( + constraints: + const BoxConstraints( + maxWidth: 160), + child: MyText.labelSmall( + p.phoneNumber, + overflow: + TextOverflow.ellipsis, + color: Colors.indigo, + decoration: TextDecoration + .underline, + ), + ), + ], + ), + ), + )), + ], + ), + if (tags.isNotEmpty) ...[ + MySpacing.height(4), + MyText.labelSmall(tags.join(', '), + color: Colors.grey[500], + maxLines: 1, + overflow: TextOverflow.ellipsis), + ], + ], + ), + ), + Column( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + const Icon(Icons.arrow_forward_ios, + color: Colors.grey, size: 16), + MySpacing.height(12), + if (phone != '-') + GestureDetector( + onTap: () => + LauncherUtils.launchWhatsApp(phone), + child: const FaIcon(FontAwesomeIcons.whatsapp, + color: Colors.green, size: 20), + ), + ], + ), + ], + ), + ), + ); + }, + ); + }), + ), + ], + ), ); } }