feat: WIP contact-modal
This commit is contained in:
parent
3f6ea2e332
commit
a64b32d114
@ -1,6 +1,8 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<uses-permission android:name="android.permission.READ_CONTACTS"/>
|
||||
<uses-permission android:name="android.permission.WRITE_CONTACTS"/>
|
||||
<uses-permission android:name="android.permission.CALL_PHONE" />
|
||||
<uses-permission android:name="android.permission.SEND_SMS" />
|
||||
<uses-feature android:name="android.hardware.camera" android:required="false" />
|
||||
<!-- The INTERNET permission is required for development. Specifically,
|
||||
the Flutter tool needs it to communicate with the running application
|
||||
|
@ -1,6 +1,8 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<uses-permission android:name="android.permission.READ_CONTACTS"/>
|
||||
<uses-permission android:name="android.permission.WRITE_CONTACTS"/>
|
||||
<uses-permission android:name="android.permission.CALL_PHONE" />
|
||||
<uses-permission android:name="android.permission.SEND_SMS" />
|
||||
<application
|
||||
android:label="com.example.dialer"
|
||||
android:name="${applicationName}"
|
||||
|
@ -1,6 +1,8 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<uses-permission android:name="android.permission.READ_CONTACTS"/>
|
||||
<uses-permission android:name="android.permission.WRITE_CONTACTS"/>
|
||||
<uses-permission android:name="android.permission.CALL_PHONE" />
|
||||
<uses-permission android:name="android.permission.SEND_SMS" />
|
||||
<uses-permission android:name="android.permission.CAMERA"/>
|
||||
<uses-feature android:name="android.hardware.camera" android:required="false" />
|
||||
<!-- The INTERNET permission is required for development. Specifically,
|
||||
|
@ -4,7 +4,7 @@ import 'package:flutter_contacts/flutter_contacts.dart';
|
||||
class ContactService {
|
||||
Future<List<Contact>> fetchContacts() async {
|
||||
if (await FlutterContacts.requestPermission()) {
|
||||
return await FlutterContacts.getContacts(withProperties: true, withThumbnail: true, withAccounts: true, withGroups: true);
|
||||
return await FlutterContacts.getContacts(withProperties: true, withThumbnail: true, withAccounts: true, withGroups: true, withPhoto: true);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
@ -4,13 +4,15 @@ import 'package:flutter_contacts/flutter_contacts.dart';
|
||||
import '../../../widgets/color_darkener.dart';
|
||||
import '../contact_state.dart';
|
||||
import 'add_contact_button.dart';
|
||||
import 'contact_modal.dart';
|
||||
import 'share_own_qr.dart';
|
||||
|
||||
class AlphabetScrollPage extends StatefulWidget {
|
||||
final List<Contact> contacts;
|
||||
final double scrollOffset;
|
||||
|
||||
const AlphabetScrollPage({super.key, required this.contacts, required this.scrollOffset});
|
||||
const AlphabetScrollPage(
|
||||
{super.key, required this.contacts, required this.scrollOffset});
|
||||
|
||||
@override
|
||||
_AlphabetScrollPageState createState() => _AlphabetScrollPageState();
|
||||
@ -18,11 +20,14 @@ class AlphabetScrollPage extends StatefulWidget {
|
||||
|
||||
class _AlphabetScrollPageState extends State<AlphabetScrollPage> {
|
||||
late ScrollController _scrollController;
|
||||
List<Contact> _contacts = []; // Local copy of contacts for updating
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_scrollController = ScrollController(initialScrollOffset: widget.scrollOffset);
|
||||
_contacts = widget.contacts; // Initialize with the provided contacts
|
||||
_scrollController =
|
||||
ScrollController(initialScrollOffset: widget.scrollOffset);
|
||||
_scrollController.addListener(_onScroll);
|
||||
}
|
||||
|
||||
@ -31,11 +36,149 @@ class _AlphabetScrollPageState extends State<AlphabetScrollPage> {
|
||||
contactState.setScrollOffset(_scrollController.offset);
|
||||
}
|
||||
|
||||
Future<void> _refreshContacts() async {
|
||||
if (await FlutterContacts.requestPermission()) {
|
||||
final updatedContacts = await FlutterContacts.getContacts(
|
||||
withProperties: true, withThumbnail: true);
|
||||
setState(() {
|
||||
_contacts = updatedContacts;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// void _toggleFavorite(Contact contact) async {
|
||||
// print(contact.id);
|
||||
|
||||
// if (await FlutterContacts.requestPermission()) {
|
||||
// try {
|
||||
// // Fetch all contacts (this can be slow if there are many contacts)
|
||||
// List<Contact> allContacts = await FlutterContacts.getContacts(
|
||||
// withProperties: true,
|
||||
// withThumbnail: true,
|
||||
// withAccounts: true,
|
||||
// withPhoto: true
|
||||
// );
|
||||
|
||||
// // Find the specific contact by matching contact.id
|
||||
// // Use `orElse` to return a nullable Contact? or throw an exception if not found
|
||||
// Contact? contactToUpdate = allContacts.firstWhere(
|
||||
// (c) => c.id == contact.id,
|
||||
// orElse: () => throw Exception(
|
||||
// "Contact not found"), // Throw an exception if not found
|
||||
// );
|
||||
|
||||
// if (contactToUpdate != null) {
|
||||
// print("Contact fetched: ${contactToUpdate.displayName}");
|
||||
// print("Current isStarred status: ${contactToUpdate.isStarred}");
|
||||
|
||||
// // Toggle the favorite status
|
||||
// contactToUpdate.isStarred = !contactToUpdate.isStarred;
|
||||
// print("Updated isStarred status: ${contactToUpdate.isStarred}");
|
||||
|
||||
// // Update the contact
|
||||
// await FlutterContacts.updateContact(contactToUpdate);
|
||||
// print("Contact updated successfully");
|
||||
|
||||
// // Refresh the UI by updating the state
|
||||
// setState(() {
|
||||
// contact.isStarred = contactToUpdate.isStarred;
|
||||
// });
|
||||
// }
|
||||
// } catch (e) {
|
||||
// print("Error updating favorite status: $e");
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// void _toggleFavorite(Contact contact) async {
|
||||
// print(contact.id);
|
||||
|
||||
// try {
|
||||
// // Fetch the contact with details
|
||||
// final contactWithDetails = await FlutterContacts.getContact(contact.id,
|
||||
// withProperties: true, withThumbnail: true, withAccounts: true);
|
||||
|
||||
// if (contactWithDetails != null) {
|
||||
// print("Contact fetched: ${contactWithDetails.displayName}");
|
||||
// print("Current isStarred status: ${contactWithDetails.isStarred}");
|
||||
|
||||
// // Toggle the favorite status
|
||||
// contactWithDetails.isStarred = !contactWithDetails.isStarred;
|
||||
// print("Updated isStarred status: ${contactWithDetails.isStarred}");
|
||||
|
||||
// // Update the contact
|
||||
// await FlutterContacts.updateContact(contactWithDetails);
|
||||
// print("Contact updated successfully");
|
||||
|
||||
// // Reset the contact state (optional)
|
||||
// setState(() {
|
||||
// contact.isStarred = contactWithDetails.isStarred;
|
||||
// });
|
||||
|
||||
// // Re-fetch the contact to ensure UI reflects changes
|
||||
// final updatedContact = await FlutterContacts.getContact(contact.id,
|
||||
// withProperties: true, withThumbnail: true, withAccounts: true);
|
||||
|
||||
// if (updatedContact != null) {
|
||||
// print("Re-fetched updated contact: ${updatedContact.displayName}");
|
||||
// print("Re-fetched isStarred status: ${updatedContact.isStarred}");
|
||||
// } else {
|
||||
// print("Re-fetching contact failed.");
|
||||
// }
|
||||
// } else {
|
||||
// print("Contact details are not available");
|
||||
// }
|
||||
// } catch (e) {
|
||||
// print("Error updating favorite status: $e");
|
||||
// }
|
||||
// }
|
||||
|
||||
void _toggleFavorite(Contact contact) async {
|
||||
print(contact.id);
|
||||
|
||||
try {
|
||||
if (contact != null) {
|
||||
print("Contact fetched: ${contact.displayName}");
|
||||
print("Current isStarred status: ${contact.isStarred}");
|
||||
|
||||
// Toggle the favorite status
|
||||
contact.isStarred = !contact.isStarred;
|
||||
print("Updated isStarred status: ${contact.isStarred}");
|
||||
|
||||
// Update the contact
|
||||
await FlutterContacts.updateContact(contact);
|
||||
print("Contact updated successfully");
|
||||
|
||||
// Reset the contact state (optional)
|
||||
setState(() {
|
||||
contact.isStarred = contact.isStarred;
|
||||
});
|
||||
|
||||
// Re-fetch the contact to ensure UI reflects changes
|
||||
final updatedContact = await FlutterContacts.getContact(contact.id,
|
||||
withProperties: true, withThumbnail: true, withAccounts: true);
|
||||
|
||||
if (updatedContact != null) {
|
||||
print("Re-fetched updated contact: ${updatedContact.displayName}");
|
||||
print("Re-fetched isStarred status: ${updatedContact.isStarred}");
|
||||
} else {
|
||||
print("Re-fetching contact failed.");
|
||||
}
|
||||
} else {
|
||||
print("Contact details are not available");
|
||||
}
|
||||
} catch (e) {
|
||||
print("Error updating favorite status: $e");
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Map<String, List<Contact>> alphabetizedContacts = {};
|
||||
for (var contact in widget.contacts) {
|
||||
String firstLetter = contact.displayName.isNotEmpty ? contact.displayName[0].toUpperCase() : '#';
|
||||
for (var contact in _contacts) {
|
||||
String firstLetter = contact.displayName.isNotEmpty
|
||||
? contact.displayName[0].toUpperCase()
|
||||
: '#';
|
||||
if (!alphabetizedContacts.containsKey(firstLetter)) {
|
||||
alphabetizedContacts[firstLetter] = [];
|
||||
}
|
||||
@ -47,15 +190,19 @@ class _AlphabetScrollPageState extends State<AlphabetScrollPage> {
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.black,
|
||||
body: Column(
|
||||
children: [ // Top buttons row
|
||||
children: [
|
||||
// Top buttons row
|
||||
Container(
|
||||
color: Colors.black,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
AddContactButton(),
|
||||
QRCodeButton(contacts: widget.contacts, selfContact: ContactState.of(context).selfContact),
|
||||
QRCodeButton(
|
||||
contacts: _contacts,
|
||||
selfContact: ContactState.of(context).selfContact),
|
||||
],
|
||||
),
|
||||
),
|
||||
@ -72,7 +219,8 @@ class _AlphabetScrollPageState extends State<AlphabetScrollPage> {
|
||||
children: [
|
||||
// Alphabet Letter Header
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 16.0),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 8.0, horizontal: 16.0),
|
||||
child: Text(
|
||||
letter,
|
||||
style: TextStyle(
|
||||
@ -84,24 +232,75 @@ class _AlphabetScrollPageState extends State<AlphabetScrollPage> {
|
||||
),
|
||||
// Contact Entries
|
||||
...contacts.map((contact) {
|
||||
String phoneNumber = contact.phones.isNotEmpty ? contact.phones.first.number : 'No phone number';
|
||||
Color avatarColor = generateColorFromName(contact.displayName);
|
||||
String phoneNumber = contact.phones.isNotEmpty
|
||||
? contact.phones.first.number
|
||||
: 'No phone number';
|
||||
Color avatarColor =
|
||||
generateColorFromName(contact.displayName);
|
||||
return ListTile(
|
||||
leading: (contact.thumbnail != null && contact.thumbnail!.isNotEmpty)
|
||||
leading: (contact.thumbnail != null &&
|
||||
contact.thumbnail!.isNotEmpty)
|
||||
? CircleAvatar(
|
||||
backgroundImage: MemoryImage(contact.thumbnail!),
|
||||
)
|
||||
backgroundImage:
|
||||
MemoryImage(contact.thumbnail!),
|
||||
)
|
||||
: CircleAvatar(
|
||||
backgroundColor: avatarColor,
|
||||
child: Text(
|
||||
contact.displayName.isNotEmpty ? contact.displayName[0].toUpperCase() : '?',
|
||||
style: TextStyle(color: darken(avatarColor, 0.4)),
|
||||
),
|
||||
),
|
||||
title: Text(contact.displayName, style: TextStyle(color: Colors.white)),
|
||||
subtitle: Text(phoneNumber, style: TextStyle(color: Colors.white70)),
|
||||
backgroundColor: avatarColor,
|
||||
child: Text(
|
||||
contact.displayName.isNotEmpty
|
||||
? contact.displayName[0].toUpperCase()
|
||||
: '?',
|
||||
style: TextStyle(
|
||||
color: darken(avatarColor, 0.4)),
|
||||
),
|
||||
),
|
||||
title: Text(contact.displayName,
|
||||
style: TextStyle(color: Colors.white)),
|
||||
subtitle: Text(phoneNumber,
|
||||
style: TextStyle(color: Colors.white70)),
|
||||
onTap: () {
|
||||
// Handle contact tap
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
backgroundColor: Colors.transparent,
|
||||
builder: (context) {
|
||||
return ContactModal(
|
||||
contact: contact,
|
||||
onEdit: () async {
|
||||
// Trigger edit logic and refresh contacts
|
||||
if (await FlutterContacts
|
||||
.requestPermission()) {
|
||||
final updatedContact =
|
||||
await FlutterContacts.openExternalEdit(
|
||||
contact.id);
|
||||
if (updatedContact != null) {
|
||||
await _refreshContacts();
|
||||
Navigator.of(context).pop();
|
||||
ScaffoldMessenger.of(context)
|
||||
.showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
'${contact.displayName} updated successfully!'),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
ScaffoldMessenger.of(context)
|
||||
.showSnackBar(
|
||||
SnackBar(
|
||||
content:
|
||||
Text('Edit canceled or failed.'),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
onToggleFavorite: () {
|
||||
_toggleFavorite(contact);
|
||||
},
|
||||
isFavorite: contact.isStarred,
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}),
|
||||
|
180
dialer/lib/features/contacts/widgets/contact_modal.dart
Normal file
180
dialer/lib/features/contacts/widgets/contact_modal.dart
Normal file
@ -0,0 +1,180 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_contacts/flutter_contacts.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
import 'package:dialer/widgets/username_color_generator.dart';
|
||||
|
||||
class ContactModal extends StatelessWidget {
|
||||
final Contact contact;
|
||||
final Function onEdit;
|
||||
final Function onToggleFavorite;
|
||||
final bool isFavorite;
|
||||
|
||||
const ContactModal({
|
||||
super.key,
|
||||
required this.contact,
|
||||
required this.onEdit,
|
||||
required this.onToggleFavorite,
|
||||
required this.isFavorite,
|
||||
});
|
||||
|
||||
void _launchPhoneDialer(String phoneNumber) async {
|
||||
final uri = Uri(scheme: 'tel', path: phoneNumber);
|
||||
if (await canLaunchUrl(uri)) {
|
||||
await launchUrl(uri);
|
||||
} else {
|
||||
debugPrint('Could not launch $phoneNumber');
|
||||
}
|
||||
}
|
||||
|
||||
void _launchSms(String phoneNumber) async {
|
||||
final uri = Uri(scheme: 'sms', path: phoneNumber);
|
||||
if (await canLaunchUrl(uri)) {
|
||||
await launchUrl(uri);
|
||||
} else {
|
||||
debugPrint('Could not launch SMS to $phoneNumber');
|
||||
}
|
||||
}
|
||||
|
||||
void _launchEmail(String email) async {
|
||||
final uri = Uri(scheme: 'mailto', path: email);
|
||||
if (await canLaunchUrl(uri)) {
|
||||
await launchUrl(uri);
|
||||
} else {
|
||||
debugPrint('Could not launch email to $email');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
String phoneNumber = contact.phones.isNotEmpty
|
||||
? contact.phones.first.number
|
||||
: 'No phone number';
|
||||
String email =
|
||||
contact.emails.isNotEmpty ? contact.emails.first.address : 'No email';
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () => Navigator.of(context).pop(),
|
||||
child: Container(
|
||||
color: Colors.black.withOpacity(0.5),
|
||||
child: GestureDetector(
|
||||
onTap: () {},
|
||||
child: FractionallySizedBox(
|
||||
heightFactor: 0.7,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey[900],
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// Modal Handle
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 10),
|
||||
child: Container(
|
||||
width: 50,
|
||||
height: 5,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade300,
|
||||
borderRadius: BorderRadius.circular(5),
|
||||
),
|
||||
),
|
||||
),
|
||||
// Contact Profile
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
children: [
|
||||
CircleAvatar(
|
||||
radius: 50,
|
||||
backgroundImage: (contact.thumbnail != null &&
|
||||
contact.thumbnail!.isNotEmpty)
|
||||
? MemoryImage(contact.thumbnail!)
|
||||
: null,
|
||||
backgroundColor:
|
||||
generateColorFromName(contact.displayName),
|
||||
child: (contact.thumbnail == null ||
|
||||
contact.thumbnail!.isEmpty)
|
||||
? Text(
|
||||
contact.displayName.isNotEmpty
|
||||
? contact.displayName[0].toUpperCase()
|
||||
: '?',
|
||||
style: TextStyle(
|
||||
fontSize: 40, color: Colors.white),
|
||||
)
|
||||
: null,
|
||||
),
|
||||
SizedBox(height: 10),
|
||||
Text(
|
||||
contact.displayName,
|
||||
style: TextStyle(
|
||||
fontSize: 24, fontWeight: FontWeight.bold),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
// Contact Actions
|
||||
Divider(),
|
||||
ListTile(
|
||||
leading: Icon(Icons.phone, color: Colors.green),
|
||||
title: Text(phoneNumber),
|
||||
onTap: () {
|
||||
if (contact.phones.isNotEmpty) {
|
||||
_launchPhoneDialer(phoneNumber);
|
||||
}
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: Icon(Icons.message, color: Colors.blue),
|
||||
title: Text(phoneNumber),
|
||||
onTap: () {
|
||||
if (contact.phones.isNotEmpty) {
|
||||
_launchSms(phoneNumber);
|
||||
}
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: Icon(Icons.email, color: Colors.orange),
|
||||
title: Text(email),
|
||||
onTap: () {
|
||||
if (contact.emails.isNotEmpty) {
|
||||
_launchEmail(email);
|
||||
}
|
||||
},
|
||||
),
|
||||
Divider(),
|
||||
// Favorite and Edit Buttons
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
ElevatedButton.icon(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
onToggleFavorite();
|
||||
},
|
||||
icon: Icon(contact.isStarred
|
||||
? Icons.star
|
||||
: Icons.star_border),
|
||||
label: Text(
|
||||
contact.isStarred ? 'Unfavorite' : 'Favorite'),
|
||||
),
|
||||
ElevatedButton.icon(
|
||||
onPressed: () => onEdit(),
|
||||
icon: Icon(Icons.edit),
|
||||
label: Text('Edit Contact'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(height: 16),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -34,6 +34,7 @@ dependencies:
|
||||
|
||||
# The following adds the Cupertino Icons font to your application.
|
||||
# Use with the CupertinoIcons class for iOS style icons.
|
||||
url_launcher: ^6.1.9 # To manage system dialer. Call, message...
|
||||
cupertino_icons: ^1.0.8
|
||||
flutter_contacts: ^1.1.9+2
|
||||
permission_handler: ^11.3.1 # For handling permissions
|
||||
|
Loading…
Reference in New Issue
Block a user