Compare commits

...

5 Commits

Author SHA1 Message Date
179f2015bc merge
All checks were successful
/ mirror (push) Successful in 4s
2024-12-18 14:27:42 +00:00
d8c9585f85 feat: Composition page (#14)
All checks were successful
/ mirror (push) Successful in 5s
Composition page

Reviewed-on: #14
Co-authored-by: Florian Griffon <florian.griffon@epitech.eu>
Co-committed-by: Florian Griffon <florian.griffon@epitech.eu>
2024-12-15 19:08:38 +00:00
09fa0a0216 favorite-page (#12)
All checks were successful
/ mirror (push) Successful in 4s
Feat: favorite page
Reviewed-on: #12
Co-authored-by: Florian Griffon <florian.griffon@epitech.eu>
Co-committed-by: Florian Griffon <florian.griffon@epitech.eu>
2024-12-15 18:42:23 +00:00
21b6b0a29a SafeArea (#11)
All checks were successful
/ mirror (push) Successful in 4s
Co-authored-by: AlexisDanlos <91090088+AlexisDanlos@users.noreply.github.com>
Reviewed-on: #11
2024-12-15 18:00:24 +00:00
fca1eea1c9 contact-modal (#10)
All checks were successful
/ mirror (push) Successful in 4s
Feat: contact-modal et refonte du contact-state pour la page de favori et possibilité de update les contacts. Aussi mis la composition page avec le service de contact, on évite de fetch dans la page directement
Reviewed-on: #10
Co-authored-by: Florian Griffon <florian.griffon@epitech.eu>
Co-committed-by: Florian Griffon <florian.griffon@epitech.eu>
2024-12-15 17:51:56 +00:00
14 changed files with 602 additions and 186 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,8 @@
import 'package:flutter/material.dart';
import 'package:flutter_contacts/flutter_contacts.dart';
import 'package:url_launcher/url_launcher.dart';
import '../../widgets/contact_service.dart';
import '../contacts/widgets/add_contact_button.dart';
class CompositionPage extends StatefulWidget {
const CompositionPage({super.key});
@ -12,6 +15,7 @@ class _CompositionPageState extends State<CompositionPage> {
String dialedNumber = "";
List<Contact> _allContacts = [];
List<Contact> _filteredContacts = [];
final ContactService _contactService = ContactService();
@override
void initState() {
@ -20,11 +24,9 @@ class _CompositionPageState extends State<CompositionPage> {
}
Future<void> _fetchContacts() async {
if (await FlutterContacts.requestPermission()) {
_allContacts = await FlutterContacts.getContacts(withProperties: true);
_filteredContacts = _allContacts;
setState(() {});
}
_allContacts = await _contactService.fetchContacts();
_filteredContacts = _allContacts;
setState(() {});
}
void _filterContacts() {
@ -63,9 +65,24 @@ class _CompositionPageState extends State<CompositionPage> {
});
}
// Placeholder function for adding contact
void addContact(String number) {
// This function is empty for now
// Function to call a contact's number
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');
}
}
// Function to send an SMS to a contact's number
void _launchSms(String phoneNumber) async {
final uri = Uri(scheme: 'sms', path: phoneNumber);
if (await canLaunchUrl(uri)) {
await launchUrl(uri);
} else {
debugPrint('Could not send SMS to $phoneNumber');
}
}
@override
@ -79,9 +96,9 @@ class _CompositionPageState extends State<CompositionPage> {
// Top half: Display contacts matching dialed number
Expanded(
flex: 2,
child:
Container(
padding: const EdgeInsets.only(top: 42.0, left: 16.0, right: 16.0, bottom: 16.0),
child: Container(
padding: const EdgeInsets.only(
top: 42.0, left: 16.0, right: 16.0, bottom: 16.0),
color: Colors.black,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
@ -90,32 +107,39 @@ class _CompositionPageState extends State<CompositionPage> {
child: ListView(
children: _filteredContacts.isNotEmpty
? _filteredContacts.map((contact) {
final phoneNumber = contact.phones.isNotEmpty
? contact.phones.first.number
: 'No phone number';
return ListTile(
title: Text(
contact.displayName,
style: const TextStyle(color: Colors.white),
style:
const TextStyle(color: Colors.white),
),
subtitle: Text(
phoneNumber,
style:
const TextStyle(color: Colors.grey),
),
subtitle: contact.phones.isNotEmpty
? Text(
contact.phones.first.number,
style: const TextStyle(color: Colors.grey),
)
: null,
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
// Call button
IconButton(
icon: Icon(Icons.phone, color: Colors.green[300], size: 20),
icon: Icon(Icons.phone,
color: Colors.green[300],
size: 20),
onPressed: () {
print('Calling ${contact.displayName}');
_launchPhoneDialer(phoneNumber);
},
),
// Text button
// Message button
IconButton(
icon: Icon(Icons.message, color: Colors.blue[300], size: 20),
icon: Icon(Icons.message,
color: Colors.blue[300],
size: 20),
onPressed: () {
print('Texting ${contact.displayName}');
_launchSms(phoneNumber);
},
),
],
@ -125,7 +149,12 @@ class _CompositionPageState extends State<CompositionPage> {
},
);
}).toList()
: [Center(child: Text('No contacts found', style: TextStyle(color: Colors.white)))],
: [
Center(
child: Text('No contacts found',
style:
TextStyle(color: Colors.white)))
],
),
),
],
@ -150,14 +179,16 @@ class _CompositionPageState extends State<CompositionPage> {
alignment: Alignment.center,
child: Text(
dialedNumber,
style: const TextStyle(fontSize: 24, color: Colors.white),
style: const TextStyle(
fontSize: 24, color: Colors.white),
overflow: TextOverflow.ellipsis,
),
),
),
IconButton(
onPressed: _onClearPress,
icon: const Icon(Icons.backspace, color: Colors.white),
icon: const Icon(Icons.backspace,
color: Colors.white),
),
],
),
@ -170,7 +201,8 @@ class _CompositionPageState extends State<CompositionPage> {
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
mainAxisAlignment:
MainAxisAlignment.spaceEvenly,
children: [
_buildDialButton('1'),
_buildDialButton('2'),
@ -178,7 +210,8 @@ class _CompositionPageState extends State<CompositionPage> {
],
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
mainAxisAlignment:
MainAxisAlignment.spaceEvenly,
children: [
_buildDialButton('4'),
_buildDialButton('5'),
@ -186,7 +219,8 @@ class _CompositionPageState extends State<CompositionPage> {
],
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
mainAxisAlignment:
MainAxisAlignment.spaceEvenly,
children: [
_buildDialButton('7'),
_buildDialButton('8'),
@ -194,7 +228,8 @@ class _CompositionPageState extends State<CompositionPage> {
],
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
mainAxisAlignment:
MainAxisAlignment.spaceEvenly,
children: [
_buildDialButton('*'),
_buildDialButton('0'),
@ -209,20 +244,19 @@ class _CompositionPageState extends State<CompositionPage> {
),
),
),
// Add Contact Button with empty function call
Padding(
padding: const EdgeInsets.only(bottom: 20.0),
child: FloatingActionButton(
backgroundColor: Colors.blue,
onPressed: () {
addContact(dialedNumber);
},
child: const Icon(Icons.person_add, color: Colors.white),
),
),
],
),
// Add Contact Button
Positioned(
bottom: 20.0,
left: 0,
right: 0,
child: Center(
child: AddContactButton(),
),
),
// Top Row with Back Arrow
Positioned(
top: 40.0,

View File

@ -17,8 +17,10 @@ class _ContactPageState extends State<ContactPage> {
return Scaffold(
body: contactState.loading
? const LoadingIndicatorWidget()
// : ContactListWidget(contacts: contactState.contacts),
: AlphabetScrollPage(contacts: contactState.contacts, scrollOffset: contactState.scrollOffset),
: AlphabetScrollPage(
scrollOffset: contactState.scrollOffset,
contacts: contactState.contacts, // Use all contacts here
),
);
}
}

View File

@ -1,15 +0,0 @@
import 'package:flutter_contacts/flutter_contacts.dart';
// Service to manage contact-related operations
class ContactService {
Future<List<Contact>> fetchContacts() async {
if (await FlutterContacts.requestPermission()) {
return await FlutterContacts.getContacts(withProperties: true, withThumbnail: true, withAccounts: true, withGroups: true);
}
return [];
}
Future<void> addNewContact(Contact contact) async {
await FlutterContacts.insertContact(contact);
}
}

View File

@ -1,99 +1,122 @@
import 'package:flutter/material.dart';
import 'package:flutter_contacts/flutter_contacts.dart';
import 'contact_service.dart';
import 'package:flutter/material.dart';
import 'package:flutter_contacts/flutter_contacts.dart';
import '../../widgets/contact_service.dart';
class ContactState extends StatefulWidget {
final Widget child;
class ContactState extends StatefulWidget {
final Widget child;
const ContactState({super.key, required this.child});
const ContactState({super.key, required this.child});
static _ContactStateState of(BuildContext context) {
return context.dependOnInheritedWidgetOfExactType<_InheritedContactState>()!.data;
}
@override
_ContactStateState createState() => _ContactStateState();
static _ContactStateState of(BuildContext context) {
return context
.dependOnInheritedWidgetOfExactType<_InheritedContactState>()!
.data;
}
class _ContactStateState extends State<ContactState> {
final ContactService _contactService = ContactService();
List<Contact> _contacts = [];
bool _loading = true;
double _scrollOffset = 0.0;
Contact? _selfContact = Contact();
@override
_ContactStateState createState() => _ContactStateState();
}
List<Contact> get contacts => _contacts;
bool get loading => _loading;
double get scrollOffset => _scrollOffset;
Contact? get selfContact => _selfContact;
class _ContactStateState extends State<ContactState> {
final ContactService _contactService = ContactService();
List<Contact> _allContacts = [];
List<Contact> _favoriteContacts = [];
bool _loading = true;
double _scrollOffset = 0.0;
Contact? _selfContact = Contact();
@override
void initState() {
super.initState();
_fetchContacts();
// Getters for all contacts and favorites
List<Contact> get contacts => _allContacts;
List<Contact> get favoriteContacts => _favoriteContacts;
bool get loading => _loading;
double get scrollOffset => _scrollOffset;
Contact? get selfContact => _selfContact;
// Add listener for contact changes
FlutterContacts.addListener(_onContactChange);
}
@override
void initState() {
super.initState();
fetchContacts(); // Fetch all contacts by default
FlutterContacts.addListener(_onContactChange);
}
void _onContactChange() => _fetchContacts();
void _onContactChange() => fetchContacts();
@override
void dispose() {
// Remove listener
FlutterContacts.removeListener(_onContactChange);
super.dispose();
}
@override
void dispose() {
FlutterContacts.removeListener(_onContactChange);
super.dispose();
}
Future<void> _fetchContacts() async {
// Fetch all contacts
Future<void> fetchContacts() async {
setState(() => _loading = true);
try {
List<Contact> contacts = await _contactService.fetchContacts();
debugPrint("Fetched ${contacts.length} contacts");
// Find selfContact before filtering
_selfContact = contacts.firstWhere(
(contact) => contact.displayName.toLowerCase() == "user",
orElse: () => Contact(),
);
if (_selfContact!.phones.isEmpty) {
debugPrint("Self contact has no phone numbers");
_selfContact = null;
}
contacts = contacts.where((contact) => contact.phones.isNotEmpty).toList();
contacts.sort((a, b) => a.displayName.compareTo(b.displayName));
setState(() {
_contacts = contacts;
_loading = false;
_selfContact = _selfContact;
});
}
Future<void> addNewContact(Contact contact) async {
await _contactService.addNewContact(contact);
await _fetchContacts();
}
void setScrollOffset(double offset) {
setState(() {
_scrollOffset = offset;
});
}
@override
Widget build(BuildContext context) {
return _InheritedContactState(
data: this,
child: widget.child,
);
_processContacts(contacts);
} finally {
setState(() => _loading = false);
}
}
class _InheritedContactState extends InheritedWidget {
final _ContactStateState data;
const _InheritedContactState({required this.data, required super.child});
@override
bool updateShouldNotify(_InheritedContactState oldWidget) => true;
// Fetch only favorite contacts
Future<void> fetchFavoriteContacts() async {
setState(() => _loading = true);
try {
List<Contact> contacts = await _contactService.fetchFavoriteContacts();
setState(() => _favoriteContacts = contacts);
} finally {
setState(() => _loading = false);
}
}
void _processContacts(List<Contact> contacts) {
_selfContact = contacts.firstWhere(
(contact) => contact.displayName.toLowerCase() == "user",
orElse: () => Contact(),
);
if (_selfContact!.phones.isEmpty) {
debugPrint("Self contact has no phone numbers");
_selfContact = null;
}
contacts = contacts.where((contact) => contact.phones.isNotEmpty).toList();
contacts.sort((a, b) => a.displayName.compareTo(b.displayName));
setState(() {
_allContacts = contacts;
_favoriteContacts =
contacts.where((contact) => contact.isStarred).toList();
_selfContact = _selfContact;
});
}
Future<void> addNewContact(Contact contact) async {
await _contactService.addNewContact(contact);
await fetchContacts();
}
void setScrollOffset(double offset) {
setState(() {
_scrollOffset = offset;
});
}
@override
Widget build(BuildContext context) {
return _InheritedContactState(
data: this,
child: widget.child,
);
}
}
class _InheritedContactState extends InheritedWidget {
final _ContactStateState data;
const _InheritedContactState({required this.data, required super.child});
@override
bool updateShouldNotify(_InheritedContactState oldWidget) => true;
}

View File

@ -1,16 +1,21 @@
import 'package:dialer/widgets/username_color_generator.dart';
import 'package:flutter/material.dart';
import 'package:flutter_contacts/flutter_contacts.dart';
import '../../../widgets/color_darkener.dart';
import '../contact_state.dart';
import '../../../widgets/color_darkener.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;
final List<Contact> contacts;
const AlphabetScrollPage({super.key, required this.contacts, required this.scrollOffset});
const AlphabetScrollPage({
super.key,
required this.scrollOffset,
required this.contacts,
});
@override
_AlphabetScrollPageState createState() => _AlphabetScrollPageState();
@ -31,11 +36,53 @@ class _AlphabetScrollPageState extends State<AlphabetScrollPage> {
contactState.setScrollOffset(_scrollController.offset);
}
Future<void> _refreshContacts() async {
final contactState = ContactState.of(context);
try {
await contactState.fetchContacts();
} catch (e) {
print('Error refreshing contacts: $e');
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Failed to refresh contacts')),
);
}
}
void _toggleFavorite(Contact contact) async {
try {
if (await FlutterContacts.requestPermission()) {
Contact? fullContact = await FlutterContacts.getContact(contact.id,
withProperties: true,
withAccounts: true,
withPhoto: true,
withThumbnail: true);
if (fullContact != null) {
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");
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Failed to update contact favorite status')),
);
}
}
@override
Widget build(BuildContext context) {
final contacts = widget.contacts;
final selfContact = ContactState.of(context).selfContact;
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,7 +94,8 @@ 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),
@ -55,7 +103,7 @@ class _AlphabetScrollPageState extends State<AlphabetScrollPage> {
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
AddContactButton(),
QRCodeButton(contacts: widget.contacts, selfContact: ContactState.of(context).selfContact),
QRCodeButton(contacts: contacts, selfContact: selfContact),
],
),
),
@ -66,13 +114,14 @@ class _AlphabetScrollPageState extends State<AlphabetScrollPage> {
itemCount: alphabetKeys.length,
itemBuilder: (context, index) {
String letter = alphabetKeys[index];
List<Contact> contacts = alphabetizedContacts[letter]!;
List<Contact> contactsForLetter = alphabetizedContacts[letter]!;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
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(
@ -83,25 +132,74 @@ 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);
...contactsForLetter.map((contact) {
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 {
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,227 @@
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
// Top Bar with Handle and Three-Dot Menu
Stack(
children: [
Align(
alignment: Alignment.center,
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 10),
child: Container(
width: 50,
height: 5,
decoration: BoxDecoration(
color: Colors.grey.shade300,
borderRadius: BorderRadius.circular(5),
),
),
),
),
Align(
alignment: Alignment.topRight,
child: Padding(
padding: const EdgeInsets.only(top: 10, right: 10),
child: PopupMenuButton<String>(
icon: Icon(Icons.more_vert, color: Colors.white),
onSelected: (String choice) {
print(
'Selected: $choice'); // Placeholder for menu actions
},
itemBuilder: (BuildContext context) {
return <PopupMenuEntry<String>>[
PopupMenuItem<String>(
value: 'show_associated_contacts',
child: Text('Show associated contacts'),
),
PopupMenuItem<String>(
value: 'delete',
child: Text('Delete'),
),
PopupMenuItem<String>(
value: 'share',
child: Text('Share (via QR code)'),
),
PopupMenuItem<String>(
value: 'create_shortcut',
child:
Text('Create shortcut (to home screen)'),
),
PopupMenuItem<String>(
value: 'set_ringtone',
child: Text('Set ringtone'),
),
];
},
),
),
),
],
),
// 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,23 +1,32 @@
import 'package:dialer/features/contacts/contact_state.dart';
import 'package:dialer/features/contacts/widgets/alphabet_scroll_page.dart';
import 'package:flutter/material.dart';
import 'package:dialer/widgets/loading_indicator.dart';
class FavoritePage extends StatefulWidget {
const FavoritePage({super.key});
class FavoritesPage extends StatefulWidget {
const FavoritesPage({super.key});
@override
_FavoritePageState createState() => _FavoritePageState();
_FavoritesPageState createState() => _FavoritesPageState();
}
class _FavoritePageState extends State<FavoritePage> {
class _FavoritesPageState extends State<FavoritesPage> {
@override
void initState() {
super.initState();
}
@override
Widget build(BuildContext context) {
final contactState = ContactState.of(context);
return Scaffold(
backgroundColor: Colors.black,
body: Center( // Center the text within the body
child: Text(
"Hello",
style: TextStyle(color: Colors.white), // Change text color for visibility
),
),
body: contactState.loading
? const LoadingIndicatorWidget()
: AlphabetScrollPage(
scrollOffset: contactState.scrollOffset,
contacts:
contactState.favoriteContacts, // Use only favorites here
),
);
}
}

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() {
@ -22,10 +25,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) {
@ -61,7 +62,7 @@ class _MyHomePageState extends State<MyHomePage>
backgroundColor: Colors.black,
body: Column(
children: [
// Search Bar and 3-dot menu
// Persistent Search Bar
Padding(
padding: const EdgeInsets.only(
top: 24.0,
@ -169,7 +170,7 @@ class _MyHomePageState extends State<MyHomePage>
TabBarView(
controller: _tabController,
children: const [
FavoritePage(),
FavoritesPage(),
HistoryPage(),
ContactPage(),
],

View File

@ -34,7 +34,7 @@ class KeyManagementPage extends StatelessWidget {
MaterialPageRoute(builder: (context) => const ExportPrivateKeyPage()),
);
break;
case 'Delete a key pair, warning POPUP':
case 'Delete a key pair':
Navigator.push(
context,
MaterialPageRoute(builder: (context) => const DeleteKeyPairPage()),
@ -52,7 +52,7 @@ class KeyManagementPage extends StatelessWidget {
'Display public key as QR code',
'Generate a new key pair',
'Export private key to password-encrypted file (AES 256)',
'Delete a key pair, warning POPUP',
'Delete a key pair',
];
return Scaffold(

View File

@ -16,7 +16,7 @@ class MyApp extends StatelessWidget {
theme: ThemeData(
brightness: Brightness.dark
),
home: const MyHomePage(),
home: SafeArea(child: MyHomePage()),
)
);
}

View File

@ -0,0 +1,31 @@
import 'package:flutter_contacts/flutter_contacts.dart';
// Service to manage contact-related operations
class ContactService {
Future<List<Contact>> fetchContacts() async {
if (await FlutterContacts.requestPermission()) {
return await FlutterContacts.getContacts(
withProperties: true,
withThumbnail: true,
withAccounts: true,
withGroups: true,
withPhoto: true);
}
return [];
}
Future<List<Contact>> fetchFavoriteContacts() async {
// Fetch all contacts
List<Contact> contacts = await fetchContacts();
// Filter contacts to only include those with isStarred: true
List<Contact> favoriteContacts =
contacts.where((contact) => contact.isStarred).toList();
return favoriteContacts;
}
Future<void> addNewContact(Contact contact) async {
await FlutterContacts.insertContact(contact);
}
}