Compare commits

..

No commits in common. "f3c092d5b8c1150488c6879b4cca7792a85c4d70" and "3f6ea2e332831481f967d28e842af40af7fe3182" have entirely different histories.

11 changed files with 29 additions and 510 deletions

View File

@ -1,8 +1,6 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"> <manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.READ_CONTACTS"/> <uses-permission android:name="android.permission.READ_CONTACTS"/>
<uses-permission android:name="android.permission.WRITE_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" /> <uses-feature android:name="android.hardware.camera" android:required="false" />
<!-- The INTERNET permission is required for development. Specifically, <!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application the Flutter tool needs it to communicate with the running application

View File

@ -1,8 +1,6 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"> <manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.READ_CONTACTS"/> <uses-permission android:name="android.permission.READ_CONTACTS"/>
<uses-permission android:name="android.permission.WRITE_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 <application
android:label="com.example.dialer" android:label="com.example.dialer"
android:name="${applicationName}" android:name="${applicationName}"

View File

@ -1,8 +1,6 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"> <manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.READ_CONTACTS"/> <uses-permission android:name="android.permission.READ_CONTACTS"/>
<uses-permission android:name="android.permission.WRITE_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-permission android:name="android.permission.CAMERA"/>
<uses-feature android:name="android.hardware.camera" android:required="false" /> <uses-feature android:name="android.hardware.camera" android:required="false" />
<!-- The INTERNET permission is required for development. Specifically, <!-- The INTERNET permission is required for development. Specifically,

View File

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

View File

@ -4,7 +4,7 @@ import 'package:flutter_contacts/flutter_contacts.dart';
class ContactService { class ContactService {
Future<List<Contact>> fetchContacts() async { Future<List<Contact>> fetchContacts() async {
if (await FlutterContacts.requestPermission()) { if (await FlutterContacts.requestPermission()) {
return await FlutterContacts.getContacts(withProperties: true, withThumbnail: true, withAccounts: true, withGroups: true, withPhoto: true); return await FlutterContacts.getContacts(withProperties: true, withThumbnail: true, withAccounts: true, withGroups: true);
} }
return []; return [];
} }

View File

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

View File

@ -3,17 +3,14 @@ import 'package:flutter/material.dart';
import 'package:flutter_contacts/flutter_contacts.dart'; import 'package:flutter_contacts/flutter_contacts.dart';
import '../../../widgets/color_darkener.dart'; import '../../../widgets/color_darkener.dart';
import '../contact_state.dart'; import '../contact_state.dart';
import '../../../widgets/contact_service.dart';
import 'add_contact_button.dart'; import 'add_contact_button.dart';
import 'contact_modal.dart';
import 'share_own_qr.dart'; import 'share_own_qr.dart';
class AlphabetScrollPage extends StatefulWidget { class AlphabetScrollPage extends StatefulWidget {
final List<Contact> contacts; final List<Contact> contacts;
final double scrollOffset; final double scrollOffset;
const AlphabetScrollPage( const AlphabetScrollPage({super.key, required this.contacts, required this.scrollOffset});
{super.key, required this.contacts, required this.scrollOffset});
@override @override
_AlphabetScrollPageState createState() => _AlphabetScrollPageState(); _AlphabetScrollPageState createState() => _AlphabetScrollPageState();
@ -21,15 +18,11 @@ class AlphabetScrollPage extends StatefulWidget {
class _AlphabetScrollPageState extends State<AlphabetScrollPage> { class _AlphabetScrollPageState extends State<AlphabetScrollPage> {
late ScrollController _scrollController; late ScrollController _scrollController;
List<Contact> _contacts = []; // Local copy of contacts for updating
final ContactService _contactService = ContactService();
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_contacts = widget.contacts; // Initialize with the provided contacts _scrollController = ScrollController(initialScrollOffset: widget.scrollOffset);
_scrollController =
ScrollController(initialScrollOffset: widget.scrollOffset);
_scrollController.addListener(_onScroll); _scrollController.addListener(_onScroll);
} }
@ -38,59 +31,11 @@ class _AlphabetScrollPageState extends State<AlphabetScrollPage> {
contactState.setScrollOffset(_scrollController.offset); 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
Map<String, List<Contact>> alphabetizedContacts = {}; Map<String, List<Contact>> alphabetizedContacts = {};
for (var contact in _contacts) { for (var contact in widget.contacts) {
String firstLetter = contact.displayName.isNotEmpty String firstLetter = contact.displayName.isNotEmpty ? contact.displayName[0].toUpperCase() : '#';
? contact.displayName[0].toUpperCase()
: '#';
if (!alphabetizedContacts.containsKey(firstLetter)) { if (!alphabetizedContacts.containsKey(firstLetter)) {
alphabetizedContacts[firstLetter] = []; alphabetizedContacts[firstLetter] = [];
} }
@ -102,19 +47,15 @@ class _AlphabetScrollPageState extends State<AlphabetScrollPage> {
return Scaffold( return Scaffold(
backgroundColor: Colors.black, backgroundColor: Colors.black,
body: Column( body: Column(
children: [ children: [ // Top buttons row
// Top buttons row
Container( Container(
color: Colors.black, color: Colors.black,
padding: padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
AddContactButton(), AddContactButton(),
QRCodeButton( QRCodeButton(contacts: widget.contacts, selfContact: ContactState.of(context).selfContact),
contacts: _contacts,
selfContact: ContactState.of(context).selfContact),
], ],
), ),
), ),
@ -131,8 +72,7 @@ class _AlphabetScrollPageState extends State<AlphabetScrollPage> {
children: [ children: [
// Alphabet Letter Header // Alphabet Letter Header
Padding( Padding(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 16.0),
vertical: 8.0, horizontal: 16.0),
child: Text( child: Text(
letter, letter,
style: TextStyle( style: TextStyle(
@ -144,75 +84,24 @@ class _AlphabetScrollPageState extends State<AlphabetScrollPage> {
), ),
// Contact Entries // Contact Entries
...contacts.map((contact) { ...contacts.map((contact) {
String phoneNumber = contact.phones.isNotEmpty String phoneNumber = contact.phones.isNotEmpty ? contact.phones.first.number : 'No phone number';
? contact.phones.first.number Color avatarColor = generateColorFromName(contact.displayName);
: 'No phone number';
Color avatarColor =
generateColorFromName(contact.displayName);
return ListTile( return ListTile(
leading: (contact.thumbnail != null && leading: (contact.thumbnail != null && contact.thumbnail!.isNotEmpty)
contact.thumbnail!.isNotEmpty)
? CircleAvatar( ? CircleAvatar(
backgroundImage: backgroundImage: MemoryImage(contact.thumbnail!),
MemoryImage(contact.thumbnail!), )
)
: CircleAvatar( : CircleAvatar(
backgroundColor: avatarColor, backgroundColor: avatarColor,
child: Text( child: Text(
contact.displayName.isNotEmpty contact.displayName.isNotEmpty ? contact.displayName[0].toUpperCase() : '?',
? contact.displayName[0].toUpperCase() style: TextStyle(color: darken(avatarColor, 0.4)),
: '?', ),
style: TextStyle( ),
color: darken(avatarColor, 0.4)), title: Text(contact.displayName, style: TextStyle(color: Colors.white)),
), subtitle: Text(phoneNumber, style: TextStyle(color: Colors.white70)),
),
title: Text(contact.displayName,
style: TextStyle(color: Colors.white)),
subtitle: Text(phoneNumber,
style: TextStyle(color: Colors.white70)),
onTap: () { onTap: () {
showModalBottomSheet( // Handle contact tap
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

@ -1,180 +0,0 @@
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,183 +1,3 @@
// 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'; import 'package:flutter/material.dart';
class FavoritePage extends StatefulWidget { class FavoritePage extends StatefulWidget {

View File

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

View File

@ -34,7 +34,6 @@ dependencies:
# The following adds the Cupertino Icons font to your application. # The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons. # 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 cupertino_icons: ^1.0.8
flutter_contacts: ^1.1.9+2 flutter_contacts: ^1.1.9+2
permission_handler: ^11.3.1 # For handling permissions permission_handler: ^11.3.1 # For handling permissions