feat(directory): refactor DirectoryView layout for improved structure and readability

This commit is contained in:
Vaibhav Surve 2025-07-07 14:11:15 +05:30
parent 43aeec4c6f
commit 5fb18a13d2

View File

@ -27,346 +27,332 @@ class DirectoryView extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Column( return Scaffold(
children: [ backgroundColor: Colors.white,
// Search + Filter + Toggle floatingActionButton: FloatingActionButton(
Padding( backgroundColor: Colors.red,
padding: MySpacing.xy(8, 8), onPressed: () async {
child: Row( final result = await Get.bottomSheet(
children: [ AddContactBottomSheet(),
// Search Field isScrollControlled: true,
Expanded( backgroundColor: Colors.transparent,
child: SizedBox( );
height: 35, if (result == true) {
child: TextField( controller.fetchContacts();
controller: searchController, }
onChanged: (value) { },
controller.searchQuery.value = value; child: const Icon(Icons.add, color: Colors.white),
controller.applyFilters(); ),
}, body: Column(
decoration: InputDecoration( children: [
contentPadding: Padding(
const EdgeInsets.symmetric(horizontal: 12), padding: MySpacing.xy(8, 8),
prefixIcon: const Icon(Icons.search, child: Row(
size: 20, color: Colors.grey), children: [
hintText: 'Search contacts...', Expanded(
filled: true, child: SizedBox(
fillColor: Colors.white, height: 35,
border: OutlineInputBorder( child: TextField(
borderRadius: BorderRadius.circular(10), controller: searchController,
borderSide: BorderSide(color: Colors.grey.shade300), onChanged: (value) {
), controller.searchQuery.value = value;
enabledBorder: OutlineInputBorder( controller.applyFilters();
borderRadius: BorderRadius.circular(10), },
borderSide: BorderSide(color: Colors.grey.shade300), 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),
MySpacing.width(8), Tooltip(
Tooltip( message: 'Refresh Data',
message: 'Refresh Data', child: InkWell(
child: InkWell( borderRadius: BorderRadius.circular(24),
borderRadius: BorderRadius.circular(24), onTap: _refreshDirectory,
onTap: _refreshDirectory,
child: MouseRegion(
cursor: SystemMouseCursors.click,
child: const Padding( child: const Padding(
padding: EdgeInsets.all(0), padding: EdgeInsets.all(0),
child: Icon(Icons.refresh, color: Colors.green, size: 28), child: Icon(Icons.refresh, color: Colors.green, size: 28),
), ),
), ),
), ),
), MySpacing.width(8),
MySpacing.width(8), Obx(() {
// Filter Icon with red dot final isFilterActive = controller.hasActiveFilters();
Obx(() { return Stack(
final isFilterActive = controller.hasActiveFilters(); children: [
return Stack( Container(
children: [ height: 35,
Container( width: 35,
height: 35, decoration: BoxDecoration(
width: 35, color: Colors.white,
decoration: BoxDecoration( border: Border.all(color: Colors.grey.shade300),
color: Colors.white, borderRadius: BorderRadius.circular(10),
border: Border.all(color: Colors.grey.shade300), ),
borderRadius: BorderRadius.circular(10), child: IconButton(
), icon: Icon(Icons.filter_alt_outlined,
child: IconButton( size: 20,
icon: Icon(Icons.filter_alt_outlined, color:
size: 20, isFilterActive ? Colors.indigo : Colors.black87),
color: isFilterActive onPressed: () {
? Colors.indigo showModalBottomSheet(
: Colors.black87), context: context,
onPressed: () { isScrollControlled: true,
showModalBottomSheet( shape: const RoundedRectangleBorder(
context: context, borderRadius:
isScrollControlled: true, BorderRadius.vertical(top: Radius.circular(20)),
shape: const RoundedRectangleBorder( ),
borderRadius: BorderRadius.vertical( builder: (_) => const DirectoryFilterBottomSheet(),
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,
),
), ),
), ),
], if (isFilterActive)
); Positioned(
}), top: 6,
MySpacing.width(10), right: 6,
// 3-dot Popup with toggle child: Container(
Container( height: 8,
height: 35, width: 8,
width: 35, decoration: const BoxDecoration(
decoration: BoxDecoration( color: Colors.red,
color: Colors.white, shape: BoxShape.circle,
border: Border.all(color: Colors.grey.shade300), ),
borderRadius: BorderRadius.circular(10), ),
), ),
child: PopupMenuButton<int>( ],
padding: EdgeInsets.zero, );
icon: const Icon(Icons.more_vert, }),
size: 20, color: Colors.black87), MySpacing.width(10),
shape: RoundedRectangleBorder( Container(
height: 35,
width: 35,
decoration: BoxDecoration(
color: Colors.white,
border: Border.all(color: Colors.grey.shade300),
borderRadius: BorderRadius.circular(10), borderRadius: BorderRadius.circular(10),
), ),
itemBuilder: (context) => [ child: PopupMenuButton<int>(
PopupMenuItem<int>( padding: EdgeInsets.zero,
value: 0, icon: const Icon(Icons.more_vert,
enabled: false, size: 20, color: Colors.black87),
child: Obx(() => Row( shape: RoundedRectangleBorder(
mainAxisAlignment: MainAxisAlignment.spaceBetween, borderRadius: BorderRadius.circular(10),
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);
},
),
],
)),
), ),
], itemBuilder: (context) => [
), PopupMenuItem<int>(
), value: 0,
], enabled: false,
), child: Obx(() => Row(
), mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
// Contact List MyText.bodySmall('Show Inactive', fontWeight: 600),
Expanded( Switch.adaptive(
child: Obx(() { value: !controller.isActive.value,
if (controller.isLoading.value) { activeColor: Colors.indigo,
return ListView.separated( onChanged: (val) {
itemCount: 10, controller.isActive.value = !val;
separatorBuilder: (_, __) => MySpacing.height(12), controller.fetchContacts(active: !val);
itemBuilder: (_, __) => SkeletonLoaders.contactSkeletonCard(), Navigator.pop(context);
); },
} ),
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),
], ],
], )),
), ),
), ],
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),
),
],
),
],
),
),
);
},
);
}),
),
],
),
); );
} }
} }