feat: settings blocked #15

Closed
florian wants to merge 13 commits from blocked-page into dev
16 changed files with 723 additions and 123 deletions

View File

@ -1,6 +1,10 @@
<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.READ_BLOCKED_NUMBERS" />
<uses-permission android:name="android.permission.WRITE_BLOCKED_NUMBERS" />
<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,6 +1,10 @@
<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.READ_BLOCKED_NUMBERS" />
<uses-permission android:name="android.permission.WRITE_BLOCKED_NUMBERS" />
<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,6 +1,10 @@
<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.READ_BLOCKED_NUMBERS" />
<uses-permission android:name="android.permission.WRITE_BLOCKED_NUMBERS" />
<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,5 +1,8 @@
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 'package:url_launcher/url_launcher.dart';
import '../../widgets/contact_service.dart';
import '../contacts/widgets/add_contact_button.dart';
class CompositionPage extends StatefulWidget { class CompositionPage extends StatefulWidget {
const CompositionPage({super.key}); const CompositionPage({super.key});
@ -12,6 +15,7 @@ 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() {
@ -20,11 +24,9 @@ class _CompositionPageState extends State<CompositionPage> {
} }
Future<void> _fetchContacts() async { Future<void> _fetchContacts() async {
if (await FlutterContacts.requestPermission()) { _allContacts = await _contactService.fetchContacts();
_allContacts = await FlutterContacts.getContacts(withProperties: true); _filteredContacts = _allContacts;
_filteredContacts = _allContacts; setState(() {});
setState(() {});
}
} }
void _filterContacts() { void _filterContacts() {
@ -63,9 +65,24 @@ class _CompositionPageState extends State<CompositionPage> {
}); });
} }
// Placeholder function for adding contact // Function to call a contact's number
void addContact(String number) { void _launchPhoneDialer(String phoneNumber) async {
// This function is empty for now 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 @override
@ -79,9 +96,9 @@ class _CompositionPageState extends State<CompositionPage> {
// Top half: Display contacts matching dialed number // Top half: Display contacts matching dialed number
Expanded( Expanded(
flex: 2, flex: 2,
child: child: Container(
Container( padding: const EdgeInsets.only(
padding: const EdgeInsets.only(top: 42.0, left: 16.0, right: 16.0, bottom: 16.0), top: 42.0, left: 16.0, right: 16.0, bottom: 16.0),
color: Colors.black, color: Colors.black,
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@ -90,32 +107,39 @@ class _CompositionPageState extends State<CompositionPage> {
child: ListView( child: ListView(
children: _filteredContacts.isNotEmpty children: _filteredContacts.isNotEmpty
? _filteredContacts.map((contact) { ? _filteredContacts.map((contact) {
final phoneNumber = contact.phones.isNotEmpty
? contact.phones.first.number
: 'No phone number';
return ListTile( return ListTile(
title: Text( title: Text(
contact.displayName, 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( trailing: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
// Call button // Call button
IconButton( IconButton(
icon: Icon(Icons.phone, color: Colors.green[300], size: 20), icon: Icon(Icons.phone,
color: Colors.green[300],
size: 20),
onPressed: () { onPressed: () {
print('Calling ${contact.displayName}'); _launchPhoneDialer(phoneNumber);
}, },
), ),
// Text button // Message button
IconButton( IconButton(
icon: Icon(Icons.message, color: Colors.blue[300], size: 20), icon: Icon(Icons.message,
color: Colors.blue[300],
size: 20),
onPressed: () { onPressed: () {
print('Texting ${contact.displayName}'); _launchSms(phoneNumber);
}, },
), ),
], ],
@ -125,7 +149,12 @@ class _CompositionPageState extends State<CompositionPage> {
}, },
); );
}).toList() }).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, alignment: Alignment.center,
child: Text( child: Text(
dialedNumber, dialedNumber,
style: const TextStyle(fontSize: 24, color: Colors.white), style: const TextStyle(
fontSize: 24, color: Colors.white),
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
), ),
), ),
IconButton( IconButton(
onPressed: _onClearPress, 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, mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [ children: [
Row( Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly, mainAxisAlignment:
MainAxisAlignment.spaceEvenly,
children: [ children: [
_buildDialButton('1'), _buildDialButton('1'),
_buildDialButton('2'), _buildDialButton('2'),
@ -178,7 +210,8 @@ class _CompositionPageState extends State<CompositionPage> {
], ],
), ),
Row( Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly, mainAxisAlignment:
MainAxisAlignment.spaceEvenly,
children: [ children: [
_buildDialButton('4'), _buildDialButton('4'),
_buildDialButton('5'), _buildDialButton('5'),
@ -186,7 +219,8 @@ class _CompositionPageState extends State<CompositionPage> {
], ],
), ),
Row( Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly, mainAxisAlignment:
MainAxisAlignment.spaceEvenly,
children: [ children: [
_buildDialButton('7'), _buildDialButton('7'),
_buildDialButton('8'), _buildDialButton('8'),
@ -194,7 +228,8 @@ class _CompositionPageState extends State<CompositionPage> {
], ],
), ),
Row( Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly, mainAxisAlignment:
MainAxisAlignment.spaceEvenly,
children: [ children: [
_buildDialButton('*'), _buildDialButton('*'),
_buildDialButton('0'), _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 // Top Row with Back Arrow
Positioned( Positioned(
top: 40.0, top: 40.0,

View File

@ -17,8 +17,10 @@ class _ContactPageState extends State<ContactPage> {
return Scaffold( return Scaffold(
body: contactState.loading body: contactState.loading
? const LoadingIndicatorWidget() ? const LoadingIndicatorWidget()
// : ContactListWidget(contacts: contactState.contacts), : AlphabetScrollPage(
: AlphabetScrollPage(contacts: contactState.contacts, scrollOffset: contactState.scrollOffset), 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,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 'contact_service.dart'; import '../../widgets/contact_service.dart';
class ContactState extends StatefulWidget { class ContactState extends StatefulWidget {
final Widget child; final Widget child;
@ -8,7 +8,9 @@ class ContactState extends StatefulWidget {
const ContactState({super.key, required this.child}); const ContactState({super.key, required this.child});
static _ContactStateState of(BuildContext context) { static _ContactStateState of(BuildContext context) {
return context.dependOnInheritedWidgetOfExactType<_InheritedContactState>()!.data; return context
.dependOnInheritedWidgetOfExactType<_InheritedContactState>()!
.data;
} }
@override @override
@ -17,12 +19,15 @@ class ContactState extends StatefulWidget {
class _ContactStateState extends State<ContactState> { class _ContactStateState extends State<ContactState> {
final ContactService _contactService = ContactService(); final ContactService _contactService = ContactService();
List<Contact> _contacts = []; List<Contact> _allContacts = [];
List<Contact> _favoriteContacts = [];
bool _loading = true; bool _loading = true;
double _scrollOffset = 0.0; double _scrollOffset = 0.0;
Contact? _selfContact = Contact(); Contact? _selfContact = Contact();
List<Contact> get contacts => _contacts; // Getters for all contacts and favorites
List<Contact> get contacts => _allContacts;
List<Contact> get favoriteContacts => _favoriteContacts;
bool get loading => _loading; bool get loading => _loading;
double get scrollOffset => _scrollOffset; double get scrollOffset => _scrollOffset;
Contact? get selfContact => _selfContact; Contact? get selfContact => _selfContact;
@ -30,31 +35,46 @@ class _ContactStateState extends State<ContactState> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_fetchContacts(); fetchContacts(); // Fetch all contacts by default
// Add listener for contact changes
FlutterContacts.addListener(_onContactChange); FlutterContacts.addListener(_onContactChange);
} }
void _onContactChange() => _fetchContacts(); void _onContactChange() => fetchContacts();
@override @override
void dispose() { void dispose() {
// Remove listener
FlutterContacts.removeListener(_onContactChange); FlutterContacts.removeListener(_onContactChange);
super.dispose(); super.dispose();
} }
Future<void> _fetchContacts() async { // Fetch all contacts
List<Contact> contacts = await _contactService.fetchContacts(); Future<void> fetchContacts() async {
setState(() => _loading = true);
try {
List<Contact> contacts = await _contactService.fetchContacts();
_processContacts(contacts);
} finally {
setState(() => _loading = false);
}
}
debugPrint("Fetched ${contacts.length} contacts"); // 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);
}
}
// Find selfContact before filtering void _processContacts(List<Contact> contacts) {
_selfContact = contacts.firstWhere( _selfContact = contacts.firstWhere(
(contact) => contact.displayName.toLowerCase() == "user", (contact) => contact.displayName.toLowerCase() == "user",
orElse: () => Contact(), orElse: () => Contact(),
); );
if (_selfContact!.phones.isEmpty) { if (_selfContact!.phones.isEmpty) {
debugPrint("Self contact has no phone numbers"); debugPrint("Self contact has no phone numbers");
_selfContact = null; _selfContact = null;
@ -62,16 +82,18 @@ class _ContactStateState extends State<ContactState> {
contacts = contacts.where((contact) => contact.phones.isNotEmpty).toList(); contacts = contacts.where((contact) => contact.phones.isNotEmpty).toList();
contacts.sort((a, b) => a.displayName.compareTo(b.displayName)); contacts.sort((a, b) => a.displayName.compareTo(b.displayName));
setState(() { setState(() {
_contacts = contacts; _allContacts = contacts;
_loading = false; _favoriteContacts =
contacts.where((contact) => contact.isStarred).toList();
_selfContact = _selfContact; _selfContact = _selfContact;
}); });
} }
Future<void> addNewContact(Contact contact) async { Future<void> addNewContact(Contact contact) async {
await _contactService.addNewContact(contact); await _contactService.addNewContact(contact);
await _fetchContacts(); await fetchContacts();
} }
void setScrollOffset(double offset) { void setScrollOffset(double offset) {
@ -89,6 +111,7 @@ class _ContactStateState extends State<ContactState> {
} }
} }
class _InheritedContactState extends InheritedWidget { class _InheritedContactState extends InheritedWidget {
final _ContactStateState data; final _ContactStateState data;

View File

@ -1,16 +1,21 @@
import 'package:dialer/widgets/username_color_generator.dart'; import 'package:dialer/widgets/username_color_generator.dart';
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/color_darkener.dart';
import '../contact_state.dart'; import '../contact_state.dart';
import '../../../widgets/color_darkener.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 double scrollOffset; 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 @override
_AlphabetScrollPageState createState() => _AlphabetScrollPageState(); _AlphabetScrollPageState createState() => _AlphabetScrollPageState();
@ -22,7 +27,8 @@ class _AlphabetScrollPageState extends State<AlphabetScrollPage> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_scrollController = ScrollController(initialScrollOffset: widget.scrollOffset); _scrollController =
ScrollController(initialScrollOffset: widget.scrollOffset);
_scrollController.addListener(_onScroll); _scrollController.addListener(_onScroll);
} }
@ -31,11 +37,53 @@ class _AlphabetScrollPageState extends State<AlphabetScrollPage> {
contactState.setScrollOffset(_scrollController.offset); 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final contacts = widget.contacts;
final selfContact = ContactState.of(context).selfContact;
Map<String, List<Contact>> alphabetizedContacts = {}; Map<String, List<Contact>> alphabetizedContacts = {};
for (var contact in widget.contacts) { for (var contact in contacts) {
String firstLetter = contact.displayName.isNotEmpty ? contact.displayName[0].toUpperCase() : '#'; String firstLetter = contact.displayName.isNotEmpty
? contact.displayName[0].toUpperCase()
: '#';
if (!alphabetizedContacts.containsKey(firstLetter)) { if (!alphabetizedContacts.containsKey(firstLetter)) {
alphabetizedContacts[firstLetter] = []; alphabetizedContacts[firstLetter] = [];
} }
@ -47,7 +95,8 @@ class _AlphabetScrollPageState extends State<AlphabetScrollPage> {
return Scaffold( return Scaffold(
backgroundColor: Colors.black, backgroundColor: Colors.black,
body: Column( body: Column(
children: [ // Top buttons row children: [
// Top buttons row
Container( Container(
color: Colors.black, color: Colors.black,
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
@ -55,7 +104,7 @@ class _AlphabetScrollPageState extends State<AlphabetScrollPage> {
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
AddContactButton(), AddContactButton(),
QRCodeButton(contacts: widget.contacts, selfContact: ContactState.of(context).selfContact), QRCodeButton(contacts: contacts, selfContact: selfContact),
], ],
), ),
), ),
@ -66,13 +115,14 @@ class _AlphabetScrollPageState extends State<AlphabetScrollPage> {
itemCount: alphabetKeys.length, itemCount: alphabetKeys.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
String letter = alphabetKeys[index]; String letter = alphabetKeys[index];
List<Contact> contacts = alphabetizedContacts[letter]!; List<Contact> contactsForLetter = alphabetizedContacts[letter]!;
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
// Alphabet Letter Header // Alphabet Letter Header
Padding( Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 16.0), padding: const EdgeInsets.symmetric(
vertical: 8.0, horizontal: 16.0),
child: Text( child: Text(
letter, letter,
style: TextStyle( style: TextStyle(
@ -83,25 +133,74 @@ class _AlphabetScrollPageState extends State<AlphabetScrollPage> {
), ),
), ),
// Contact Entries // Contact Entries
...contacts.map((contact) { ...contactsForLetter.map((contact) {
String phoneNumber = contact.phones.isNotEmpty ? contact.phones.first.number : 'No phone number'; String phoneNumber = contact.phones.isNotEmpty
Color avatarColor = generateColorFromName(contact.displayName); ? contact.phones.first.number
: 'No phone number';
Color avatarColor =
generateColorFromName(contact.displayName);
return ListTile( return ListTile(
leading: (contact.thumbnail != null && contact.thumbnail!.isNotEmpty) leading: (contact.thumbnail != null &&
contact.thumbnail!.isNotEmpty)
? CircleAvatar( ? CircleAvatar(
backgroundImage: MemoryImage(contact.thumbnail!), backgroundImage:
) MemoryImage(contact.thumbnail!),
)
: CircleAvatar( : CircleAvatar(
backgroundColor: avatarColor, backgroundColor: avatarColor,
child: Text( child: Text(
contact.displayName.isNotEmpty ? contact.displayName[0].toUpperCase() : '?', contact.displayName.isNotEmpty
style: TextStyle(color: darken(avatarColor, 0.4)), ? contact.displayName[0].toUpperCase()
), : '?',
), style: TextStyle(
title: Text(contact.displayName, style: TextStyle(color: Colors.white)), color: darken(avatarColor, 0.4)),
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: () {
// 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:flutter/material.dart';
import 'package:dialer/widgets/loading_indicator.dart';
class FavoritePage extends StatefulWidget { class FavoritesPage extends StatefulWidget {
const FavoritePage({super.key}); const FavoritesPage({super.key});
@override @override
_FavoritePageState createState() => _FavoritePageState(); _FavoritesPageState createState() => _FavoritesPageState();
} }
class _FavoritePageState extends State<FavoritePage> { class _FavoritesPageState extends State<FavoritesPage> {
@override
void initState() {
super.initState();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final contactState = ContactState.of(context);
return Scaffold( return Scaffold(
backgroundColor: Colors.black, body: contactState.loading
body: Center( // Center the text within the body ? const LoadingIndicatorWidget()
child: Text( : AlphabetScrollPage(
"Hello", scrollOffset: contactState.scrollOffset,
style: TextStyle(color: Colors.white), // Change text color for visibility 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: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() {
@ -21,10 +24,8 @@ class _MyHomePageState extends State<MyHomePage>
} }
void _fetchContacts() async { void _fetchContacts() async {
if (await FlutterContacts.requestPermission()) { _allContacts = await _contactService.fetchContacts();
_allContacts = await FlutterContacts.getContacts(withProperties: true); setState(() {});
setState(() {});
}
} }
void _onSearchChanged(String query) { void _onSearchChanged(String query) {
@ -83,7 +84,7 @@ class _MyHomePageState extends State<MyHomePage>
builder: (BuildContext context, SearchController controller) { builder: (BuildContext context, SearchController controller) {
return SearchBar( return SearchBar(
controller: controller, controller: controller,
padding: MaterialStateProperty.all<EdgeInsetsGeometry>( padding: WidgetStateProperty.all<EdgeInsetsGeometry>(
const EdgeInsets.only( const EdgeInsets.only(
top: 6.0, top: 6.0,
bottom: 6.0, bottom: 6.0,
@ -95,10 +96,10 @@ class _MyHomePageState extends State<MyHomePage>
controller.openView(); controller.openView();
_onSearchChanged(''); _onSearchChanged('');
}, },
backgroundColor: MaterialStateProperty.all( backgroundColor: WidgetStateProperty.all(
const Color.fromARGB(255, 30, 30, 30)), const Color.fromARGB(255, 30, 30, 30)),
hintText: 'Search contacts', hintText: 'Search contacts',
hintStyle: MaterialStateProperty.all( hintStyle: WidgetStateProperty.all(
const TextStyle(color: Colors.grey, fontSize: 16.0), const TextStyle(color: Colors.grey, fontSize: 16.0),
), ),
leading: const Icon( leading: const Icon(
@ -106,7 +107,7 @@ class _MyHomePageState extends State<MyHomePage>
color: Colors.grey, color: Colors.grey,
size: 24.0, size: 24.0,
), ),
shape: MaterialStateProperty.all<RoundedRectangleBorder>( shape: WidgetStateProperty.all<RoundedRectangleBorder>(
RoundedRectangleBorder( RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12.0), borderRadius: BorderRadius.circular(12.0),
), ),
@ -139,7 +140,7 @@ class _MyHomePageState extends State<MyHomePage>
TabBarView( TabBarView(
controller: _tabController, controller: _tabController,
children: const [ children: const [
FavoritePage(), FavoritesPage(),
HistoryPage(), HistoryPage(),
ContactPage(), ContactPage(),
SettingsPage(), // Add your SettingsPage here SettingsPage(), // Add your SettingsPage here

View File

@ -0,0 +1,167 @@
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
class BlockedNumbersPage extends StatefulWidget {
const BlockedNumbersPage({super.key});
@override
_BlockedNumbersPageState createState() => _BlockedNumbersPageState();
}
class _BlockedNumbersPageState extends State<BlockedNumbersPage> {
bool _blockUnknownNumbers = false; // Toggle for blocking unknown numbers
List<String> _blockedNumbers = []; // List of blocked numbers
final TextEditingController _numberController = TextEditingController();
@override
void initState() {
super.initState();
_loadPreferences(); // Load data on initialization
}
// Load preferences from local storage
Future<void> _loadPreferences() async {
final prefs = await SharedPreferences.getInstance();
setState(() {
_blockUnknownNumbers = prefs.getBool('blockUnknownNumbers') ?? false;
_blockedNumbers = prefs.getStringList('blockedNumbers') ?? [];
});
}
// Save preferences to local storage
Future<void> _savePreferences() async {
final prefs = await SharedPreferences.getInstance();
await prefs.setBool('blockUnknownNumbers', _blockUnknownNumbers);
await prefs.setStringList('blockedNumbers', _blockedNumbers);
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.black,
appBar: AppBar(
title: const Text('Blocked Numbers'),
),
body: ListView(
padding: const EdgeInsets.all(16),
children: [
SwitchListTile(
title: const Text(
'Block Unknown Numbers',
style: TextStyle(color: Colors.white),
),
value: _blockUnknownNumbers,
onChanged: (bool value) {
setState(() {
_blockUnknownNumbers = value;
_savePreferences(); // Save the state to local storage
});
},
),
const SizedBox(height: 16),
ListTile(
title: const Text(
'Blocked Numbers',
style: TextStyle(color: Colors.white, fontSize: 18, fontWeight: FontWeight.bold),
),
subtitle: _blockedNumbers.isEmpty
? const Text(
'No blocked numbers',
style: TextStyle(color: Colors.grey),
)
: null,
),
..._blockedNumbers.map(
(number) => ListTile(
title: Text(
number,
style: const TextStyle(color: Colors.white),
),
trailing: IconButton(
icon: const Icon(Icons.delete, color: Colors.red),
onPressed: () => _unblockNumber(number),
),
),
),
const Divider(color: Colors.grey),
ListTile(
title: const Text(
'Block a Number',
style: TextStyle(color: Colors.white),
),
trailing: const Icon(Icons.add, color: Colors.white),
onTap: () => _showBlockNumberDialog(),
),
],
),
);
}
// Function to block a number
void _blockNumber(String number) {
if (number.isNotEmpty && !_blockedNumbers.contains(number)) {
setState(() {
_blockedNumbers.add(number);
_savePreferences(); // Save the updated list
});
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('$number has been blocked')),
);
}
}
// Function to unblock a number
void _unblockNumber(String number) {
setState(() {
_blockedNumbers.remove(number);
_savePreferences(); // Save the updated list
});
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('$number has been unblocked')),
);
}
// Dialog for blocking a new number
void _showBlockNumberDialog() {
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
backgroundColor: Colors.grey[900],
title: const Text('Block a Number', style: TextStyle(color: Colors.white)),
content: TextField(
controller: _numberController,
keyboardType: TextInputType.phone,
decoration: const InputDecoration(
hintText: 'Enter number',
hintStyle: TextStyle(color: Colors.grey),
),
style: const TextStyle(color: Colors.white),
),
actions: [
TextButton(
onPressed: () {
Navigator.pop(context);
},
child: const Text('Cancel', style: TextStyle(color: Colors.white)),
),
TextButton(
onPressed: () {
_blockNumber(_numberController.text);
_numberController.clear();
Navigator.pop(context);
},
child: const Text('Block', style: TextStyle(color: Colors.red)),
),
],
);
},
);
}
@override
void dispose() {
_numberController.dispose();
super.dispose();
}
}

View File

@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
import 'package:dialer/features/settings/call/settingsCall.dart'; import 'package:dialer/features/settings/call/settingsCall.dart';
import 'package:dialer/features/settings/sim/settings_accounts.dart'; import 'package:dialer/features/settings/sim/settings_accounts.dart';
import 'package:dialer/features/settings/key/manage_keys_page.dart'; import 'package:dialer/features/settings/key/manage_keys_page.dart';
import 'package:dialer/features/settings/blocked/settings_blocked.dart';
class SettingsPage extends StatelessWidget { class SettingsPage extends StatelessWidget {
const SettingsPage({super.key}); const SettingsPage({super.key});
@ -28,9 +29,15 @@ class SettingsPage extends StatelessWidget {
MaterialPageRoute(builder: (context) => const KeyManagementPage()), MaterialPageRoute(builder: (context) => const KeyManagementPage()),
); );
break; break;
// Add more cases for other settings pages case 'Blocked numbers':
Navigator.push(
context,
MaterialPageRoute(builder: (context) => const BlockedNumbersPage()),
);
break;
// Add more cases for other settings pages
default: default:
// Handle default or unknown settings // Handle default or unknown settings
break; break;
} }
} }
@ -41,6 +48,7 @@ class SettingsPage extends StatelessWidget {
'Calling settings', 'Calling settings',
'Page of telephone accounts', 'Page of telephone accounts',
'Key management', 'Key management',
'Blocked numbers'
]; ];
return Scaffold( return Scaffold(

View File

@ -16,7 +16,7 @@ class MyApp extends StatelessWidget {
theme: ThemeData( theme: ThemeData(
brightness: Brightness.dark 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);
}
}

View File

@ -34,6 +34,7 @@ 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
@ -49,6 +50,7 @@ dependencies:
intl_utils: ^2.0.7 intl_utils: ^2.0.7
mobile_number: mobile_number:
path: packages/mobile_number path: packages/mobile_number
shared_preferences: ^2.3.3
dev_dependencies: dev_dependencies:
flutter_test: flutter_test: