diff --git a/lib/classes/contactClass.dart b/lib/classes/contactClass.dart deleted file mode 100644 index 3d7dd91..0000000 --- a/lib/classes/contactClass.dart +++ /dev/null @@ -1,9 +0,0 @@ -// Create contact lists -class Contact { - final String name; - final String phoneNumber; - final bool isFavorite; - final bool isLocked; - - Contact(this.name, this.phoneNumber, {this.isFavorite = false, this.isLocked = false}); -} diff --git a/lib/classes/displayAvatar.dart b/lib/classes/displayAvatar.dart deleted file mode 100644 index f5cacb9..0000000 --- a/lib/classes/displayAvatar.dart +++ /dev/null @@ -1,29 +0,0 @@ -import 'package:dialer/classes/contactClass.dart'; -import 'package:flutter/material.dart'; - -class DisplayAvatar extends StatelessWidget { - final Contact contact; - - const DisplayAvatar({super.key, required this.contact}); - - @override - Widget build(BuildContext context) { - return Stack( - children: [ - CircleAvatar( - child: Text(contact.name[0]), - ), - if (contact.isLocked) - const Positioned( - right: 0, - bottom: 0, - child: Icon( - Icons.lock, - color: Colors.white, - size: 20.0, - ), - ), - ], - ); - } -} diff --git a/lib/features/contacts/contact_page.dart b/lib/features/contacts/contact_page.dart new file mode 100644 index 0000000..d27fb9d --- /dev/null +++ b/lib/features/contacts/contact_page.dart @@ -0,0 +1,27 @@ +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 ContactPage extends StatefulWidget { + const ContactPage({super.key}); + + @override + _ContactPageState createState() => _ContactPageState(); +} + +class _ContactPageState extends State { + @override + Widget build(BuildContext context) { + final contactState = ContactState.of(context); + return Scaffold( + appBar: AppBar( + title: const Text('Contacts'), + ), + body: contactState.loading + ? const LoadingIndicatorWidget() + // : ContactListWidget(contacts: contactState.contacts), + : AlphabetScrollPage(contacts: contactState.contacts, scrollOffset: contactState.scrollOffset), + ); + } +} diff --git a/lib/features/contacts/contact_service.dart b/lib/features/contacts/contact_service.dart new file mode 100644 index 0000000..85e4cf1 --- /dev/null +++ b/lib/features/contacts/contact_service.dart @@ -0,0 +1,15 @@ +import 'package:flutter_contacts/flutter_contacts.dart'; + +// Service to manage contact-related operations +class ContactService { + Future> fetchContacts() async { + if (await FlutterContacts.requestPermission()) { + return await FlutterContacts.getContacts(withProperties: true, withThumbnail: true); + } + return []; + } + + Future addNewContact(Contact contact) async { + await FlutterContacts.insertContact(contact); + } +} \ No newline at end of file diff --git a/lib/features/contacts/contact_state.dart b/lib/features/contacts/contact_state.dart new file mode 100644 index 0000000..710cb5b --- /dev/null +++ b/lib/features/contacts/contact_state.dart @@ -0,0 +1,78 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_contacts/flutter_contacts.dart'; +import 'contact_service.dart'; + +class ContactState extends StatefulWidget { + final Widget child; + + const ContactState({Key? key, required this.child}) : super(key: key); + + static _ContactStateState of(BuildContext context) { + return context.dependOnInheritedWidgetOfExactType<_InheritedContactState>()!.data; + } + + @override + _ContactStateState createState() => _ContactStateState(); +} + +class _ContactStateState extends State { + final ContactService _contactService = ContactService(); + List _contacts = []; + bool _loading = true; + double _scrollOffset = 0.0; + + List get contacts => _contacts; + bool get loading => _loading; + double get scrollOffset => _scrollOffset; + + @override + void initState() { + super.initState(); + _fetchContacts(); + } + + Future _fetchContacts() async { + List contacts = await _contactService.fetchContacts(); + contacts = contacts.where((contact) => contact.phones.isNotEmpty).toList(); + contacts.sort((a, b) => a.displayName.compareTo(b.displayName)); + // contacts.sort((a, b) { + // String aName = a.displayName.isNotEmpty ? a.displayName : '􏿿'; + // String bName = b.displayName.isNotEmpty ? b.displayName : '􏿿'; + // return aName.compareTo(bName); + // }); + setState(() { + _contacts = contacts; + _loading = false; + }); + } + + + Future 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({Key? key, required this.data, required Widget child}) + : super(key: key, child: child); + + @override + bool updateShouldNotify(_InheritedContactState oldWidget) => true; +} diff --git a/lib/features/contacts/widgets/alphabet_scroll_page.dart b/lib/features/contacts/widgets/alphabet_scroll_page.dart new file mode 100644 index 0000000..4165755 --- /dev/null +++ b/lib/features/contacts/widgets/alphabet_scroll_page.dart @@ -0,0 +1,98 @@ +import 'package:dialer/widgets/username_color_generator.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_contacts/flutter_contacts.dart'; +import '../contact_state.dart'; + +class AlphabetScrollPage extends StatefulWidget { + final List contacts; + final double scrollOffset; + + const AlphabetScrollPage({Key? key, required this.contacts, required this.scrollOffset}) : super(key: key); + + @override + _AlphabetScrollPageState createState() => _AlphabetScrollPageState(); +} + +class _AlphabetScrollPageState extends State { + late ScrollController _scrollController; + + @override + void initState() { + super.initState(); + _scrollController = ScrollController(initialScrollOffset: widget.scrollOffset); + _scrollController.addListener(_onScroll); + } + + void _onScroll() { + final contactState = ContactState.of(context); + contactState.setScrollOffset(_scrollController.offset); + } + + @override + Widget build(BuildContext context) { + Map> alphabetizedContacts = {}; + for (var contact in widget.contacts) { + String firstLetter = contact.displayName.isNotEmpty ? contact.displayName[0].toUpperCase() : '#'; + if (!alphabetizedContacts.containsKey(firstLetter)) { + alphabetizedContacts[firstLetter] = []; + } + alphabetizedContacts[firstLetter]!.add(contact); + } + + List alphabetKeys = alphabetizedContacts.keys.toList()..sort(); + + return ListView.builder( + controller: _scrollController, + itemCount: alphabetKeys.length, + itemBuilder: (context, index) { + String letter = alphabetKeys[index]; + List contacts = alphabetizedContacts[letter]!; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + color: Colors.grey[300], + width: double.infinity, + padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 16.0), + child: Text( + letter, + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + ), + ...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) + ? CircleAvatar( + backgroundImage: MemoryImage(contact.thumbnail!), + ) + : CircleAvatar( + backgroundColor: avatarColor, + child: Text( + contact.displayName.isNotEmpty ? contact.displayName[0].toUpperCase() : '?', + style: TextStyle(color: Colors.white), + ), + ), + title: Text(contact.displayName), + subtitle: Text(phoneNumber), + onTap: () { + // Handle contact tap + }, + ); + }).toList(), + ], + ); + }, + ); + } + + @override + void dispose() { + _scrollController.dispose(); + super.dispose(); + } +} diff --git a/lib/features/contacts/widgets/contact_list_widget.dart b/lib/features/contacts/widgets/contact_list_widget.dart new file mode 100644 index 0000000..6a0cbcd --- /dev/null +++ b/lib/features/contacts/widgets/contact_list_widget.dart @@ -0,0 +1,40 @@ +import 'package:dialer/widgets/color_darkener.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_contacts/flutter_contacts.dart'; +import 'package:dialer/widgets/username_color_generator.dart'; + +class ContactListWidget extends StatelessWidget { + final List contacts; + + const ContactListWidget({Key? key, required this.contacts}) : super(key: key); + + @override + Widget build(BuildContext context) { + return ListView.builder( + itemCount: contacts.length, + itemBuilder: (context, index) { + Contact contact = contacts[index]; + String phoneNumber = contact.phones.isNotEmpty ? contact.phones.first.number : 'No phone number'; + Color avatarColor = generateColorFromName(contact.displayName); + return ListTile( + leading: (contact.thumbnail != null && contact.thumbnail!.isNotEmpty) + ? CircleAvatar( + backgroundImage: MemoryImage(contact.thumbnail!), + ) + : CircleAvatar( + backgroundColor: avatarColor, + child: Text( + contact.displayName.isNotEmpty ? contact.displayName[0].toUpperCase() : '?', + style: TextStyle(fontSize: 25, color: darken(avatarColor, 0.4)), + ), + ), + title: Text(contact.displayName), + subtitle: Text(phoneNumber), + onTap: () { + // Handle contact tap + }, + ); + }, + ); + } +} \ No newline at end of file diff --git a/lib/pages/favorites.dart b/lib/features/favorites/favorites_page.dart similarity index 100% rename from lib/pages/favorites.dart rename to lib/features/favorites/favorites_page.dart diff --git a/lib/pages/history.dart b/lib/features/history/history_page.dart similarity index 91% rename from lib/pages/history.dart rename to lib/features/history/history_page.dart index 6207ed8..c1e378c 100644 --- a/lib/pages/history.dart +++ b/lib/features/history/history_page.dart @@ -1,4 +1,4 @@ -import 'package:dialer/classes/contactClass.dart'; +import 'package:dialer/features/contacts/contact_service.dart'; import 'package:flutter/material.dart'; diff --git a/lib/pages/myHomePage.dart b/lib/features/home/home_page.dart similarity index 88% rename from lib/pages/myHomePage.dart rename to lib/features/home/home_page.dart index 4858bb3..da601d4 100644 --- a/lib/pages/myHomePage.dart +++ b/lib/features/home/home_page.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; -import 'package:dialer/pages/contact.dart'; // Import ContactPage -import 'package:dialer/pages/favorites.dart'; // Import FavoritePage -import 'package:dialer/pages/history.dart'; // Import HistoryPage +import 'package:dialer/features/contacts/contact_page.dart'; // Import ContactPage +import 'package:dialer/features/favorites/favorites_page.dart'; // Import FavoritePage +import 'package:dialer/features/history/history_page.dart'; // Import HistoryPage class _MyHomePageState extends State with SingleTickerProviderStateMixin { late TabController _tabController; diff --git a/lib/main.dart b/lib/main.dart index 27e3df6..851c5f8 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,7 +1,8 @@ // This is DEV -import 'package:dialer/pages/myHomePage.dart'; +import 'package:dialer/features/home/home_page.dart'; import 'package:flutter/material.dart'; +import 'package:dialer/features/contacts/contact_state.dart'; void main() { runApp(const MyApp()); @@ -12,11 +13,13 @@ class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { - return MaterialApp( - theme: ThemeData( - brightness: Brightness.dark - ), - home: const MyHomePage(), + return ContactState( + child: MaterialApp( + theme: ThemeData( + brightness: Brightness.dark + ), + home: const MyHomePage(), + ) ); } } diff --git a/lib/pages/callingPage.dart b/lib/pages/callingPage.dart deleted file mode 100644 index 4a9789c..0000000 --- a/lib/pages/callingPage.dart +++ /dev/null @@ -1,93 +0,0 @@ -import 'package:dialer/classes/contactClass.dart'; -import 'package:flutter/material.dart'; - -// display the calling page as if the call is already in progress -class _CallingPageState extends State { - @override - Widget build(BuildContext context) { - final contact = widget.contact; - return Scaffold( - backgroundColor: Colors.black, - appBar: AppBar( - title: const Text('Appel en cours'), - backgroundColor: Colors.black, - ), - body: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const CircleAvatar( - radius: 100.0, - backgroundImage: - NetworkImage('https://thispersondoesnotexist.com/'), - backgroundColor: Colors.transparent, - ), - const SizedBox(height: 10.0), - // Add the contact name here - Text(contact.name, style: const TextStyle(fontSize: 40.0, color: Colors.white)), - // const SizedBox(height: 10.0), - const Text('99 : 59 : 59', style: - TextStyle(fontSize: 40.0, color: - Colors.white)), - const SizedBox(height: 50.0), - contact.isLocked - ? const Text('Con. Health - 98% (excellent)', - style: - TextStyle(fontSize: - 16.0,color: - Colors.green)) - : - const Text('No Icing available', - style: - TextStyle(fontSize: - 16.0,color: - Colors.white)), - const SizedBox(height: - 50.0), // Adjust size box height as needed - const Row( - mainAxisAlignment: - MainAxisAlignment.spaceEvenly, - children:[ - Icon(Icons.mic_off,size: - 30.0,color: - Colors.white), - Icon(Icons.dialpad,size: - 30.0,color: - Colors.white), - Icon(Icons.more_vert,size: - 30.0,color: - Colors.white), - Icon(Icons.volume_up,size: - 30.0,color: - Colors.white), - ], - ), - const SizedBox(height: - 50.0), // Adjust size box height as needed - const Row( - mainAxisAlignment: - MainAxisAlignment.spaceEvenly, - children:[ - Icon(Icons.pause,size: - 60.0,color: - Colors.white), - Icon(Icons.call_end,size: - 60.0,color: - Colors.red), - ], - ), - ], - ), - ), - ); - } -} - -class CallingPage extends StatefulWidget { - final Contact contact; - - const CallingPage({super.key, required this.contact}); - - @override - _CallingPageState createState() => _CallingPageState(); -} diff --git a/lib/pages/contact.dart b/lib/pages/contact.dart deleted file mode 100644 index 5f9f9ab..0000000 --- a/lib/pages/contact.dart +++ /dev/null @@ -1,100 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_contacts/flutter_contacts.dart'; // Updated package - -class ContactPage extends StatefulWidget { - const ContactPage({super.key}); - - @override - _ContactPageState createState() => _ContactPageState(); -} - -class _ContactPageState extends State { - List _contacts = []; - bool _loading = true; - - @override - void initState() { - super.initState(); - _fetchContacts(); - } - - // Request permission and fetch contacts - Future _fetchContacts() async { - if (await FlutterContacts.requestPermission()) { - List contacts = await FlutterContacts.getContacts(withProperties: true, withThumbnail: true); - setState(() { - _contacts = contacts; - _loading = false; - }); - } else { - setState(() { - _loading = false; - }); - } - } - - // Add a new contact using flutter_contacts - Future _addNewContact() async { - Contact newContact = Contact( - name: Name(first: 'John', last: 'Doe'), - phones: [Phone('123456789')], - ); - await FlutterContacts.insertContact(newContact); - _fetchContacts(); // Refresh the contact list - } - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: const Text('Contacts'), - ), - body: _loading - ? const Center(child: CircularProgressIndicator()) - : _contacts.isEmpty - ? const Center(child: Text('No contacts found')) - : ListView.builder( - itemCount: _contacts.length, - itemBuilder: (context, index) { - return ContactTile(contact: _contacts[index]); - }, - ), - floatingActionButton: FloatingActionButton( - child: const Icon(Icons.add), - onPressed: _addNewContact, - ), - ); - } -} - -// Contact Tile to display each contact -class ContactTile extends StatelessWidget { - final Contact contact; - - const ContactTile({super.key, required this.contact}); - - @override - Widget build(BuildContext context) { - return ListTile( - leading: (contact.thumbnail != null) - ? CircleAvatar(backgroundImage: MemoryImage(contact.thumbnail!)) - : CircleAvatar(child: Text(_getInitials(contact.displayName))), - title: Text(contact.displayName ?? 'No Name'), - subtitle: contact.phones.isNotEmpty - ? Text(contact.phones.first.number) - : const Text('No phone number'), - trailing: IconButton( - icon: const Icon(Icons.call), - onPressed: () { - // Handle call action - }, - ), - ); - } - - String _getInitials(String? name) { - if (name == null || name.isEmpty) return ""; - List names = name.split(' '); - return names.map((n) => n[0]).take(2).join().toUpperCase(); - } -} diff --git a/lib/widgets/color_darkener.dart b/lib/widgets/color_darkener.dart new file mode 100644 index 0000000..8442304 --- /dev/null +++ b/lib/widgets/color_darkener.dart @@ -0,0 +1,10 @@ +import 'package:flutter/material.dart'; + +Color darken(Color color, [double amount = .1]) { + assert(amount >= 0 && amount <= 1); + + final hsl = HSLColor.fromColor(color); + final hslDark = hsl.withLightness((hsl.lightness - amount).clamp(0.0, 1.0)); + + return hslDark.toColor(); +} \ No newline at end of file diff --git a/lib/widgets/loading_indicator.dart b/lib/widgets/loading_indicator.dart new file mode 100644 index 0000000..59e2409 --- /dev/null +++ b/lib/widgets/loading_indicator.dart @@ -0,0 +1,10 @@ +import 'package:flutter/material.dart'; + +class LoadingIndicatorWidget extends StatelessWidget { + const LoadingIndicatorWidget({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return const Center(child: CircularProgressIndicator()); + } +} diff --git a/lib/widgets/username_color_generator.dart b/lib/widgets/username_color_generator.dart new file mode 100644 index 0000000..5686a48 --- /dev/null +++ b/lib/widgets/username_color_generator.dart @@ -0,0 +1,12 @@ +import 'dart:math'; +import 'package:flutter/material.dart'; + +Color generateColorFromName(String name) { + final random = Random(name.hashCode); + return Color.fromARGB( + 255, + random.nextInt(256), + random.nextInt(256), + random.nextInt(256), + ); +}