Compare commits

...

2 Commits

Author SHA1 Message Date
f3c092d5b8 feat: all getContacts use the contactService | can favorite a contact
All checks were successful
/ mirror (push) Successful in 5s
2024-12-12 17:47:49 +01:00
a64b32d114 feat: WIP contact-modal 2024-12-11 21:54:20 +01:00
11 changed files with 510 additions and 29 deletions

View File

@ -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

View File

@ -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}"

View File

@ -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,

View File

@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter_contacts/flutter_contacts.dart';
import '../../widgets/contact_service.dart';
class CompositionPage extends StatefulWidget {
const CompositionPage({super.key});
@ -12,6 +13,7 @@ class _CompositionPageState extends State<CompositionPage> {
String dialedNumber = "";
List<Contact> _allContacts = [];
List<Contact> _filteredContacts = [];
final ContactService _contactService = ContactService();
@override
void initState() {
@ -21,7 +23,7 @@ class _CompositionPageState extends State<CompositionPage> {
Future<void> _fetchContacts() async {
if (await FlutterContacts.requestPermission()) {
_allContacts = await FlutterContacts.getContacts(withProperties: true);
_allContacts = await _contactService.fetchContacts();
_filteredContacts = _allContacts;
setState(() {});
}

View File

@ -1,6 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter_contacts/flutter_contacts.dart';
import 'contact_service.dart';
import '../../widgets/contact_service.dart';
class ContactState extends StatefulWidget {
final Widget child;

View File

@ -3,14 +3,17 @@ import 'package:flutter/material.dart';
import 'package:flutter_contacts/flutter_contacts.dart';
import '../../../widgets/color_darkener.dart';
import '../contact_state.dart';
import '../../../widgets/contact_service.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 +21,15 @@ class AlphabetScrollPage extends StatefulWidget {
class _AlphabetScrollPageState extends State<AlphabetScrollPage> {
late ScrollController _scrollController;
List<Contact> _contacts = []; // Local copy of contacts for updating
final ContactService _contactService = ContactService();
@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 +38,59 @@ class _AlphabetScrollPageState extends State<AlphabetScrollPage> {
contactState.setScrollOffset(_scrollController.offset);
}
Future<void> _refreshContacts() async {
try {
// Use the fetchContacts method from ContactService
final updatedContacts = await _contactService.fetchContacts();
if (mounted) {
setState(() {
_contacts = updatedContacts;
});
}
} catch (e) {
print('Error refreshing contacts: $e');
// Optionally show a user-friendly error message
ScaffoldMessenger.of(context)
.showSnackBar(SnackBar(content: Text('Failed to refresh contacts')));
}
}
void _toggleFavorite(Contact contact) async {
try {
// Request permission first
if (await FlutterContacts.requestPermission()) {
// Fetch the full contact details with all available properties
Contact? fullContact = await FlutterContacts.getContact(contact.id,
withProperties: true,
withAccounts: true,
withPhoto: true,
withThumbnail: true);
if (fullContact != null) {
// Update the contact
fullContact.isStarred = !fullContact.isStarred;
await FlutterContacts.updateContact(fullContact);
}
await _refreshContacts();
} else {
print("Could not fetch contact details");
}
} catch (e) {
print("Error updating favorite status: $e");
// Optional: Show a user-friendly error message
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Failed to update contact favorite status')));
}
}
@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 +102,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 +131,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 +144,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,
);
},
);
},
);
}),

View 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),
],
),
),
),
),
),
);
}
}

View File

@ -1,3 +1,183 @@
// import 'package:flutter/material.dart';
// import 'package:flutter_contacts/flutter_contacts.dart';
// import '../../../widgets/color_darkener.dart';
// import '../contacts/contact_state.dart';
// import '../contacts/widgets/add_contact_button.dart';
// import '../contacts/widgets/contact_modal.dart';
// import '../contacts/widgets/share_own_qr.dart';
// import 'package:dialer/widgets/username_color_generator.dart';
// class FavoritePage extends StatefulWidget {
// final List<Contact> contacts;
// final double scrollOffset;
// const FavoritePage({
// super.key,
// required this.contacts,
// required this.scrollOffset,
// });
// @override
// _FavoritePageState createState() => _FavoritePageState();
// }
// class _FavoritePageState extends State<FavoritePage> {
// late ScrollController _scrollController;
// List<Contact> _favoriteContacts = []; // Local list of favorite contacts
// @override
// void initState() {
// super.initState();
// _favoriteContacts = widget.contacts.where((contact) => contact.isStarred).toList(); // Filter only favorites
// _scrollController = ScrollController(initialScrollOffset: widget.scrollOffset);
// _scrollController.addListener(_onScroll);
// }
// void _onScroll() {
// final contactState = ContactState.of(context);
// contactState.setScrollOffset(_scrollController.offset);
// }
// Future<void> _refreshContacts() async {
// if (await FlutterContacts.requestPermission()) {
// final updatedContacts = await FlutterContacts.getContacts(
// withProperties: true,
// withThumbnail: true,
// );
// setState(() {
// _favoriteContacts = updatedContacts.where((contact) => contact.isStarred).toList();
// });
// }
// }
// void _toggleFavorite(Contact contact) async {
// 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,
// );
// // Find the specific contact by matching contact.id
// Contact? contactToUpdate = allContacts.firstWhere(
// (c) => c.id == contact.id,
// orElse: () => throw Exception("Contact not found"),
// );
// if (contactToUpdate != null) {
// contactToUpdate.isStarred = !contactToUpdate.isStarred;
// // Update the contact
// await FlutterContacts.updateContact(contactToUpdate);
// // Refresh the favorite contacts list
// setState(() {
// _favoriteContacts = allContacts.where((c) => c.isStarred).toList();
// });
// }
// } catch (e) {
// print("Error updating favorite status: $e");
// }
// }
// }
// @override
// Widget build(BuildContext context) {
// return Scaffold(
// backgroundColor: Colors.black,
// appBar: AppBar(
// title: const Text('Favorites'),
// backgroundColor: Colors.black,
// actions: [
// IconButton(
// icon: const Icon(Icons.refresh),
// onPressed: _refreshContacts,
// ),
// ],
// ),
// body: _favoriteContacts.isEmpty
// ? Center(
// child: Text(
// 'No favorite contacts yet!',
// style: TextStyle(color: Colors.white),
// ),
// )
// : ListView.builder(
// controller: _scrollController,
// itemCount: _favoriteContacts.length,
// itemBuilder: (context, index) {
// Contact contact = _favoriteContacts[index];
// 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)
// ? CircleAvatar(
// 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)),
// onTap: () {
// 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,
// );
// },
// );
// },
// );
// },
// ),
// );
// }
// @override
// void dispose() {
// _scrollController.dispose();
// super.dispose();
// }
// }
import 'package:flutter/material.dart';
class FavoritePage extends StatefulWidget {

View File

@ -5,12 +5,15 @@ import 'package:dialer/features/history/history_page.dart';
import 'package:dialer/features/composition/composition.dart';
import 'package:flutter_contacts/flutter_contacts.dart';
import 'package:dialer/features/settings/settings.dart';
import '../../widgets/contact_service.dart';
class _MyHomePageState extends State<MyHomePage>
with SingleTickerProviderStateMixin {
late TabController _tabController;
List<Contact> _allContacts = [];
List<Contact> _contactSuggestions = [];
final ContactService _contactService = ContactService();
@override
void initState() {
@ -21,10 +24,8 @@ class _MyHomePageState extends State<MyHomePage>
}
void _fetchContacts() async {
if (await FlutterContacts.requestPermission()) {
_allContacts = await FlutterContacts.getContacts(withProperties: true);
setState(() {});
}
_allContacts = await _contactService.fetchContacts();
setState(() {});
}
void _onSearchChanged(String query) {

View File

@ -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 [];
}

View File

@ -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