Compare commits

..

No commits in common. "7f648dfa6a2d5f639d4ee129fea73de02cdb941b" and "1d6a897dc3ce94c35c8c7060e43aa4a35a177b98" have entirely different histories.

14 changed files with 79 additions and 470 deletions

View File

@ -3,10 +3,6 @@
<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.READ_BLOCKED_NUMBERS" />
<uses-permission android:name="android.permission.WRITE_BLOCKED_NUMBERS" />
<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

@ -3,10 +3,6 @@
<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.READ_BLOCKED_NUMBERS" />
<uses-permission android:name="android.permission.WRITE_BLOCKED_NUMBERS" />
<application
android:label="com.example.dialer"
android:name="${applicationName}"

View File

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

View File

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

View File

@ -0,0 +1,15 @@
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_contacts/flutter_contacts.dart';
import '../../widgets/contact_service.dart';
import 'contact_service.dart';
class ContactState extends StatefulWidget {
final Widget child;
@ -8,9 +8,7 @@ class ContactState extends StatefulWidget {
const ContactState({super.key, required this.child});
static _ContactStateState of(BuildContext context) {
return context
.dependOnInheritedWidgetOfExactType<_InheritedContactState>()!
.data;
return context.dependOnInheritedWidgetOfExactType<_InheritedContactState>()!.data;
}
@override
@ -19,15 +17,12 @@ class ContactState extends StatefulWidget {
class _ContactStateState extends State<ContactState> {
final ContactService _contactService = ContactService();
List<Contact> _allContacts = [];
List<Contact> _favoriteContacts = [];
List<Contact> _contacts = [];
bool _loading = true;
double _scrollOffset = 0.0;
Contact? _selfContact = Contact();
// Getters for all contacts and favorites
List<Contact> get contacts => _allContacts;
List<Contact> get favoriteContacts => _favoriteContacts;
List<Contact> get contacts => _contacts;
bool get loading => _loading;
double get scrollOffset => _scrollOffset;
Contact? get selfContact => _selfContact;
@ -35,46 +30,31 @@ class _ContactStateState extends State<ContactState> {
@override
void initState() {
super.initState();
fetchContacts(); // Fetch all contacts by default
_fetchContacts();
// Add listener for contact changes
FlutterContacts.addListener(_onContactChange);
}
void _onContactChange() => fetchContacts();
void _onContactChange() => _fetchContacts();
@override
void dispose() {
// Remove listener
FlutterContacts.removeListener(_onContactChange);
super.dispose();
}
// Fetch all contacts
Future<void> fetchContacts() async {
setState(() => _loading = true);
try {
Future<void> _fetchContacts() async {
List<Contact> contacts = await _contactService.fetchContacts();
_processContacts(contacts);
} finally {
setState(() => _loading = false);
}
}
// 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);
}
}
debugPrint("Fetched ${contacts.length} contacts");
void _processContacts(List<Contact> 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;
@ -82,18 +62,16 @@ class _ContactStateState extends State<ContactState> {
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();
_contacts = contacts;
_loading = false;
_selfContact = _selfContact;
});
}
Future<void> addNewContact(Contact contact) async {
await _contactService.addNewContact(contact);
await fetchContacts();
await _fetchContacts();
}
void setScrollOffset(double offset) {
@ -111,7 +89,6 @@ class _ContactStateState extends State<ContactState> {
}
}
class _InheritedContactState extends InheritedWidget {
final _ContactStateState data;

View File

@ -1,21 +1,16 @@
import 'package:dialer/widgets/username_color_generator.dart';
import 'package:flutter/material.dart';
import 'package:flutter_contacts/flutter_contacts.dart';
import '../contact_state.dart';
import '../../../widgets/color_darkener.dart';
import '../contact_state.dart';
import 'add_contact_button.dart';
import 'contact_modal.dart';
import 'share_own_qr.dart';
class AlphabetScrollPage extends StatefulWidget {
final double scrollOffset;
final List<Contact> contacts;
final double scrollOffset;
const AlphabetScrollPage({
super.key,
required this.scrollOffset,
required this.contacts,
});
const AlphabetScrollPage({super.key, required this.contacts, required this.scrollOffset});
@override
_AlphabetScrollPageState createState() => _AlphabetScrollPageState();
@ -36,53 +31,11 @@ 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 contacts) {
String firstLetter = contact.displayName.isNotEmpty
? contact.displayName[0].toUpperCase()
: '#';
for (var contact in widget.contacts) {
String firstLetter = contact.displayName.isNotEmpty ? contact.displayName[0].toUpperCase() : '#';
if (!alphabetizedContacts.containsKey(firstLetter)) {
alphabetizedContacts[firstLetter] = [];
}
@ -94,8 +47,7 @@ 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),
@ -103,7 +55,7 @@ class _AlphabetScrollPageState extends State<AlphabetScrollPage> {
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
AddContactButton(),
QRCodeButton(contacts: contacts, selfContact: selfContact),
QRCodeButton(contacts: widget.contacts, selfContact: ContactState.of(context).selfContact),
],
),
),
@ -114,14 +66,13 @@ class _AlphabetScrollPageState extends State<AlphabetScrollPage> {
itemCount: alphabetKeys.length,
itemBuilder: (context, index) {
String letter = alphabetKeys[index];
List<Contact> contactsForLetter = alphabetizedContacts[letter]!;
List<Contact> contacts = 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(
@ -132,74 +83,25 @@ class _AlphabetScrollPageState extends State<AlphabetScrollPage> {
),
),
// Contact Entries
...contactsForLetter.map((contact) {
String phoneNumber = contact.phones.isNotEmpty
? contact.phones.first.number
: 'No phone number';
Color avatarColor =
generateColorFromName(contact.displayName);
...contacts.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)),
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)),
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 {
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,
);
},
);
// Handle contact tap
},
);
}),

View File

@ -1,227 +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
// 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,31 +1,22 @@
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 FavoritesPage extends StatefulWidget {
const FavoritesPage({super.key});
class FavoritePage extends StatefulWidget {
const FavoritePage({super.key});
@override
_FavoritesPageState createState() => _FavoritesPageState();
_FavoritePageState createState() => _FavoritePageState();
}
class _FavoritesPageState extends State<FavoritesPage> {
@override
void initState() {
super.initState();
}
class _FavoritePageState extends State<FavoritePage> {
@override
Widget build(BuildContext context) {
final contactState = ContactState.of(context);
return Scaffold(
body: contactState.loading
? const LoadingIndicatorWidget()
: AlphabetScrollPage(
scrollOffset: contactState.scrollOffset,
contacts:
contactState.favoriteContacts, // Use only favorites here
backgroundColor: Colors.black,
body: Center( // Center the text within the body
child: Text(
"Hello",
style: TextStyle(color: Colors.white), // Change text color for visibility
),
),
);
}

View File

@ -5,15 +5,12 @@ 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() {
@ -24,9 +21,11 @@ class _MyHomePageState extends State<MyHomePage>
}
void _fetchContacts() async {
_allContacts = await _contactService.fetchContacts();
if (await FlutterContacts.requestPermission()) {
_allContacts = await FlutterContacts.getContacts(withProperties: true);
setState(() {});
}
}
void _onSearchChanged(String query) {
print("Search query: $query");
@ -84,7 +83,7 @@ class _MyHomePageState extends State<MyHomePage>
builder: (BuildContext context, SearchController controller) {
return SearchBar(
controller: controller,
padding: WidgetStateProperty.all<EdgeInsetsGeometry>(
padding: MaterialStateProperty.all<EdgeInsetsGeometry>(
const EdgeInsets.only(
top: 6.0,
bottom: 6.0,
@ -96,10 +95,10 @@ class _MyHomePageState extends State<MyHomePage>
controller.openView();
_onSearchChanged('');
},
backgroundColor: WidgetStateProperty.all(
backgroundColor: MaterialStateProperty.all(
const Color.fromARGB(255, 30, 30, 30)),
hintText: 'Search contacts',
hintStyle: WidgetStateProperty.all(
hintStyle: MaterialStateProperty.all(
const TextStyle(color: Colors.grey, fontSize: 16.0),
),
leading: const Icon(
@ -107,7 +106,7 @@ class _MyHomePageState extends State<MyHomePage>
color: Colors.grey,
size: 24.0,
),
shape: WidgetStateProperty.all<RoundedRectangleBorder>(
shape: MaterialStateProperty.all<RoundedRectangleBorder>(
RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12.0),
),
@ -140,7 +139,7 @@ class _MyHomePageState extends State<MyHomePage>
TabBarView(
controller: _tabController,
children: const [
FavoritesPage(),
FavoritePage(),
HistoryPage(),
ContactPage(),
SettingsPage(), // Add your SettingsPage here

View File

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

View File

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