From 409afb0481bf6749e15e2b474710639ddc936235 Mon Sep 17 00:00:00 2001 From: AlexisDanlos Date: Wed, 4 Jun 2025 16:35:26 +0000 Subject: [PATCH] rework-app (#54) Co-authored-by: AlexisDanlos <91090088+AlexisDanlos@users.noreply.github.com> Reviewed-on: https://git.gmoker.com/icing/monorepo/pulls/54 Co-authored-by: AlexisDanlos Co-committed-by: AlexisDanlos --- dialer/lib/core/config/app_config.dart | 14 + dialer/lib/core/navigation/app_router.dart | 57 ++++ dialer/lib/domain/services/block_service.dart | 78 +++++ .../{ => domain}/services/call_service.dart | 6 +- .../lib/domain/services/contact_service.dart | 86 +++++ .../asymmetric_crypto_service.dart | 0 .../services/obfuscate_service.dart | 8 +- dialer/lib/domain/services/qr/qr_scanner.dart | 57 ++++ .../lib/features/call/incoming_call_page.dart | 181 ----------- .../lib/features/contacts/contact_page.dart | 26 -- .../widgets/alphabet_scroll_page.dart | 217 ------------- .../features/favorites/favorites_page.dart | 32 -- dialer/lib/globals.dart | 8 +- dialer/lib/main.dart | 34 +- .../presentation/common/theme/app_theme.dart | 42 +++ .../common/widgets/color_darkener.dart | 21 ++ .../common}/widgets/loading_indicator.dart | 0 .../common}/widgets/qr_scanner.dart | 0 .../widgets/username_color_generator.dart | 17 + .../features/call/call_page.dart | 6 +- .../features/call/incoming_call_page.dart | 163 ++++++++++ .../features/composition/composition.dart | 6 +- .../features/contacts/contact_page.dart | 20 ++ .../features/contacts/contact_state.dart | 113 ++++--- .../contacts/widgets/add_contact_button.dart | 2 +- .../widgets/alphabet_scroll_page.dart | 232 ++++++++++++++ .../contacts/widgets/contact_modal.dart | 298 ++++++++++-------- .../contacts/widgets/share_own_qr.dart | 2 +- .../features/favorites/favorites_page.dart | 37 +++ .../features/history/history_page.dart | 27 +- .../features/home/default_dialer_prompt.dart | 0 .../features/home/home_page.dart | 18 +- .../settings/blocked/settings_blocked.dart | 0 .../settings/call/settings_call.dart} | 2 +- .../settings/call/settings_call_page.dart | 32 ++ .../settings/cryptography/key_management.dart | 2 +- .../features/settings/settings.dart | 9 +- .../features/settings/settings_page.dart | 0 .../features/voicemail/voicemail_page.dart | 11 +- dialer/lib/services/block_service.dart | 52 --- dialer/lib/services/contact_service.dart | 51 --- dialer/lib/widgets/color_darkener.dart | 10 - .../lib/widgets/username_color_generator.dart | 12 - 43 files changed, 1170 insertions(+), 819 deletions(-) create mode 100644 dialer/lib/core/config/app_config.dart create mode 100644 dialer/lib/core/navigation/app_router.dart create mode 100644 dialer/lib/domain/services/block_service.dart rename dialer/lib/{ => domain}/services/call_service.dart (99%) create mode 100644 dialer/lib/domain/services/contact_service.dart rename dialer/lib/{ => domain}/services/cryptography/asymmetric_crypto_service.dart (100%) rename dialer/lib/{ => domain}/services/obfuscate_service.dart (91%) create mode 100644 dialer/lib/domain/services/qr/qr_scanner.dart delete mode 100644 dialer/lib/features/call/incoming_call_page.dart delete mode 100644 dialer/lib/features/contacts/contact_page.dart delete mode 100644 dialer/lib/features/contacts/widgets/alphabet_scroll_page.dart delete mode 100644 dialer/lib/features/favorites/favorites_page.dart create mode 100644 dialer/lib/presentation/common/theme/app_theme.dart create mode 100644 dialer/lib/presentation/common/widgets/color_darkener.dart rename dialer/lib/{ => presentation/common}/widgets/loading_indicator.dart (100%) rename dialer/lib/{ => presentation/common}/widgets/qr_scanner.dart (100%) create mode 100644 dialer/lib/presentation/common/widgets/username_color_generator.dart rename dialer/lib/{ => presentation}/features/call/call_page.dart (99%) create mode 100644 dialer/lib/presentation/features/call/incoming_call_page.dart rename dialer/lib/{ => presentation}/features/composition/composition.dart (98%) create mode 100644 dialer/lib/presentation/features/contacts/contact_page.dart rename dialer/lib/{ => presentation}/features/contacts/contact_state.dart (51%) rename dialer/lib/{ => presentation}/features/contacts/widgets/add_contact_button.dart (97%) create mode 100644 dialer/lib/presentation/features/contacts/widgets/alphabet_scroll_page.dart rename dialer/lib/{ => presentation}/features/contacts/widgets/contact_modal.dart (50%) rename dialer/lib/{ => presentation}/features/contacts/widgets/share_own_qr.dart (94%) create mode 100644 dialer/lib/presentation/features/favorites/favorites_page.dart rename dialer/lib/{ => presentation}/features/history/history_page.dart (96%) rename dialer/lib/{ => presentation}/features/home/default_dialer_prompt.dart (100%) rename dialer/lib/{ => presentation}/features/home/home_page.dart (96%) rename dialer/lib/{ => presentation}/features/settings/blocked/settings_blocked.dart (100%) rename dialer/lib/{features/settings/call/settingsCall.dart => presentation/features/settings/call/settings_call.dart} (98%) create mode 100644 dialer/lib/presentation/features/settings/call/settings_call_page.dart rename dialer/lib/{ => presentation}/features/settings/cryptography/key_management.dart (98%) rename dialer/lib/{ => presentation}/features/settings/settings.dart (86%) create mode 100644 dialer/lib/presentation/features/settings/settings_page.dart rename dialer/lib/{ => presentation}/features/voicemail/voicemail_page.dart (97%) delete mode 100644 dialer/lib/services/block_service.dart delete mode 100644 dialer/lib/services/contact_service.dart delete mode 100644 dialer/lib/widgets/color_darkener.dart delete mode 100644 dialer/lib/widgets/username_color_generator.dart diff --git a/dialer/lib/core/config/app_config.dart b/dialer/lib/core/config/app_config.dart new file mode 100644 index 0000000..d937e6c --- /dev/null +++ b/dialer/lib/core/config/app_config.dart @@ -0,0 +1,14 @@ +class AppConfig { + // Private constructor to prevent instantiation + AppConfig._(); + + // Global configuration + static bool isStealthMode = false; + + // App initialization + static Future initialize() async { + const stealthFlag = String.fromEnvironment('STEALTH', defaultValue: 'false'); + isStealthMode = stealthFlag.toLowerCase() == 'true'; + print('Stealth mode is ${isStealthMode ? 'enabled' : 'disabled'}'); + } +} diff --git a/dialer/lib/core/navigation/app_router.dart b/dialer/lib/core/navigation/app_router.dart new file mode 100644 index 0000000..f63aff2 --- /dev/null +++ b/dialer/lib/core/navigation/app_router.dart @@ -0,0 +1,57 @@ +import 'package:flutter/material.dart'; +import '../../presentation/features/call/call_page.dart'; +import '../../presentation/features/call/incoming_call_page.dart'; +import '../../presentation/features/home/home_page.dart'; +import '../../presentation/features/settings/settings.dart'; // Updated import +import '../../presentation/features/contacts/contact_page.dart'; +import '../../presentation/features/composition/composition.dart'; +import 'dart:typed_data'; + +class AppRouter { + static Route generateRoute(RouteSettings settings) { + switch (settings.name) { + case '/': + return MaterialPageRoute(builder: (_) => const MyHomePage()); + + case '/settings': + return MaterialPageRoute(builder: (_) => const SettingsPage()); // Now correctly imported + + case '/composition': + return MaterialPageRoute(builder: (_) => const CompositionPage()); + + case '/contacts': + return MaterialPageRoute(builder: (_) => const ContactPage()); + + case '/call': + final args = settings.arguments as Map; + return MaterialPageRoute( + settings: settings, + builder: (_) => CallPage( + displayName: args['displayName'] as String, + phoneNumber: args['phoneNumber'] as String, + thumbnail: args['thumbnail'] as Uint8List?, + ), + ); + + case '/incoming_call': + final args = settings.arguments as Map; + return MaterialPageRoute( + settings: settings, + builder: (_) => IncomingCallPage( + displayName: args['displayName'] as String, + phoneNumber: args['phoneNumber'] as String, + thumbnail: args['thumbnail'] as Uint8List?, + ), + ); + + default: + return MaterialPageRoute( + builder: (_) => Scaffold( + body: Center( + child: Text('No route defined for ${settings.name}'), + ), + ), + ); + } + } +} diff --git a/dialer/lib/domain/services/block_service.dart b/dialer/lib/domain/services/block_service.dart new file mode 100644 index 0000000..5a4a0b0 --- /dev/null +++ b/dialer/lib/domain/services/block_service.dart @@ -0,0 +1,78 @@ +import 'package:flutter/material.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +/// Service for managing blocked phone numbers +class BlockService { + static const String _blockedNumbersKey = 'blocked_numbers'; + + // Private constructor + BlockService._privateConstructor(); + + // Singleton instance + static final BlockService _instance = BlockService._privateConstructor(); + + // Factory constructor to return the same instance + factory BlockService() { + return _instance; + } + + /// Block a phone number + Future blockNumber(String phoneNumber) async { + try { + final prefs = await SharedPreferences.getInstance(); + final blockedNumbers = prefs.getStringList(_blockedNumbersKey) ?? []; + + // Don't add if already blocked + if (blockedNumbers.contains(phoneNumber)) { + return true; + } + + blockedNumbers.add(phoneNumber); + return await prefs.setStringList(_blockedNumbersKey, blockedNumbers); + } catch (e) { + debugPrint('Error blocking number: $e'); + return false; + } + } + + /// Unblock a phone number + Future unblockNumber(String phoneNumber) async { + try { + final prefs = await SharedPreferences.getInstance(); + final blockedNumbers = prefs.getStringList(_blockedNumbersKey) ?? []; + + if (!blockedNumbers.contains(phoneNumber)) { + return true; + } + + blockedNumbers.remove(phoneNumber); + return await prefs.setStringList(_blockedNumbersKey, blockedNumbers); + } catch (e) { + debugPrint('Error unblocking number: $e'); + return false; + } + } + + /// Check if a number is blocked + Future isNumberBlocked(String phoneNumber) async { + try { + final prefs = await SharedPreferences.getInstance(); + final blockedNumbers = prefs.getStringList(_blockedNumbersKey) ?? []; + return blockedNumbers.contains(phoneNumber); + } catch (e) { + debugPrint('Error checking if number is blocked: $e'); + return false; + } + } + + /// Get all blocked numbers + Future> getBlockedNumbers() async { + try { + final prefs = await SharedPreferences.getInstance(); + return prefs.getStringList(_blockedNumbersKey) ?? []; + } catch (e) { + debugPrint('Error getting blocked numbers: $e'); + return []; + } + } +} diff --git a/dialer/lib/services/call_service.dart b/dialer/lib/domain/services/call_service.dart similarity index 99% rename from dialer/lib/services/call_service.dart rename to dialer/lib/domain/services/call_service.dart index d14073e..1c6d242 100644 --- a/dialer/lib/services/call_service.dart +++ b/dialer/lib/domain/services/call_service.dart @@ -1,9 +1,9 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import '../features/call/call_page.dart'; -import '../features/call/incoming_call_page.dart'; -import '../services/contact_service.dart'; +import '../../presentation/features/call/call_page.dart'; +import '../../presentation/features/call/incoming_call_page.dart'; // Import the new page +import 'contact_service.dart'; class CallService { static const MethodChannel _channel = MethodChannel('call_service'); diff --git a/dialer/lib/domain/services/contact_service.dart b/dialer/lib/domain/services/contact_service.dart new file mode 100644 index 0000000..d961100 --- /dev/null +++ b/dialer/lib/domain/services/contact_service.dart @@ -0,0 +1,86 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_contacts/flutter_contacts.dart'; +import 'package:qr_flutter/qr_flutter.dart'; + +class ContactService { + Future> fetchContacts() async { + if (await FlutterContacts.requestPermission()) { + List contacts = await FlutterContacts.getContacts( + withProperties: true, + withThumbnail: true, + ); + return contacts; + } else { + // Permission denied + return []; + } + } + + Future> fetchFavoriteContacts() async { + if (await FlutterContacts.requestPermission()) { + // Get all contacts and filter for favorites + List allContacts = await FlutterContacts.getContacts( + withProperties: true, + withThumbnail: true, + ); + return allContacts.where((c) => c.isStarred).toList(); + } else { + // Permission denied + return []; + } + } + + Future addNewContact(Contact contact) async { + if (await FlutterContacts.requestPermission()) { + try { + return await FlutterContacts.insertContact(contact); + } catch (e) { + debugPrint('Error adding contact: $e'); + return null; + } + } + return null; + } + + void showContactQRCodeDialog(BuildContext context, Contact contact) { + final String vCard = contact.toVCard(); + + showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + backgroundColor: Colors.grey[900], + title: Text( + 'QR Code for ${contact.displayName}', + style: const TextStyle(color: Colors.white), + ), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + color: Colors.white, + padding: const EdgeInsets.all(16.0), + child: QrImageView( + data: vCard, + version: QrVersions.auto, + size: 200.0, + ), + ), + const SizedBox(height: 16.0), + const Text( + 'Scan this code to add this contact', + style: TextStyle(color: Colors.white70), + ), + ], + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: const Text('Close'), + ), + ], + ); + }, + ); + } +} diff --git a/dialer/lib/services/cryptography/asymmetric_crypto_service.dart b/dialer/lib/domain/services/cryptography/asymmetric_crypto_service.dart similarity index 100% rename from dialer/lib/services/cryptography/asymmetric_crypto_service.dart rename to dialer/lib/domain/services/cryptography/asymmetric_crypto_service.dart diff --git a/dialer/lib/services/obfuscate_service.dart b/dialer/lib/domain/services/obfuscate_service.dart similarity index 91% rename from dialer/lib/services/obfuscate_service.dart rename to dialer/lib/domain/services/obfuscate_service.dart index 6eaf43f..82f0bbf 100644 --- a/dialer/lib/services/obfuscate_service.dart +++ b/dialer/lib/domain/services/obfuscate_service.dart @@ -1,7 +1,7 @@ // lib/services/obfuscate_service.dart -import 'package:dialer/widgets/color_darkener.dart'; +import 'package:dialer/presentation/common/widgets/color_darkener.dart'; -import '../../globals.dart' as globals; +import '../../core/config/app_config.dart'; import 'dart:ui'; import 'dart:typed_data'; import 'package:flutter/material.dart'; @@ -20,7 +20,7 @@ class ObfuscateService { // Public method to obfuscate data String obfuscateData(String data) { - if (globals.isStealthMode) { + if (AppConfig.isStealthMode) { return _obfuscateData(data); } else { return data; @@ -61,7 +61,7 @@ class ObfuscatedAvatar extends StatelessWidget { if (imageBytes != null && imageBytes!.isNotEmpty) { return ClipOval( child: ImageFiltered( - imageFilter: globals.isStealthMode + imageFilter: AppConfig.isStealthMode ? ImageFilter.blur(sigmaX: 10, sigmaY: 10) : ImageFilter.blur(sigmaX: 0, sigmaY: 0), child: Image.memory( diff --git a/dialer/lib/domain/services/qr/qr_scanner.dart b/dialer/lib/domain/services/qr/qr_scanner.dart new file mode 100644 index 0000000..df405f6 --- /dev/null +++ b/dialer/lib/domain/services/qr/qr_scanner.dart @@ -0,0 +1,57 @@ +import 'package:flutter/material.dart'; +import 'package:mobile_scanner/mobile_scanner.dart'; + +class QRCodeScannerScreen extends StatefulWidget { + const QRCodeScannerScreen({super.key}); + + @override + _QRCodeScannerScreenState createState() => _QRCodeScannerScreenState(); +} + +class _QRCodeScannerScreenState extends State { + MobileScannerController cameraController = MobileScannerController(); + bool _flashEnabled = false; + + @override + void dispose() { + cameraController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Scan QR Code'), + actions: [ + IconButton( + icon: Icon(_flashEnabled ? Icons.flash_on : Icons.flash_off), + onPressed: () { + setState(() { + _flashEnabled = !_flashEnabled; + cameraController.toggleTorch(); + }); + }, + ), + IconButton( + icon: const Icon(Icons.flip_camera_ios), + onPressed: () => cameraController.switchCamera(), + ), + ], + ), + body: MobileScanner( + controller: cameraController, + onDetect: (capture) { + final List barcodes = capture.barcodes; + if (barcodes.isNotEmpty) { + // Return the first barcode value + final String? code = barcodes.first.rawValue; + if (code != null) { + Navigator.pop(context, code); + } + } + }, + ), + ); + } +} diff --git a/dialer/lib/features/call/incoming_call_page.dart b/dialer/lib/features/call/incoming_call_page.dart deleted file mode 100644 index 2bad2eb..0000000 --- a/dialer/lib/features/call/incoming_call_page.dart +++ /dev/null @@ -1,181 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:dialer/services/call_service.dart'; -import 'package:dialer/services/obfuscate_service.dart'; -import 'package:dialer/widgets/username_color_generator.dart'; -import 'package:dialer/features/call/call_page.dart'; - -class IncomingCallPage extends StatefulWidget { - final String displayName; - final String phoneNumber; - final Uint8List? thumbnail; - - const IncomingCallPage({ - super.key, - required this.displayName, - required this.phoneNumber, - this.thumbnail, - }); - - @override - _IncomingCallPageState createState() => _IncomingCallPageState(); -} - -class _IncomingCallPageState extends State { - static const MethodChannel _channel = MethodChannel('call_service'); - final ObfuscateService _obfuscateService = ObfuscateService(); - final CallService _callService = CallService(); - bool icingProtocolOk = true; - - void _toggleIcingProtocol() { - setState(() { - icingProtocolOk = !icingProtocolOk; - }); - } - - void _answerCall() async { - try { - final result = await _channel.invokeMethod('answerCall'); - print('IncomingCallPage: Answer call result: $result'); - if (result["status"] == "answered") { - Navigator.pushReplacement( - context, - MaterialPageRoute( - builder: (context) => CallPage( - displayName: widget.displayName, - phoneNumber: widget.phoneNumber, - thumbnail: widget.thumbnail, - ), - ), - ); - } - } catch (e) { - print("IncomingCallPage: Error answering call: $e"); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text("Error answering call: $e")), - ); - } - } - - void _declineCall() async { - try { - await _callService.hangUpCall(context); - } catch (e) { - print("IncomingCallPage: Error declining call: $e"); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text("Error declining call: $e")), - ); - } - } - - @override - Widget build(BuildContext context) { - const double avatarRadius = 45.0; - const double nameFontSize = 24.0; - const double statusFontSize = 16.0; - - return Scaffold( - body: Container( - color: Colors.black, - child: SafeArea( - child: Column( - children: [ - Container( - padding: const EdgeInsets.symmetric(vertical: 8.0), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - const SizedBox(height: 35), - ObfuscatedAvatar( - imageBytes: widget.thumbnail, - radius: avatarRadius, - backgroundColor: generateColorFromName(widget.displayName), - fallbackInitial: widget.displayName, - ), - const SizedBox(height: 4), - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - icingProtocolOk ? Icons.lock : Icons.lock_open, - color: icingProtocolOk ? Colors.green : Colors.red, - size: 16, - ), - const SizedBox(width: 4), - Text( - 'Icing protocol: ${icingProtocolOk ? "ok" : "ko"}', - style: TextStyle( - color: icingProtocolOk ? Colors.green : Colors.red, - fontSize: 12, - fontWeight: FontWeight.bold, - ), - ), - ], - ), - const SizedBox(height: 4), - Text( - _obfuscateService.obfuscateData(widget.displayName), - style: const TextStyle( - fontSize: nameFontSize, - color: Colors.white, - fontWeight: FontWeight.bold, - ), - ), - Text( - widget.phoneNumber, - style: const TextStyle(fontSize: statusFontSize, color: Colors.white70), - ), - const Text( - 'Incoming Call...', - style: TextStyle(fontSize: statusFontSize, color: Colors.white70), - ), - ], - ), - ), - const Spacer(), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 32.0, vertical: 16.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - GestureDetector( - onTap: _declineCall, - child: Container( - padding: const EdgeInsets.all(12), - decoration: const BoxDecoration( - color: Colors.red, - shape: BoxShape.circle, - ), - child: const Icon( - Icons.call_end, - color: Colors.white, - size: 32, - ), - ), - ), - GestureDetector( - onTap: _answerCall, - child: Container( - padding: const EdgeInsets.all(12), - decoration: const BoxDecoration( - color: Colors.green, - shape: BoxShape.circle, - ), - child: const Icon( - Icons.call, - color: Colors.white, - size: 32, - ), - ), - ), - ], - ), - ), - const SizedBox(height: 16), - ], - ), - ), - ), - ); - } -} \ No newline at end of file diff --git a/dialer/lib/features/contacts/contact_page.dart b/dialer/lib/features/contacts/contact_page.dart deleted file mode 100644 index 9f09490..0000000 --- a/dialer/lib/features/contacts/contact_page.dart +++ /dev/null @@ -1,26 +0,0 @@ -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( - body: contactState.loading - ? const LoadingIndicatorWidget() - : AlphabetScrollPage( - scrollOffset: contactState.scrollOffset, - contacts: contactState.contacts, // Use all contacts here - ), - ); - } -} diff --git a/dialer/lib/features/contacts/widgets/alphabet_scroll_page.dart b/dialer/lib/features/contacts/widgets/alphabet_scroll_page.dart deleted file mode 100644 index 822c36e..0000000 --- a/dialer/lib/features/contacts/widgets/alphabet_scroll_page.dart +++ /dev/null @@ -1,217 +0,0 @@ -import 'package:dialer/services/obfuscate_service.dart'; -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 'add_contact_button.dart'; -import 'contact_modal.dart'; -import 'share_own_qr.dart'; - -class AlphabetScrollPage extends StatefulWidget { - final double scrollOffset; - final List contacts; - - const AlphabetScrollPage({ - super.key, - required this.scrollOffset, - required this.contacts, - }); - - @override - _AlphabetScrollPageState createState() => _AlphabetScrollPageState(); -} - -class _AlphabetScrollPageState extends State { - late ScrollController _scrollController; - - final ObfuscateService _obfuscateService = ObfuscateService(); - - @override - void initState() { - super.initState(); - _scrollController = ScrollController(initialScrollOffset: widget.scrollOffset); - _scrollController.addListener(_onScroll); - } - - void _onScroll() { - final contactState = ContactState.of(context); - contactState.setScrollOffset(_scrollController.offset); - } - - Future _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> alphabetizedContacts = {}; - for (var contact in 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 Scaffold( - backgroundColor: Colors.black, - body: Column( - children: [ - // Top buttons row - Container( - color: Colors.black, - padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - AddContactButton(), - QRCodeButton(contacts: contacts, selfContact: selfContact), - ], - ), - ), - // Contact List - Expanded( - child: ListView.builder( - controller: _scrollController, - itemCount: alphabetKeys.length, - itemBuilder: (context, index) { - String letter = alphabetKeys[index]; - List contactsForLetter = alphabetizedContacts[letter]!; - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Alphabet Letter Header - Padding( - padding: const EdgeInsets.symmetric( - vertical: 8.0, horizontal: 16.0), - child: Text( - letter, - style: const TextStyle( - fontSize: 28, - fontWeight: FontWeight.bold, - color: Colors.white, - ), - ), - ), - // Contact Entries - ...contactsForLetter.map((contact) { - String phoneNumber = contact.phones.isNotEmpty - ? _obfuscateService.obfuscateData(contact.phones.first.number) - : 'No phone number'; - Color avatarColor = - generateColorFromName(contact.displayName); - return ListTile( - leading: ObfuscatedAvatar( - imageBytes: contact.thumbnail, - radius: 25, - backgroundColor: avatarColor, - fallbackInitial: contact.displayName, - ), - title: Text( - _obfuscateService.obfuscateData(contact.displayName), - style: const TextStyle(color: Colors.white), - ), - subtitle: Text( - phoneNumber, - style: const 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, - ); - }, - ); - }, - ); - }), - ], - ); - }, - ), - ), - ], - ), - ); - } - - @override - void dispose() { - _scrollController.dispose(); - super.dispose(); - } -} diff --git a/dialer/lib/features/favorites/favorites_page.dart b/dialer/lib/features/favorites/favorites_page.dart deleted file mode 100644 index 47f74ec..0000000 --- a/dialer/lib/features/favorites/favorites_page.dart +++ /dev/null @@ -1,32 +0,0 @@ -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}); - - @override - _FavoritesPageState createState() => _FavoritesPageState(); -} - -class _FavoritesPageState extends State { - @override - void initState() { - super.initState(); - } - - @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 - ), - ); - } -} diff --git a/dialer/lib/globals.dart b/dialer/lib/globals.dart index 2750ab2..c1410d0 100644 --- a/dialer/lib/globals.dart +++ b/dialer/lib/globals.dart @@ -1 +1,7 @@ -bool isStealthMode = false; \ No newline at end of file +// Global variables accessible throughout the app +library globals; + +import 'core/config/app_config.dart'; + +// Whether the app is in stealth mode (obfuscated content) +bool get isStealthMode => AppConfig.isStealthMode; \ No newline at end of file diff --git a/dialer/lib/main.dart b/dialer/lib/main.dart index 2cc72e3..ad6f54e 100644 --- a/dialer/lib/main.dart +++ b/dialer/lib/main.dart @@ -1,25 +1,34 @@ -import 'package:dialer/features/home/home_page.dart'; -import 'package:dialer/features/home/default_dialer_prompt.dart'; +import 'package:dialer/presentation/features/home/home_page.dart'; +import 'package:dialer/presentation/features/home/default_dialer_prompt.dart'; import 'package:flutter/material.dart'; -import 'package:dialer/features/contacts/contact_state.dart'; -import 'package:dialer/services/call_service.dart'; import 'package:flutter/services.dart'; -import 'globals.dart' as globals; -import 'package:dialer/services/cryptography/asymmetric_crypto_service.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:provider/provider.dart'; +import 'core/config/app_config.dart'; +import 'domain/services/call_service.dart'; +import 'domain/services/cryptography/asymmetric_crypto_service.dart'; +import 'presentation/features/contacts/contact_state.dart'; + void main() async { WidgetsFlutterBinding.ensureInitialized(); - const stealthFlag = String.fromEnvironment('STEALTH', defaultValue: 'false'); - globals.isStealthMode = stealthFlag.toLowerCase() == 'true'; + + // Initialize app configuration + await AppConfig.initialize(); + // Initialize cryptography service with error handling final AsymmetricCryptoService cryptoService = AsymmetricCryptoService(); - await cryptoService.initializeDefaultKeyPair(); + try { + await cryptoService.initializeDefaultKeyPair(); + } catch (e) { + debugPrint('Error initializing cryptography: $e'); + // Continue app initialization even if crypto fails + } // Request permissions before running the app await _requestPermissions(); + // Initialize call service CallService(); runApp( @@ -29,7 +38,7 @@ void main() async { create: (_) => cryptoService, ), ], - child: Dialer(), + child: const DialerApp(), ), ); } @@ -49,8 +58,8 @@ Future _requestPermissions() async { } } -class Dialer extends StatelessWidget { - const Dialer({super.key}); +class DialerApp extends StatelessWidget { + const DialerApp({super.key}); Future _isDefaultDialer() async { const channel = MethodChannel('call_service'); @@ -67,6 +76,7 @@ class Dialer extends StatelessWidget { Widget build(BuildContext context) { return ContactState( child: MaterialApp( + title: 'Dialer App', navigatorKey: CallService.navigatorKey, theme: ThemeData( brightness: Brightness.dark, diff --git a/dialer/lib/presentation/common/theme/app_theme.dart b/dialer/lib/presentation/common/theme/app_theme.dart new file mode 100644 index 0000000..208a1de --- /dev/null +++ b/dialer/lib/presentation/common/theme/app_theme.dart @@ -0,0 +1,42 @@ +import 'package:flutter/material.dart'; + +class AppTheme { + static ThemeData get darkTheme => ThemeData( + brightness: Brightness.dark, + scaffoldBackgroundColor: Colors.black, + appBarTheme: const AppBarTheme( + backgroundColor: Colors.black, + elevation: 0, + ), + tabBarTheme: const TabBarThemeData( + labelColor: Colors.white, + unselectedLabelColor: Color.fromARGB(255, 158, 158, 158), + indicatorSize: TabBarIndicatorSize.label, + indicatorColor: Colors.white, + ), + bottomNavigationBarTheme: const BottomNavigationBarThemeData( + backgroundColor: Colors.black, + ), + floatingActionButtonTheme: const FloatingActionButtonThemeData( + backgroundColor: Colors.blue, + foregroundColor: Colors.white, + ), + textTheme: const TextTheme( + bodyLarge: TextStyle(color: Colors.white), + bodyMedium: TextStyle(color: Colors.white), + titleLarge: TextStyle(color: Colors.white), + ), + snackBarTheme: const SnackBarThemeData( + backgroundColor: Color(0xFF303030), + contentTextStyle: TextStyle(color: Colors.white), + ), + inputDecorationTheme: InputDecorationTheme( + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: BorderSide.none, + ), + filled: true, + fillColor: const Color.fromARGB(255, 30, 30, 30), + ), + ); +} diff --git a/dialer/lib/presentation/common/widgets/color_darkener.dart b/dialer/lib/presentation/common/widgets/color_darkener.dart new file mode 100644 index 0000000..be581cd --- /dev/null +++ b/dialer/lib/presentation/common/widgets/color_darkener.dart @@ -0,0 +1,21 @@ +import 'package:flutter/material.dart'; + +/// Darkens a color by a given percentage +Color darken(Color color, [double amount = 0.3]) { + assert(amount >= 0 && amount <= 1); + + final hsl = HSLColor.fromColor(color); + final darkened = hsl.withLightness((hsl.lightness - amount).clamp(0.0, 1.0)); + + return darkened.toColor(); +} + +/// Lightens a color by a given percentage +Color lighten(Color color, [double amount = 0.3]) { + assert(amount >= 0 && amount <= 1); + + final hsl = HSLColor.fromColor(color); + final lightened = hsl.withLightness((hsl.lightness + amount).clamp(0.0, 1.0)); + + return lightened.toColor(); +} \ No newline at end of file diff --git a/dialer/lib/widgets/loading_indicator.dart b/dialer/lib/presentation/common/widgets/loading_indicator.dart similarity index 100% rename from dialer/lib/widgets/loading_indicator.dart rename to dialer/lib/presentation/common/widgets/loading_indicator.dart diff --git a/dialer/lib/widgets/qr_scanner.dart b/dialer/lib/presentation/common/widgets/qr_scanner.dart similarity index 100% rename from dialer/lib/widgets/qr_scanner.dart rename to dialer/lib/presentation/common/widgets/qr_scanner.dart diff --git a/dialer/lib/presentation/common/widgets/username_color_generator.dart b/dialer/lib/presentation/common/widgets/username_color_generator.dart new file mode 100644 index 0000000..480cef4 --- /dev/null +++ b/dialer/lib/presentation/common/widgets/username_color_generator.dart @@ -0,0 +1,17 @@ +import 'package:flutter/material.dart'; + +/// Generates a deterministic color from a string input +Color generateColorFromName(String name) { + if (name.isEmpty) return Colors.grey; + + // Use the hashCode of the name to generate a consistent color + int hash = name.hashCode; + + // Use the hash to generate RGB values + final r = (hash & 0xFF0000) >> 16; + final g = (hash & 0x00FF00) >> 8; + final b = hash & 0x0000FF; + + // Create a color with these RGB values + return Color.fromARGB(255, r, g, b); +} diff --git a/dialer/lib/features/call/call_page.dart b/dialer/lib/presentation/features/call/call_page.dart similarity index 99% rename from dialer/lib/features/call/call_page.dart rename to dialer/lib/presentation/features/call/call_page.dart index 95f48a5..38c665b 100644 --- a/dialer/lib/features/call/call_page.dart +++ b/dialer/lib/presentation/features/call/call_page.dart @@ -1,9 +1,9 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_contacts/flutter_contacts.dart'; -import 'package:dialer/services/call_service.dart'; -import 'package:dialer/services/obfuscate_service.dart'; -import 'package:dialer/widgets/username_color_generator.dart'; +import 'package:dialer/domain/services/call_service.dart'; +import 'package:dialer/domain/services/obfuscate_service.dart'; +import 'package:dialer/presentation/common/widgets/username_color_generator.dart'; import 'package:flutter/services.dart'; class CallPage extends StatefulWidget { diff --git a/dialer/lib/presentation/features/call/incoming_call_page.dart b/dialer/lib/presentation/features/call/incoming_call_page.dart new file mode 100644 index 0000000..e1eae95 --- /dev/null +++ b/dialer/lib/presentation/features/call/incoming_call_page.dart @@ -0,0 +1,163 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'dart:typed_data'; +import '../../../domain/services/call_service.dart'; +import '../../../domain/services/obfuscate_service.dart'; +import 'package:dialer/presentation/common/widgets/username_color_generator.dart'; +import 'call_page.dart'; + +class IncomingCallPage extends StatefulWidget { + final String displayName; + final String phoneNumber; + final Uint8List? thumbnail; + + const IncomingCallPage({ + super.key, + required this.displayName, + required this.phoneNumber, + this.thumbnail, + }); + + @override + _IncomingCallPageState createState() => _IncomingCallPageState(); +} + +class _IncomingCallPageState extends State { + static const MethodChannel _channel = MethodChannel('call_service'); + final ObfuscateService _obfuscateService = ObfuscateService(); + final CallService _callService = CallService(); + + void _answerCall() async { + try { + final result = await _channel.invokeMethod('answerCall'); + debugPrint('IncomingCallPage: Answer call result: $result'); + + if (result["status"] == "answered") { + if (!mounted) return; + + Navigator.pushReplacement( + context, + MaterialPageRoute( + builder: (context) => CallPage( + displayName: widget.displayName, + phoneNumber: widget.phoneNumber, + thumbnail: widget.thumbnail, + ), + ), + ); + } + } catch (e) { + debugPrint("IncomingCallPage: Error answering call: $e"); + if (!mounted) return; + + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text("Error answering call: $e")), + ); + } + } + + void _declineCall() async { + try { + await _callService.hangUpCall(context); + } catch (e) { + debugPrint("IncomingCallPage: Error declining call: $e"); + if (!mounted) return; + + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text("Error declining call: $e")), + ); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.black, + body: SafeArea( + child: Column( + children: [ + const Spacer(), + ObfuscatedAvatar( + imageBytes: widget.thumbnail, + radius: 60, + backgroundColor: generateColorFromName(widget.displayName), + fallbackInitial: widget.displayName, + ), + const SizedBox(height: 24), + Text( + _obfuscateService.obfuscateData(widget.displayName), + style: const TextStyle( + fontSize: 28, + color: Colors.white, + fontWeight: FontWeight.bold, + ), + ), + Text( + widget.phoneNumber, + style: const TextStyle(fontSize: 18, color: Colors.white70), + ), + const SizedBox(height: 16), + const Text( + 'Incoming Call', + style: TextStyle(fontSize: 20, color: Colors.white70), + ), + const Spacer(), + Padding( + padding: const EdgeInsets.only(bottom: 48.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + _buildActionButton( + icon: Icons.call_end, + color: Colors.red, + onPressed: _declineCall, + label: 'Decline', + ), + _buildActionButton( + icon: Icons.call, + color: Colors.green, + onPressed: _answerCall, + label: 'Answer', + ), + ], + ), + ), + ], + ), + ), + ); + } + + Widget _buildActionButton({ + required IconData icon, + required Color color, + required VoidCallback onPressed, + required String label, + }) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + GestureDetector( + onTap: onPressed, + child: Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: color, + shape: BoxShape.circle, + ), + child: Icon( + icon, + color: Colors.white, + size: 32, + ), + ), + ), + const SizedBox(height: 8), + Text( + label, + style: const TextStyle(color: Colors.white), + ), + ], + ); + } +} \ No newline at end of file diff --git a/dialer/lib/features/composition/composition.dart b/dialer/lib/presentation/features/composition/composition.dart similarity index 98% rename from dialer/lib/features/composition/composition.dart rename to dialer/lib/presentation/features/composition/composition.dart index 6fb9962..14cd776 100644 --- a/dialer/lib/features/composition/composition.dart +++ b/dialer/lib/presentation/features/composition/composition.dart @@ -1,9 +1,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_contacts/flutter_contacts.dart'; import 'package:url_launcher/url_launcher.dart'; -import '../../services/contact_service.dart'; -import '../../services/obfuscate_service.dart'; -import '../../services/call_service.dart'; +import '../../../domain/services/contact_service.dart'; +import '../../../domain/services/obfuscate_service.dart'; +import '../../../domain/services/call_service.dart'; class CompositionPage extends StatefulWidget { const CompositionPage({super.key}); diff --git a/dialer/lib/presentation/features/contacts/contact_page.dart b/dialer/lib/presentation/features/contacts/contact_page.dart new file mode 100644 index 0000000..bebaad3 --- /dev/null +++ b/dialer/lib/presentation/features/contacts/contact_page.dart @@ -0,0 +1,20 @@ +import 'package:flutter/material.dart'; +import '../contacts/contact_state.dart'; +import '../contacts/widgets/alphabet_scroll_page.dart'; + +class ContactPage extends StatelessWidget { + const ContactPage({super.key}); + + @override + Widget build(BuildContext context) { + final contactState = ContactState.of(context); + return Scaffold( + body: contactState.loading + ? const Center(child: CircularProgressIndicator()) + : AlphabetScrollPage( + scrollOffset: contactState.scrollOffset, + contacts: contactState.contacts, + ), + ); + } +} diff --git a/dialer/lib/features/contacts/contact_state.dart b/dialer/lib/presentation/features/contacts/contact_state.dart similarity index 51% rename from dialer/lib/features/contacts/contact_state.dart rename to dialer/lib/presentation/features/contacts/contact_state.dart index 58e7574..2cf192d 100644 --- a/dialer/lib/features/contacts/contact_state.dart +++ b/dialer/lib/presentation/features/contacts/contact_state.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_contacts/flutter_contacts.dart'; -import '../../services/contact_service.dart'; +import '../../../domain/services/contact_service.dart'; class ContactState extends StatefulWidget { final Widget child; @@ -23,7 +23,7 @@ class _ContactStateState extends State { List _favoriteContacts = []; bool _loading = true; double _scrollOffset = 0.0; - Contact? _selfContact = Contact(); + Contact? _selfContact; // Getters for all contacts and favorites List get contacts => _allContacts; @@ -35,11 +35,23 @@ class _ContactStateState extends State { @override void initState() { super.initState(); - fetchContacts(); // Fetch all contacts by default + _initializeContacts(); // Rename to make it clear this is initialization FlutterContacts.addListener(_onContactChange); } - void _onContactChange() => fetchContacts(); + // Private method to initialize contacts without setState during build + Future _initializeContacts() async { + try { + List contacts = await _contactService.fetchContacts(); + _processContactsInitial(contacts); + } catch (e) { + debugPrint('Error fetching contacts: $e'); + } + } + + void _onContactChange() async { + await fetchContacts(); + } @override void dispose() { @@ -47,47 +59,92 @@ class _ContactStateState extends State { super.dispose(); } - // Fetch all contacts + // Fetch all contacts - public method that can be called after build Future fetchContacts() async { + if (!mounted) return; + setState(() => _loading = true); try { List contacts = await _contactService.fetchContacts(); - _processContacts(contacts); + if (mounted) { + _processContacts(contacts); + } + } catch (e) { + debugPrint('Error fetching contacts: $e'); } finally { - setState(() => _loading = false); + if (mounted) { + setState(() => _loading = false); + } } } // Fetch only favorite contacts Future fetchFavoriteContacts() async { + if (!mounted) return; + setState(() => _loading = true); try { List contacts = await _contactService.fetchFavoriteContacts(); - setState(() => _favoriteContacts = contacts); + if (mounted) { + setState(() => _favoriteContacts = contacts); + } + } catch (e) { + debugPrint('Error fetching favorite contacts: $e'); } finally { - setState(() => _loading = false); + if (mounted) { + setState(() => _loading = false); + } } } - void _processContacts(List contacts) { + // Process contacts without setState for initial loading + void _processContactsInitial(List contacts) { + if (!mounted) return; + + // Optimize by doing a single pass through contacts instead of multiple iterations + final filteredContacts = contacts.where((contact) => contact.phones.isNotEmpty).toList() + ..sort((a, b) => a.displayName.compareTo(b.displayName)); + + _selfContact = contacts.firstWhere( + (contact) => contact.displayName.toLowerCase() == "user", + orElse: () => Contact(), + ); + + if (_selfContact!.phones.isEmpty) { + _selfContact = null; + } + + _allContacts = filteredContacts; + _favoriteContacts = filteredContacts.where((contact) => contact.isStarred).toList(); + _loading = false; + + // Force a rebuild after initialization is complete + if (mounted) { + WidgetsBinding.instance.addPostFrameCallback((_) { + setState(() {}); + }); + } + } + + void _processContacts(List contacts) { + if (!mounted) return; + + // Same optimization as above + final filteredContacts = contacts.where((contact) => contact.phones.isNotEmpty).toList() + ..sort((a, b) => a.displayName.compareTo(b.displayName)); + _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; + _allContacts = filteredContacts; + _favoriteContacts = filteredContacts.where((contact) => contact.isStarred).toList(); }); } @@ -102,25 +159,6 @@ class _ContactStateState extends State { }); } - bool doesContactExist(Contact contact) { - // Example: consider it "existing" if there's a matching phone number - for (final existing in _allContacts) { - if (existing.toVCard() == contact.toVCard()) { - return true; - } - // for (final phone in existing.phones) { - // for (final newPhone in contact.phones) { - // // Simple exact match; you can do more advanced logic - // if (phone.normalizedNumber == newPhone.normalizedNumber) { - // return true; - // } - // } - // } We might switch to finer and smarter logic later, ex: remove trailing spaces, capitals - } - return false; - } - - @override Widget build(BuildContext context) { return _InheritedContactState( @@ -130,7 +168,6 @@ class _ContactStateState extends State { } } - class _InheritedContactState extends InheritedWidget { final _ContactStateState data; diff --git a/dialer/lib/features/contacts/widgets/add_contact_button.dart b/dialer/lib/presentation/features/contacts/widgets/add_contact_button.dart similarity index 97% rename from dialer/lib/features/contacts/widgets/add_contact_button.dart rename to dialer/lib/presentation/features/contacts/widgets/add_contact_button.dart index 399ab48..947bbcf 100644 --- a/dialer/lib/features/contacts/widgets/add_contact_button.dart +++ b/dialer/lib/presentation/features/contacts/widgets/add_contact_button.dart @@ -1,6 +1,6 @@ -import 'package:dialer/widgets/qr_scanner.dart'; import 'package:flutter/material.dart'; import 'package:flutter_contacts/flutter_contacts.dart'; +import '../../../../domain/services/qr/qr_scanner.dart'; class AddContactButton extends StatelessWidget { const AddContactButton({super.key}); diff --git a/dialer/lib/presentation/features/contacts/widgets/alphabet_scroll_page.dart b/dialer/lib/presentation/features/contacts/widgets/alphabet_scroll_page.dart new file mode 100644 index 0000000..ff723fe --- /dev/null +++ b/dialer/lib/presentation/features/contacts/widgets/alphabet_scroll_page.dart @@ -0,0 +1,232 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_contacts/flutter_contacts.dart'; +import '../../../../domain/services/obfuscate_service.dart'; +import 'package:dialer/presentation/common/widgets/username_color_generator.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 contacts; + + const AlphabetScrollPage({ + super.key, + required this.scrollOffset, + required this.contacts, + }); + + @override + _AlphabetScrollPageState createState() => _AlphabetScrollPageState(); +} + +class _AlphabetScrollPageState extends State { + late ScrollController _scrollController; + final ObfuscateService _obfuscateService = ObfuscateService(); + late Map> _alphabetizedContacts; + late List _alphabetKeys; + + @override + void initState() { + super.initState(); + _scrollController = ScrollController(initialScrollOffset: widget.scrollOffset); + _scrollController.addListener(_onScroll); + _organizeContacts(); + } + + @override + void didUpdateWidget(AlphabetScrollPage oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.contacts != widget.contacts) { + _organizeContacts(); + } + } + + void _organizeContacts() { + _alphabetizedContacts = {}; + for (var contact in widget.contacts) { + String firstLetter = contact.displayName.isNotEmpty + ? contact.displayName[0].toUpperCase() + : '#'; + (_alphabetizedContacts[firstLetter] ??= []).add(contact); + } + _alphabetKeys = _alphabetizedContacts.keys.toList()..sort(); + } + + void _onScroll() { + final contactState = ContactState.of(context); + contactState.setScrollOffset(_scrollController.offset); + } + + Future _refreshContacts() async { + final contactState = ContactState.of(context); + try { + await contactState.fetchContacts(); + } catch (e) { + debugPrint('Error refreshing contacts: $e'); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Failed to refresh contacts')), + ); + } + } + + Future _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); + } + + // Check if widget is still mounted before calling functions that use context + if (mounted) { + await _refreshContacts(); + } + } else { + debugPrint("Could not fetch contact details"); + } + } catch (e) { + debugPrint("Error updating favorite status: $e"); + // Only show snackbar if still mounted + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Failed to update contact favorite status')), + ); + } + } + } + + @override + Widget build(BuildContext context) { + final selfContact = ContactState.of(context).selfContact; + + return Scaffold( + backgroundColor: Colors.black, + body: Column( + children: [ + // Top buttons row + Container( + color: Colors.black, + padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + AddContactButton(), + QRCodeButton(contacts: widget.contacts, selfContact: selfContact), + ], + ), + ), + // Contact List + Expanded( + child: ListView.builder( + controller: _scrollController, + itemCount: _alphabetKeys.length, + itemBuilder: (context, index) { + String letter = _alphabetKeys[index]; + List contactsForLetter = _alphabetizedContacts[letter]!; + return _buildLetterSection(letter, contactsForLetter); + }, + ), + ), + ], + ), + ); + } + + Widget _buildLetterSection(String letter, List contactsForLetter) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Alphabet Letter Header + Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 16.0), + child: Text( + letter, + style: const TextStyle( + fontSize: 28, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + ), + // Contact Entries + ...contactsForLetter.map((contact) => _buildContactTile(contact)), + ], + ); + } + + Widget _buildContactTile(Contact contact) { + String phoneNumber = contact.phones.isNotEmpty + ? _obfuscateService.obfuscateData(contact.phones.first.number) + : 'No phone number'; + Color avatarColor = generateColorFromName(contact.displayName); + + return ListTile( + leading: ObfuscatedAvatar( + imageBytes: contact.thumbnail, + radius: 25, + backgroundColor: avatarColor, + fallbackInitial: contact.displayName, + ), + title: Text( + _obfuscateService.obfuscateData(contact.displayName), + style: const TextStyle(color: Colors.white), + ), + subtitle: Text( + phoneNumber, + style: const TextStyle(color: Colors.white70), + ), + onTap: () => _showContactModal(contact), + ); + } + + void _showContactModal(Contact contact) { + showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (context) { + return ContactModal( + contact: contact, + onEdit: () => _onEditContact(contact), + onToggleFavorite: () => _toggleFavorite(contact), + isFavorite: contact.isStarred, + ); + }, + ); + } + + Future _onEditContact(Contact contact) 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.'), + ), + ); + } + } + } + + @override + void dispose() { + _scrollController.dispose(); + super.dispose(); + } +} diff --git a/dialer/lib/features/contacts/widgets/contact_modal.dart b/dialer/lib/presentation/features/contacts/widgets/contact_modal.dart similarity index 50% rename from dialer/lib/features/contacts/widgets/contact_modal.dart rename to dialer/lib/presentation/features/contacts/widgets/contact_modal.dart index a73f7e6..ddf8692 100644 --- a/dialer/lib/features/contacts/widgets/contact_modal.dart +++ b/dialer/lib/presentation/features/contacts/widgets/contact_modal.dart @@ -1,11 +1,12 @@ -import 'package:dialer/services/obfuscate_service.dart'; 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'; -import '../../../services/block_service.dart'; -import '../../../services/contact_service.dart'; -import '../../../services/call_service.dart'; +import '../../../common/widgets/username_color_generator.dart'; +import '../../../common/widgets/color_darkener.dart'; +import '../../../../domain/services/obfuscate_service.dart'; +import '../../../../domain/services/block_service.dart'; +import '../../../../domain/services/contact_service.dart'; +import '../../../../domain/services/call_service.dart'; class ContactModal extends StatefulWidget { final Contact contact; @@ -30,6 +31,7 @@ class _ContactModalState extends State { bool isBlocked = false; final ObfuscateService _obfuscateService = ObfuscateService(); final CallService _callService = CallService(); + final ContactService _contactService = ContactService(); @override void initState() { @@ -43,9 +45,11 @@ class _ContactModalState extends State { Future _checkIfBlocked() async { if (phoneNumber != 'No phone number') { bool blocked = await BlockService().isNumberBlocked(phoneNumber); - setState(() { - isBlocked = blocked; - }); + if (mounted) { + setState(() { + isBlocked = blocked; + }); + } } } @@ -54,30 +58,31 @@ class _ContactModalState extends State { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('No phone number to block or unblock')), ); - } else if (isBlocked) { + return; + } + + if (isBlocked) { await BlockService().unblockNumber(phoneNumber); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('$phoneNumber has been unblocked')), - ); + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('$phoneNumber has been unblocked')), + ); + } } else { await BlockService().blockNumber(phoneNumber); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('$phoneNumber has been blocked')), - ); + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('$phoneNumber has been blocked')), + ); + } } - if (phoneNumber != 'No phone number') { + if (phoneNumber != 'No phone number' && mounted) { _checkIfBlocked(); } - Navigator.of(context).pop(); - } - 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'); + if (mounted) { + Navigator.of(context).pop(); } } @@ -119,34 +124,38 @@ class _ContactModalState extends State { ), ); - if (shouldDelete) { + if (shouldDelete && mounted) { try { // Delete the contact await FlutterContacts.deleteContact(widget.contact); // Show success message - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - '${_obfuscateService.obfuscateData(widget.contact.displayName)} deleted')), - ); + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + '${_obfuscateService.obfuscateData(widget.contact.displayName)} deleted')), + ); - // Close the modal - Navigator.of(context).pop(); + // Close the modal + Navigator.of(context).pop(); + } } catch (e) { // Handle errors and show a failure message - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: - Text('Failed to delete ${widget.contact.displayName}: $e')), - ); + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: + Text('Failed to delete ${widget.contact.displayName}: $e')), + ); + } } } } void _shareContactAsQRCode() { // Use the ContactService to show the QR code for the contact's vCard - ContactService().showContactQRCodeDialog(context, widget.contact); + _contactService.showContactQRCodeDialog(context, widget.contact); } @override @@ -155,6 +164,8 @@ class _ContactModalState extends State { ? _obfuscateService.obfuscateData(widget.contact.emails.first.address) : 'No email'; + final avatarColor = generateColorFromName(widget.contact.displayName); + return GestureDetector( onTap: () => Navigator.of(context).pop(), child: Container( @@ -162,6 +173,7 @@ class _ContactModalState extends State { child: GestureDetector( onTap: () {}, child: FractionallySizedBox( + heightFactor: 0.8, child: Container( decoration: BoxDecoration( color: Colors.grey[900], @@ -205,10 +217,6 @@ class _ContactModalState extends State { }, itemBuilder: (BuildContext context) { return [ - const PopupMenuItem( - value: 'show_associated_contacts', - child: Text('Show associated contacts'), - ), const PopupMenuItem( value: 'delete', child: Text('Delete'), @@ -217,15 +225,6 @@ class _ContactModalState extends State { value: 'share', child: Text('Share (via QR code)'), ), - const PopupMenuItem( - value: 'create_shortcut', - child: - Text('Create shortcut (to home screen)'), - ), - const PopupMenuItem( - value: 'set_ringtone', - child: Text('Set ringtone'), - ), ]; }, ), @@ -238,13 +237,28 @@ class _ContactModalState extends State { padding: const EdgeInsets.all(16.0), child: Column( children: [ - ObfuscatedAvatar( - imageBytes: widget.contact.thumbnail, - radius: 50, - backgroundColor: - generateColorFromName(widget.contact.displayName), - fallbackInitial: widget.contact.displayName, - ), + widget.contact.thumbnail != null && widget.contact.thumbnail!.isNotEmpty + ? ClipOval( + child: Image.memory( + widget.contact.thumbnail!, + fit: BoxFit.cover, + width: 100, + height: 100, + ), + ) + : CircleAvatar( + backgroundColor: avatarColor, + radius: 50, + child: Text( + widget.contact.displayName.isNotEmpty + ? widget.contact.displayName[0].toUpperCase() + : '?', + style: TextStyle( + color: darken(avatarColor), + fontSize: 40, + ), + ), + ), const SizedBox(height: 10), Text( _obfuscateService @@ -259,93 +273,99 @@ class _ContactModalState extends State { ), const Divider(color: Colors.grey), // Contact Actions - ListTile( - leading: const Icon(Icons.phone, color: Colors.green), - title: Text( - _obfuscateService.obfuscateData(phoneNumber), - style: const TextStyle(color: Colors.white), - ), - onTap: () async { - if (widget.contact.phones.isNotEmpty) { - await _callService.makeGsmCall( - context, - phoneNumber: phoneNumber, - displayName: widget.contact.displayName, - thumbnail: widget.contact.thumbnail, - ); - } - }, - ), - ListTile( - leading: const Icon(Icons.message, color: Colors.blue), - title: Text( - _obfuscateService.obfuscateData(phoneNumber), - style: const TextStyle(color: Colors.white), - ), - onTap: () { - if (widget.contact.phones.isNotEmpty) { - _launchSms(phoneNumber); - } - }, - ), - ListTile( - leading: const Icon(Icons.email, color: Colors.orange), - title: Text( - email, - style: const TextStyle(color: Colors.white), - ), - onTap: () { - if (widget.contact.emails.isNotEmpty) { - _launchEmail(email); - } - }, - ), - const Divider(color: Colors.grey), - // Favorite, Edit, and Block/Unblock Buttons - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0), - child: Column( - children: [ - // Favorite button - SizedBox( - width: double.infinity, - child: ElevatedButton.icon( - onPressed: () { - Navigator.of(context).pop(); - widget.onToggleFavorite(); + Expanded( + child: SingleChildScrollView( + child: Column( + children: [ + ListTile( + leading: const Icon(Icons.phone, color: Colors.green), + title: Text( + _obfuscateService.obfuscateData(phoneNumber), + style: const TextStyle(color: Colors.white), + ), + onTap: () async { + if (widget.contact.phones.isNotEmpty) { + await _callService.makeGsmCall(context, + phoneNumber: phoneNumber); + } }, - icon: Icon(widget.isFavorite - ? Icons.star - : Icons.star_border), - label: Text( - widget.isFavorite ? 'Unfavorite' : 'Favorite'), ), - ), - const SizedBox(height: 10), - // Edit button - SizedBox( - width: double.infinity, - child: ElevatedButton.icon( - onPressed: () => widget.onEdit(), - icon: const Icon(Icons.edit), - label: const Text('Edit Contact'), + ListTile( + leading: const Icon(Icons.message, color: Colors.blue), + title: Text( + _obfuscateService.obfuscateData(phoneNumber), + style: const TextStyle(color: Colors.white), + ), + onTap: () { + if (widget.contact.phones.isNotEmpty) { + _launchSms(phoneNumber); + } + }, ), - ), - const SizedBox(height: 10), - // Block/Unblock button - SizedBox( - width: double.infinity, - child: ElevatedButton.icon( - onPressed: _toggleBlockState, - icon: Icon( - isBlocked ? Icons.block : Icons.block_flipped), - label: Text(isBlocked ? 'Unblock' : 'Block'), + ListTile( + leading: const Icon(Icons.email, color: Colors.orange), + title: Text( + email, + style: const TextStyle(color: Colors.white), + ), + onTap: () { + if (widget.contact.emails.isNotEmpty) { + _launchEmail(email); + } + }, ), - ), - ], + const Divider(color: Colors.grey), + // Favorite, Edit, and Block/Unblock Buttons + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Column( + children: [ + // Favorite button + SizedBox( + width: double.infinity, + child: ElevatedButton.icon( + onPressed: () { + // First close the modal to avoid unmounted widget issues + Navigator.of(context).pop(); + // Then toggle the favorite status + widget.onToggleFavorite(); + }, + icon: Icon(widget.isFavorite + ? Icons.star + : Icons.star_border), + label: Text( + widget.isFavorite ? 'Unfavorite' : 'Favorite'), + ), + ), + const SizedBox(height: 10), + // Edit button + SizedBox( + width: double.infinity, + child: ElevatedButton.icon( + onPressed: () => widget.onEdit(), + icon: const Icon(Icons.edit), + label: const Text('Edit Contact'), + ), + ), + const SizedBox(height: 10), + // Block/Unblock button + SizedBox( + width: double.infinity, + child: ElevatedButton.icon( + onPressed: _toggleBlockState, + icon: Icon( + isBlocked ? Icons.block : Icons.block_flipped), + label: Text(isBlocked ? 'Unblock' : 'Block'), + ), + ), + ], + ), + ), + const SizedBox(height: 16), + ], + ), ), ), - const SizedBox(height: 16), ], ), ), diff --git a/dialer/lib/features/contacts/widgets/share_own_qr.dart b/dialer/lib/presentation/features/contacts/widgets/share_own_qr.dart similarity index 94% rename from dialer/lib/features/contacts/widgets/share_own_qr.dart rename to dialer/lib/presentation/features/contacts/widgets/share_own_qr.dart index 058ed35..10648b7 100644 --- a/dialer/lib/features/contacts/widgets/share_own_qr.dart +++ b/dialer/lib/presentation/features/contacts/widgets/share_own_qr.dart @@ -1,7 +1,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_contacts/contact.dart'; -import 'package:dialer/services/contact_service.dart'; +import 'package:dialer/domain/services/contact_service.dart'; class QRCodeButton extends StatelessWidget { final List contacts; diff --git a/dialer/lib/presentation/features/favorites/favorites_page.dart b/dialer/lib/presentation/features/favorites/favorites_page.dart new file mode 100644 index 0000000..712ece6 --- /dev/null +++ b/dialer/lib/presentation/features/favorites/favorites_page.dart @@ -0,0 +1,37 @@ +import 'package:flutter/material.dart'; +import '../contacts/contact_state.dart'; +import '../contacts/widgets/alphabet_scroll_page.dart'; + +class FavoritesPage extends StatelessWidget { + const FavoritesPage({super.key}); + + @override + Widget build(BuildContext context) { + final contactState = ContactState.of(context); + + if (contactState.loading) { + return const Scaffold( + backgroundColor: Colors.black, + body: Center(child: CircularProgressIndicator()), + ); + } + + final favorites = contactState.favoriteContacts; + + return Scaffold( + backgroundColor: Colors.black, + body: favorites.isEmpty + ? const Center( + child: Text( + 'No favorites yet.\nStar your contacts to add them here.', + style: TextStyle(color: Colors.white60), + textAlign: TextAlign.center, + ), + ) + : AlphabetScrollPage( + scrollOffset: contactState.scrollOffset, + contacts: favorites, + ), + ); + } +} diff --git a/dialer/lib/features/history/history_page.dart b/dialer/lib/presentation/features/history/history_page.dart similarity index 96% rename from dialer/lib/features/history/history_page.dart rename to dialer/lib/presentation/features/history/history_page.dart index 9906908..efd9afd 100644 --- a/dialer/lib/features/history/history_page.dart +++ b/dialer/lib/presentation/features/history/history_page.dart @@ -1,17 +1,17 @@ import 'dart:async'; -import 'package:dialer/services/obfuscate_service.dart'; -import 'package:dialer/widgets/color_darkener.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_contacts/flutter_contacts.dart'; import 'package:intl/intl.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:url_launcher/url_launcher.dart'; -import 'package:dialer/features/contacts/contact_state.dart'; -import 'package:dialer/widgets/username_color_generator.dart'; -import '../../services/block_service.dart'; +import '../../../domain/services/obfuscate_service.dart'; +import '../../common/widgets/color_darkener.dart'; +import '../../common/widgets/username_color_generator.dart'; +import '../../../domain/services/block_service.dart'; +import '../../../domain/services/call_service.dart'; +import '../contacts/contact_state.dart'; import '../contacts/widgets/contact_modal.dart'; -import '../../services/call_service.dart'; class History { final Contact contact; @@ -82,14 +82,19 @@ class _HistoryPageState extends State fullContact.isStarred = !fullContact.isStarred; await FlutterContacts.updateContact(fullContact); } - await _refreshContacts(); + // Check if still mounted before accessing context + if (mounted) { + await _refreshContacts(); + } } else { - print("Could not fetch contact details"); + debugPrint("Could not fetch contact details"); } } catch (e) { - print("Error updating favorite status: $e"); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Failed to update favorite status'))); + debugPrint("Error updating favorite status: $e"); + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Failed to update favorite status'))); + } } } diff --git a/dialer/lib/features/home/default_dialer_prompt.dart b/dialer/lib/presentation/features/home/default_dialer_prompt.dart similarity index 100% rename from dialer/lib/features/home/default_dialer_prompt.dart rename to dialer/lib/presentation/features/home/default_dialer_prompt.dart diff --git a/dialer/lib/features/home/home_page.dart b/dialer/lib/presentation/features/home/home_page.dart similarity index 96% rename from dialer/lib/features/home/home_page.dart rename to dialer/lib/presentation/features/home/home_page.dart index 47ded26..95ca9eb 100644 --- a/dialer/lib/features/home/home_page.dart +++ b/dialer/lib/presentation/features/home/home_page.dart @@ -1,14 +1,14 @@ -import 'package:dialer/services/obfuscate_service.dart'; import 'package:flutter/material.dart'; -import 'package:dialer/features/contacts/contact_page.dart'; -import 'package:dialer/features/favorites/favorites_page.dart'; -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 '../../services/contact_service.dart'; -import 'package:dialer/features/voicemail/voicemail_page.dart'; +import '../../../domain/services/obfuscate_service.dart'; +import '../contacts/contact_page.dart'; +import '../favorites/favorites_page.dart'; +import '../history/history_page.dart'; +import '../composition/composition.dart'; +import '../settings/settings.dart'; +import '../voicemail/voicemail_page.dart'; import '../contacts/widgets/contact_modal.dart'; +import 'package:flutter_contacts/flutter_contacts.dart'; +import 'package:dialer/domain/services/contact_service.dart'; class _MyHomePageState extends State with SingleTickerProviderStateMixin { late TabController _tabController; diff --git a/dialer/lib/features/settings/blocked/settings_blocked.dart b/dialer/lib/presentation/features/settings/blocked/settings_blocked.dart similarity index 100% rename from dialer/lib/features/settings/blocked/settings_blocked.dart rename to dialer/lib/presentation/features/settings/blocked/settings_blocked.dart diff --git a/dialer/lib/features/settings/call/settingsCall.dart b/dialer/lib/presentation/features/settings/call/settings_call.dart similarity index 98% rename from dialer/lib/features/settings/call/settingsCall.dart rename to dialer/lib/presentation/features/settings/call/settings_call.dart index a052b29..09928ab 100644 --- a/dialer/lib/features/settings/call/settingsCall.dart +++ b/dialer/lib/presentation/features/settings/call/settings_call.dart @@ -77,7 +77,7 @@ class _SettingsCallPageState extends State { }, child: const Text('Beep'), ), - // Add more ringtone options + // ...existing options... ], ); }, diff --git a/dialer/lib/presentation/features/settings/call/settings_call_page.dart b/dialer/lib/presentation/features/settings/call/settings_call_page.dart new file mode 100644 index 0000000..5ad4de4 --- /dev/null +++ b/dialer/lib/presentation/features/settings/call/settings_call_page.dart @@ -0,0 +1,32 @@ +import 'package:flutter/material.dart'; + +/// Calling settings configuration page +class SettingsCallPage extends StatelessWidget { + const SettingsCallPage({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.black, + appBar: AppBar( + title: const Text('Call Settings'), + ), + body: ListView( + children: const [ + ListTile( + title: Text('Call Forwarding', style: TextStyle(color: Colors.white)), + subtitle: Text('Manage call forwarding options', style: TextStyle(color: Colors.grey)), + ), + ListTile( + title: Text('Call Waiting', style: TextStyle(color: Colors.white)), + subtitle: Text('Enable or disable call waiting', style: TextStyle(color: Colors.grey)), + ), + ListTile( + title: Text('Caller ID', style: TextStyle(color: Colors.white)), + subtitle: Text('Manage your caller ID settings', style: TextStyle(color: Colors.grey)), + ), + ], + ), + ); + } +} diff --git a/dialer/lib/features/settings/cryptography/key_management.dart b/dialer/lib/presentation/features/settings/cryptography/key_management.dart similarity index 98% rename from dialer/lib/features/settings/cryptography/key_management.dart rename to dialer/lib/presentation/features/settings/cryptography/key_management.dart index f766ebc..681db26 100644 --- a/dialer/lib/features/settings/cryptography/key_management.dart +++ b/dialer/lib/presentation/features/settings/cryptography/key_management.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:dialer/services/cryptography/asymmetric_crypto_service.dart'; +import 'package:dialer/domain/services/cryptography/asymmetric_crypto_service.dart'; class ManageKeysPage extends StatefulWidget { const ManageKeysPage({Key? key}) : super(key: key); diff --git a/dialer/lib/features/settings/settings.dart b/dialer/lib/presentation/features/settings/settings.dart similarity index 86% rename from dialer/lib/features/settings/settings.dart rename to dialer/lib/presentation/features/settings/settings.dart index 410cc7f..d6f1808 100644 --- a/dialer/lib/features/settings/settings.dart +++ b/dialer/lib/presentation/features/settings/settings.dart @@ -1,10 +1,7 @@ -// settings.dart - import 'package:flutter/material.dart'; -import 'package:dialer/features/settings/call/settingsCall.dart'; -// import 'package:dialer/features/settings/cryptography/'; -import 'package:dialer/features/settings/blocked/settings_blocked.dart'; -import 'cryptography/key_management.dart'; +import 'package:dialer/presentation/features/settings/call/settings_call.dart'; +import 'package:dialer/presentation/features/settings/cryptography/key_management.dart'; +import 'package:dialer/presentation/features/settings/blocked/settings_blocked.dart'; class SettingsPage extends StatelessWidget { const SettingsPage({super.key}); diff --git a/dialer/lib/presentation/features/settings/settings_page.dart b/dialer/lib/presentation/features/settings/settings_page.dart new file mode 100644 index 0000000..e69de29 diff --git a/dialer/lib/features/voicemail/voicemail_page.dart b/dialer/lib/presentation/features/voicemail/voicemail_page.dart similarity index 97% rename from dialer/lib/features/voicemail/voicemail_page.dart rename to dialer/lib/presentation/features/voicemail/voicemail_page.dart index 1fadac8..1fea74d 100644 --- a/dialer/lib/features/voicemail/voicemail_page.dart +++ b/dialer/lib/presentation/features/voicemail/voicemail_page.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:audioplayers/audioplayers.dart'; class VoicemailPage extends StatefulWidget { - const VoicemailPage({Key? key}) : super(key: key); + const VoicemailPage({super.key}); @override State createState() => _VoicemailPageState(); @@ -14,6 +14,7 @@ class _VoicemailPageState extends State { Duration _duration = Duration.zero; Duration _position = Duration.zero; late AudioPlayer _audioPlayer; + bool _loading = false; @override void initState() { @@ -50,12 +51,12 @@ class _VoicemailPageState extends State { @override Widget build(BuildContext context) { + if (_loading) { + return const Center(child: CircularProgressIndicator()); + } + return Scaffold( backgroundColor: Colors.black, - // appBar: AppBar( - // // title: const Text('Voicemail'), - // backgroundColor: Colors.black, - // ), body: ListView( children: [ GestureDetector( diff --git a/dialer/lib/services/block_service.dart b/dialer/lib/services/block_service.dart deleted file mode 100644 index d8aaaf0..0000000 --- a/dialer/lib/services/block_service.dart +++ /dev/null @@ -1,52 +0,0 @@ -import 'package:shared_preferences/shared_preferences.dart'; - -class BlockService { - static final BlockService _instance = BlockService._internal(); - - factory BlockService() { - return _instance; - } - - BlockService._internal(); - - // Function to add a number to the blocked list - Future blockNumber(String number) async { - if (number.isEmpty) return; - - final prefs = await SharedPreferences.getInstance(); - List blockedNumbers = prefs.getStringList('blockedNumbers') ?? []; - - if (!blockedNumbers.contains(number)) { - blockedNumbers.add(number); - await prefs.setStringList('blockedNumbers', blockedNumbers); - print('$number has been blocked'); - } else { - print('$number is already blocked'); - } - } - - // Function to remove a number from the blocked list - Future unblockNumber(String number) async { - if (number.isEmpty) return; - - final prefs = await SharedPreferences.getInstance(); - List blockedNumbers = prefs.getStringList('blockedNumbers') ?? []; - - if (blockedNumbers.contains(number)) { - blockedNumbers.remove(number); - await prefs.setStringList('blockedNumbers', blockedNumbers); - print('$number has been unblocked'); - } else { - print('$number is not blocked'); - } - } - - // Check if a number is blocked - Future isNumberBlocked(String number) async { - if (number.isEmpty) return false; - - final prefs = await SharedPreferences.getInstance(); - List blockedNumbers = prefs.getStringList('blockedNumbers') ?? []; - return blockedNumbers.contains(number); - } -} diff --git a/dialer/lib/services/contact_service.dart b/dialer/lib/services/contact_service.dart deleted file mode 100644 index ef00185..0000000 --- a/dialer/lib/services/contact_service.dart +++ /dev/null @@ -1,51 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_contacts/flutter_contacts.dart'; -import 'package:qr_flutter/qr_flutter.dart'; - -// Service to manage contact-related operations -class ContactService { - Future> fetchContacts() async { - if (await FlutterContacts.requestPermission()) { - return await FlutterContacts.getContacts( - withProperties: true, - withThumbnail: true, - withAccounts: true, - withGroups: true, - withPhoto: true); - } - return []; - } - - Future> fetchFavoriteContacts() async { - List contacts = await fetchContacts(); - return contacts.where((contact) => contact.isStarred).toList(); - } - - Future addNewContact(Contact contact) async { - await FlutterContacts.insertContact(contact); - } - - // Function to show an AlertDialog with a QR code for the contact's vCard - void showContactQRCodeDialog(BuildContext context, Contact contact) { - showDialog( - barrierColor: Colors.white24, - context: context, - barrierDismissible: true, - builder: (BuildContext context) { - return AlertDialog( - backgroundColor: Colors.black, - content: SizedBox( - width: 200, - height: 220, - child: QrImageView( - data: contact.toVCard(), // Generate vCard QR code - version: QrVersions.auto, - backgroundColor: Colors.white, // Make sure QR code is visible on black background - size: 200.0, - ), - ), - ); - }, - ); - } -} diff --git a/dialer/lib/widgets/color_darkener.dart b/dialer/lib/widgets/color_darkener.dart deleted file mode 100644 index 8442304..0000000 --- a/dialer/lib/widgets/color_darkener.dart +++ /dev/null @@ -1,10 +0,0 @@ -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/dialer/lib/widgets/username_color_generator.dart b/dialer/lib/widgets/username_color_generator.dart deleted file mode 100644 index 5686a48..0000000 --- a/dialer/lib/widgets/username_color_generator.dart +++ /dev/null @@ -1,12 +0,0 @@ -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), - ); -}