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/call_page.dart b/dialer/lib/features/call/call_page.dart deleted file mode 100644 index 0d7cf48..0000000 --- a/dialer/lib/features/call/call_page.dart +++ /dev/null @@ -1,524 +0,0 @@ -import 'dart:async'; -import 'dart:typed_data'; -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'; - -class CallPage extends StatefulWidget { - final String displayName; - final String phoneNumber; - final Uint8List? thumbnail; - - const CallPage({ - super.key, - required this.displayName, - required this.phoneNumber, - this.thumbnail, - }); - - @override - _CallPageState createState() => _CallPageState(); -} - -class _CallPageState extends State { - final ObfuscateService _obfuscateService = ObfuscateService(); - final CallService _callService = CallService(); - bool isMuted = false; - bool isSpeaker = false; - bool isKeypadVisible = false; - bool icingProtocolOk = true; - String _typedDigits = ""; - Timer? _callTimer; - int _callSeconds = 0; - String _callStatus = "Calling..."; - StreamSubscription? _callStateSubscription; - StreamSubscription>? _audioStateSubscription; - - bool get isNumberUnknown => widget.displayName == widget.phoneNumber; - - @override - void initState() { - super.initState(); - _checkInitialCallState(); - _listenToCallState(); - _listenToAudioState(); - _setInitialAudioState(); - } - - @override - void dispose() { - _callTimer?.cancel(); - _callStateSubscription?.cancel(); - _audioStateSubscription?.cancel(); - super.dispose(); - } - - void _setInitialAudioState() { - final initialAudioState = _callService.currentAudioState; - if (initialAudioState != null) { - setState(() { - isMuted = initialAudioState['muted'] ?? false; - isSpeaker = initialAudioState['speaker'] ?? false; - }); - } - } - - void _checkInitialCallState() async { - try { - final state = await _callService.getCallState(); - print('CallPage: Initial call state: $state'); - if (mounted && state == "active") { - setState(() { - _callStatus = "00:00"; - _startCallTimer(); - }); - } - } catch (e) { - print('CallPage: Error checking initial state: $e'); - } - } - - void _listenToCallState() { - _callStateSubscription = _callService.callStateStream.listen((state) { - print('CallPage: Call state changed to $state'); - if (mounted) { - setState(() { - if (state == "active") { - _callStatus = "00:00"; - _startCallTimer(); - } else if (state == "disconnected" || state == "disconnecting") { - _callTimer?.cancel(); - _callStatus = "Call Ended"; - } else { - _callStatus = "Calling..."; - } - }); - } - }); - } - - void _listenToAudioState() { - _audioStateSubscription = _callService.audioStateStream.listen((state) { - if (mounted) { - setState(() { - isMuted = state['muted'] ?? isMuted; - isSpeaker = state['speaker'] ?? isSpeaker; - }); - } - }); - } - - void _startCallTimer() { - _callTimer?.cancel(); - _callTimer = Timer.periodic(const Duration(seconds: 1), (timer) { - if (mounted) { - setState(() { - _callSeconds++; - final minutes = (_callSeconds ~/ 60).toString().padLeft(2, '0'); - final seconds = (_callSeconds % 60).toString().padLeft(2, '0'); - _callStatus = '$minutes:$seconds'; - }); - } - }); - } - - void _addDigit(String digit) { - setState(() { - _typedDigits += digit; - }); - } - - void _toggleMute() async { - try { - print('CallPage: Toggling mute, current state: $isMuted'); - final result = await _callService.muteCall(context, mute: !isMuted); - print('CallPage: Mute call result: $result'); - if (mounted && result['status'] != 'success') { - print('CallPage: Failed to toggle mute: ${result['message']}'); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('Failed to toggle mute: ${result['message']}')), - ); - } - } catch (e) { - print('CallPage: Error toggling mute: $e'); - if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Error toggling mute: $e')), - ); - } - } - } - - Future _toggleSpeaker() async { - try { - print('CallPage: Toggling speaker, current state: $isSpeaker'); - final result = - await _callService.speakerCall(context, speaker: !isSpeaker); - print('CallPage: Speaker call result: $result'); - if (result['status'] != 'success') { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('Failed to toggle speaker: ${result['message']}')), - ); - } - } catch (e) { - print('CallPage: Error toggling speaker: $e'); - if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Error toggling speaker: $e')), - ); - } - } - } - - void _toggleKeypad() { - setState(() { - isKeypadVisible = !isKeypadVisible; - }); - } - - void _toggleIcingProtocol() { - setState(() { - icingProtocolOk = !icingProtocolOk; - }); - } - - void _hangUp() async { - try { - print('CallPage: Initiating hangUp'); - final result = await _callService.hangUpCall(context); - print('CallPage: Hang up result: $result'); - } catch (e) { - print('CallPage: Error hanging up: $e'); - if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text("Error hanging up: $e")), - ); - } - } - } - - void _addContact() async { - if (await FlutterContacts.requestPermission()) { - final newContact = Contact()..phones = [Phone(widget.phoneNumber)]; - final updatedContact = - await FlutterContacts.openExternalInsert(newContact); - if (updatedContact != null) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Contact added successfully!')), - ); - } - } else { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Permission denied for contacts')), - ); - } - } - - @override - Widget build(BuildContext context) { - final double avatarRadius = isKeypadVisible ? 45.0 : 45.0; - final double nameFontSize = isKeypadVisible ? 24.0 : 24.0; - final double statusFontSize = isKeypadVisible ? 16.0 : 16.0; - - print( - 'CallPage: Building UI, _callStatus: $_callStatus, route: ${ModalRoute.of(context)?.settings.name ?? "unknown"}'); - return PopScope( - canPop: - _callStatus == "Call Ended", // Allow navigation only if call ended - child: 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: TextStyle( - fontSize: nameFontSize, - color: Colors.white, - fontWeight: FontWeight.bold, - ), - ), - Text( - widget.phoneNumber, - style: TextStyle( - fontSize: statusFontSize, color: Colors.white70), - ), - Text( - _callStatus, - style: TextStyle( - fontSize: statusFontSize, color: Colors.white70), - ), - ], - ), - ), - Expanded( - child: Column( - children: [ - if (isKeypadVisible) ...[ - const Spacer(flex: 2), - Padding( - padding: - const EdgeInsets.symmetric(horizontal: 20.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Expanded( - child: Text( - _typedDigits, - maxLines: 1, - textAlign: TextAlign.right, - overflow: TextOverflow.ellipsis, - style: const TextStyle( - fontSize: 24, - color: Colors.white, - fontWeight: FontWeight.bold, - ), - ), - ), - IconButton( - padding: EdgeInsets.zero, - onPressed: _toggleKeypad, - icon: const Icon(Icons.close, - color: Colors.white), - ), - ], - ), - ), - Container( - height: MediaQuery.of(context).size.height * 0.35, - margin: const EdgeInsets.symmetric(horizontal: 20), - child: GridView.count( - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - crossAxisCount: 3, - childAspectRatio: 1.3, - mainAxisSpacing: 8, - crossAxisSpacing: 8, - children: List.generate(12, (index) { - String label; - if (index < 9) { - label = '${index + 1}'; - } else if (index == 9) { - label = '*'; - } else if (index == 10) { - label = '0'; - } else { - label = '#'; - } - return GestureDetector( - onTap: () => _addDigit(label), - child: Container( - decoration: const BoxDecoration( - shape: BoxShape.circle, - color: Colors.transparent, - ), - child: Center( - child: Text( - label, - style: const TextStyle( - fontSize: 32, color: Colors.white), - ), - ), - ), - ); - }), - ), - ), - const Spacer(flex: 1), - ] else ...[ - const Spacer(), - Padding( - padding: - const EdgeInsets.symmetric(horizontal: 32.0), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Row( - mainAxisAlignment: - MainAxisAlignment.spaceEvenly, - children: [ - Column( - mainAxisSize: MainAxisSize.min, - children: [ - IconButton( - onPressed: _toggleMute, - icon: Icon( - isMuted ? Icons.mic_off : Icons.mic, - color: isMuted - ? Colors.amber - : Colors.white, - size: 32, - ), - ), - Text( - isMuted ? 'Unmute' : 'Mute', - style: const TextStyle( - color: Colors.white, - fontSize: 14), - ), - ], - ), - Column( - mainAxisSize: MainAxisSize.min, - children: [ - IconButton( - onPressed: _toggleKeypad, - icon: const Icon( - Icons.dialpad, - color: Colors.white, - size: 32, - ), - ), - const Text( - 'Keypad', - style: TextStyle( - color: Colors.white, - fontSize: 14), - ), - ], - ), - Column( - mainAxisSize: MainAxisSize.min, - children: [ - IconButton( - onPressed: _toggleSpeaker, - icon: Icon( - isSpeaker - ? Icons.volume_up - : Icons.volume_off, - color: isSpeaker - ? Colors.amber - : Colors.white, - size: 32, - ), - ), - const Text( - 'Speaker', - style: TextStyle( - color: Colors.white, - fontSize: 14), - ), - ], - ), - ], - ), - const SizedBox(height: 20), - Row( - mainAxisAlignment: - MainAxisAlignment.spaceEvenly, - children: [ - if (isNumberUnknown) - Column( - mainAxisSize: MainAxisSize.min, - children: [ - IconButton( - onPressed: _addContact, - icon: const Icon( - Icons.person_add, - color: Colors.white, - size: 32, - ), - ), - const Text( - 'Add Contact', - style: TextStyle( - color: Colors.white, - fontSize: 14), - ), - ], - ), - Column( - mainAxisSize: MainAxisSize.min, - children: [ - IconButton( - onPressed: () {}, - icon: const Icon( - Icons.sim_card, - color: Colors.white, - size: 32, - ), - ), - const Text( - 'Change SIM', - style: TextStyle( - color: Colors.white, - fontSize: 14), - ), - ], - ), - ], - ), - ], - ), - ), - const Spacer(flex: 3), - ], - ], - ), - ), - Padding( - padding: const EdgeInsets.only(bottom: 16.0), - child: GestureDetector( - onTap: _hangUp, - 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, - ), - ), - ), - ), - ], - ), - ), - ), - )); - } -} 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/features/home/home_page.dart b/dialer/lib/features/home/home_page.dart deleted file mode 100644 index 47ded26..0000000 --- a/dialer/lib/features/home/home_page.dart +++ /dev/null @@ -1,342 +0,0 @@ -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 '../contacts/widgets/contact_modal.dart'; - -class _MyHomePageState extends State with SingleTickerProviderStateMixin { - late TabController _tabController; - List _allContacts = []; - List _contactSuggestions = []; - final ContactService _contactService = ContactService(); - final ObfuscateService _obfuscateService = ObfuscateService(); - final TextEditingController _searchController = TextEditingController(); - late SearchController _searchBarController; - String _rawSearchInput = ''; - - @override - void initState() { - super.initState(); - _tabController = TabController(length: 4, vsync: this, initialIndex: 2); - _tabController.addListener(_handleTabIndex); - _searchBarController = SearchController(); - _searchBarController.addListener(() { - if (_searchController.text != _searchBarController.text) { - _rawSearchInput = _searchBarController.text; - _searchController.text = _rawSearchInput; - _onSearchChanged(_searchBarController.text); - } - }); - _fetchContacts(); - } - - void _fetchContacts() async { - _allContacts = await _contactService.fetchContacts(); - _contactSuggestions = List.from(_allContacts); - if (mounted) setState(() {}); - } - - void _clearSearch() { - _searchController.clear(); - _searchBarController.clear(); - _rawSearchInput = ''; - _onSearchChanged(''); - } - - void _onSearchChanged(String query) { - setState(() { - if (query.isEmpty) { - _contactSuggestions = List.from(_allContacts); - } else { - final normalizedQuery = _normalizeString(query.toLowerCase()); - _contactSuggestions = _allContacts.where((contact) { - final normalizedName = _normalizeString(contact.displayName.toLowerCase()); - return normalizedName.contains(normalizedQuery); - }).toList(); - } - }); - } - - String _normalizeString(String input) { - const accentMap = { - 'àáâãäå': 'a', - 'èéêë': 'e', - 'ìíîï': 'i', - 'òóôõö': 'o', - 'ùúûü': 'u', - 'ç': 'c', - 'ñ': 'n', - }; - String normalized = input; - accentMap.forEach((accents, base) { - for (var accent in accents.split('')) { - normalized = normalized.replaceAll(accent, base); - } - }); - return normalized; - } - - @override - void dispose() { - _searchController.dispose(); - _searchBarController.dispose(); - _tabController.removeListener(_handleTabIndex); - _tabController.dispose(); - super.dispose(); - } - - void _handleTabIndex() { - setState(() {}); - } - - 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); - _fetchContacts(); - } - } 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) { - return Scaffold( - backgroundColor: Colors.black, - body: Column( - children: [ - Padding( - padding: const EdgeInsets.only( - top: 24.0, - bottom: 10.0, - left: 16.0, - right: 16.0, - ), - child: Row( - children: [ - Expanded( - child: Container( - decoration: BoxDecoration( - color: const Color.fromARGB(255, 30, 30, 30), - borderRadius: BorderRadius.circular(12.0), - border: Border.all(color: Colors.grey.shade800, width: 1), - ), - child: SearchAnchor( - searchController: _searchBarController, - builder: (BuildContext context, SearchController controller) { - return GestureDetector( - onTap: () { - controller.openView(); - }, - child: Container( - decoration: BoxDecoration( - color: const Color.fromARGB(255, 30, 30, 30), - borderRadius: BorderRadius.circular(12.0), - border: Border.all(color: Colors.grey.shade800, width: 1), - ), - padding: const EdgeInsets.symmetric(vertical: 12.0, horizontal: 16.0), - child: Row( - children: [ - const Icon(Icons.search, color: Colors.grey, size: 24.0), - const SizedBox(width: 8.0), - Expanded( - child: Text( - _rawSearchInput.isEmpty - ? 'Search contacts' - : _rawSearchInput, - style: const TextStyle(color: Colors.grey, fontSize: 16.0), - overflow: TextOverflow.ellipsis, - ), - ), - if (_rawSearchInput.isNotEmpty) - GestureDetector( - onTap: _clearSearch, - child: const Icon( - Icons.clear, - color: Colors.grey, - size: 24.0, - ), - ), - ], - ), - ), - ); - }, - viewOnChanged: (query) { - - if (_searchBarController.text != query) { - _rawSearchInput = query; - _searchBarController.text = query; - _searchController.text = query; - } - _onSearchChanged(query); - }, - suggestionsBuilder: (BuildContext context, SearchController controller) { - return _contactSuggestions.map((contact) { - return ListTile( - key: ValueKey(contact.id), - title: Text( - _obfuscateService.obfuscateData(contact.displayName), - style: const TextStyle(color: Colors.white), - ), - onTap: () { - controller.closeView(contact.displayName); - 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) { - _fetchContacts(); - 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, - ); - }, - ); - }, - ); - }).toList(); - }, - ), - ), - ), - PopupMenuButton( - icon: const Icon(Icons.more_vert, color: Colors.white), - itemBuilder: (BuildContext context) => [ - const PopupMenuItem( - value: 'settings', - child: Text('Settings'), - ), - ], - onSelected: (String value) { - if (value == 'settings') { - Navigator.push( - context, - MaterialPageRoute(builder: (context) => const SettingsPage()), - ); - } - }, - ), - ], - ), - ), - Expanded( - child: Stack( - children: [ - TabBarView( - controller: _tabController, - children: const [ - FavoritesPage(), - HistoryPage(), - ContactPage(), - VoicemailPage(), - ], - ), - Positioned( - right: 20, - bottom: 20, - child: FloatingActionButton( - onPressed: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => const CompositionPage(), - ), - ); - }, - backgroundColor: Colors.blue, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(45), - ), - child: const Icon(Icons.dialpad, color: Colors.white), - ), - ), - ], - ), - ), - ], - ), - bottomNavigationBar: Container( - color: Colors.black, - child: TabBar( - controller: _tabController, - tabs: [ - Tab( - icon: Icon(_tabController.index == 0 - ? Icons.star - : Icons.star_border)), - Tab( - icon: Icon(_tabController.index == 1 - ? Icons.access_time_filled - : Icons.access_time_outlined)), - Tab( - icon: Icon(_tabController.index == 2 - ? Icons.contacts - : Icons.contacts_outlined)), - Tab( - icon: Icon(_tabController.index == 3 - ? Icons.voicemail - : Icons.voicemail_outlined), - ), - ], - labelColor: Colors.white, - unselectedLabelColor: const Color.fromARGB(255, 158, 158, 158), - indicatorSize: TabBarIndicatorSize.label, - indicatorColor: Colors.white, - ), - ), - ); - } -} - -class MyHomePage extends StatefulWidget { - const MyHomePage({super.key}); - - @override - _MyHomePageState createState() => _MyHomePageState(); -} \ No newline at end of file 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 d3513cf..7ebebfc 100644 --- a/dialer/lib/main.dart +++ b/dialer/lib/main.dart @@ -1,24 +1,34 @@ -import 'package:dialer/features/home/home_page.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 'core/navigation/app_router.dart'; +import 'domain/services/call_service.dart'; +import 'domain/services/cryptography/asymmetric_crypto_service.dart'; +import 'presentation/common/theme/app_theme.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( @@ -28,38 +38,49 @@ void main() async { create: (_) => cryptoService, ), ], - child: Dialer(), + child: const DialerApp(), ), ); } Future _requestPermissions() async { - Map statuses = await [ - Permission.phone, - Permission.contacts, - Permission.microphone, - ].request(); - if (statuses.values.every((status) => status.isGranted)) { - print("All required permissions granted"); - const channel = MethodChannel('call_service'); - await channel.invokeMethod('permissionsGranted'); - } else { - print("Permissions denied: ${statuses.entries.where((e) => !e.value.isGranted).map((e) => e.key).join(', ')}"); + try { + Map statuses = await [ + Permission.phone, + Permission.contacts, + Permission.microphone, + ].request(); + + if (statuses.values.every((status) => status.isGranted)) { + debugPrint("All required permissions granted"); + const channel = MethodChannel('call_service'); + await channel.invokeMethod('permissionsGranted'); + } else { + debugPrint("Permissions denied: ${statuses.entries.where((e) => !e.value.isGranted).map((e) => e.key).join(', ')}"); + } + } catch (e) { + debugPrint("Error requesting permissions: $e"); } } -class Dialer extends StatelessWidget { - const Dialer({super.key}); +class DialerApp extends StatelessWidget { + const DialerApp({super.key}); @override Widget build(BuildContext context) { return ContactState( child: MaterialApp( + title: 'Dialer App', navigatorKey: CallService.navigatorKey, - theme: ThemeData( - brightness: Brightness.dark, - ), - home: SafeArea(child: MyHomePage()), + theme: AppTheme.darkTheme, + onGenerateRoute: AppRouter.generateRoute, + initialRoute: '/', + // Add a builder to wrap all routes with SafeArea + builder: (context, child) { + return SafeArea( + child: child ?? const SizedBox.shrink(), + ); + }, ), ); } 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..f45e68d --- /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 TabBarTheme( + 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/presentation/features/call/call_page.dart b/dialer/lib/presentation/features/call/call_page.dart new file mode 100644 index 0000000..46f04b5 --- /dev/null +++ b/dialer/lib/presentation/features/call/call_page.dart @@ -0,0 +1,251 @@ +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 '../../../presentation/common/widgets/obfuscated_avatar.dart'; + +class CallPage extends StatefulWidget { + final String displayName; + final String phoneNumber; + final Uint8List? thumbnail; + + const CallPage({ + super.key, + required this.displayName, + required this.phoneNumber, + this.thumbnail, + }); + + @override + _CallPageState createState() => _CallPageState(); +} + +class _CallPageState extends State { + final ObfuscateService _obfuscateService = ObfuscateService(); + final CallService _callService = CallService(); + bool isMuted = false; + bool isSpeakerOn = false; + bool isKeypadVisible = false; + bool icingProtocolOk = true; + String _typedDigits = ""; + + void _addDigit(String digit) { + setState(() { + _typedDigits += digit; + }); + } + + void _toggleMute() { + setState(() { + isMuted = !isMuted; + }); + } + + void _toggleSpeaker() { + setState(() { + isSpeakerOn = !isSpeakerOn; + }); + } + + void _toggleKeypad() { + setState(() { + isKeypadVisible = !isKeypadVisible; + }); + } + + void _hangUp() async { + try { + await _callService.hangUpCall(context); + } catch (e) { + debugPrint("Error hanging up: $e"); + } + } + + @override + Widget build(BuildContext context) { + final double avatarRadius = 45.0; + + return Scaffold( + backgroundColor: Colors.black, + body: SafeArea( + child: Column( + children: [ + Container( + padding: const EdgeInsets.symmetric(vertical: 24.0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + ObfuscatedAvatar( + imageBytes: widget.thumbnail, + radius: avatarRadius, + backgroundColor: generateColorFromName(widget.displayName), + fallbackInitial: widget.displayName, + ), + const SizedBox(height: 16), + Text( + _obfuscateService.obfuscateData(widget.displayName), + style: const TextStyle( + fontSize: 24, + color: Colors.white, + fontWeight: FontWeight.bold, + ), + ), + Text( + widget.phoneNumber, + style: const TextStyle(fontSize: 16, color: Colors.white70), + ), + const SizedBox(height: 8), + Text( + 'Calling...', + style: const TextStyle(fontSize: 16, color: Colors.white70), + ), + ], + ), + ), + + Expanded( + child: isKeypadVisible + ? _buildKeypad() + : _buildCallControls(), + ), + + Padding( + padding: const EdgeInsets.only(bottom: 24.0), + child: GestureDetector( + onTap: _hangUp, + child: Container( + padding: const EdgeInsets.all(16), + decoration: const BoxDecoration( + color: Colors.red, + shape: BoxShape.circle, + ), + child: const Icon( + Icons.call_end, + color: Colors.white, + size: 32, + ), + ), + ), + ), + ], + ), + ), + ); + } + + Widget _buildKeypad() { + return Column( + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 24.0), + child: Text( + _typedDigits, + style: const TextStyle( + fontSize: 24, + color: Colors.white, + ), + ), + ), + const SizedBox(height: 16), + Expanded( + child: GridView.count( + crossAxisCount: 3, + childAspectRatio: 1.5, + children: [ + _buildDialButton('1'), + _buildDialButton('2'), + _buildDialButton('3'), + _buildDialButton('4'), + _buildDialButton('5'), + _buildDialButton('6'), + _buildDialButton('7'), + _buildDialButton('8'), + _buildDialButton('9'), + _buildDialButton('*'), + _buildDialButton('0'), + _buildDialButton('#'), + ], + ), + ), + TextButton( + onPressed: _toggleKeypad, + child: const Text('Hide Keypad'), + ), + ], + ); + } + + Widget _buildCallControls() { + return Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + _buildControlButton( + icon: isMuted ? Icons.mic_off : Icons.mic, + label: isMuted ? 'Unmute' : 'Mute', + onPressed: _toggleMute, + ), + _buildControlButton( + icon: Icons.dialpad, + label: 'Keypad', + onPressed: _toggleKeypad, + ), + _buildControlButton( + icon: isSpeakerOn ? Icons.volume_up : Icons.volume_off, + label: 'Speaker', + onPressed: _toggleSpeaker, + iconColor: isSpeakerOn ? Colors.amber : Colors.white, + ), + ], + ), + ], + ); + } + + Widget _buildDialButton(String digit) { + return InkWell( + onTap: () => _addDigit(digit), + child: Container( + margin: const EdgeInsets.all(4), + decoration: BoxDecoration( + shape: BoxShape.circle, + color: Colors.grey[800], + ), + child: Center( + child: Text( + digit, + style: const TextStyle( + fontSize: 24, + color: Colors.white, + ), + ), + ), + ), + ); + } + + Widget _buildControlButton({ + required IconData icon, + required String label, + required VoidCallback onPressed, + Color iconColor = Colors.white, + }) { + return Column( + children: [ + IconButton( + iconSize: 32, + icon: Icon(icon, color: iconColor), + onPressed: onPressed, + ), + Text( + label, + style: const TextStyle(color: Colors.white), + ), + ], + ); + } +} \ No newline at end of file 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/presentation/features/home/home_page.dart b/dialer/lib/presentation/features/home/home_page.dart new file mode 100644 index 0000000..a2ad446 --- /dev/null +++ b/dialer/lib/presentation/features/home/home_page.dart @@ -0,0 +1,333 @@ +import 'package:flutter/material.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/contact_state.dart'; +import '../contacts/widgets/contact_modal.dart'; +import 'package:flutter_contacts/flutter_contacts.dart'; + +class MyHomePage extends StatefulWidget { + const MyHomePage({super.key}); + + @override + _MyHomePageState createState() => _MyHomePageState(); +} + +class _MyHomePageState extends State + with SingleTickerProviderStateMixin { + late TabController _tabController; + List _allContacts = []; + List _contactSuggestions = []; + final ObfuscateService _obfuscateService = ObfuscateService(); + final SearchController _searchController = SearchController(); + bool _isInitialized = false; + + @override + void initState() { + super.initState(); + _tabController = TabController(length: 4, vsync: this, initialIndex: 1); + _tabController.addListener(_handleTabIndex); + } + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + if (!_isInitialized) { + // Use a post-frame callback to avoid setState during build + WidgetsBinding.instance.addPostFrameCallback((_) { + _fetchContacts(); + }); + _isInitialized = true; + } + } + + void _fetchContacts() async { + final contactState = ContactState.of(context); + // Wait for initial load to finish if it hasn't already + if (contactState.loading) { + await Future.delayed(const Duration(milliseconds: 100)); + } + // Then explicitly fetch contacts (which will call setState safely) + await contactState.fetchContacts(); + if (mounted) { + setState(() { + _allContacts = contactState.contacts; + _contactSuggestions = _allContacts; + }); + } + } + + void _onSearchChanged(String query) { + if (query.isEmpty) { + setState(() { + _contactSuggestions = List.from(_allContacts); // Reset suggestions + }); + return; + } + + // Convert query to lowercase once for efficiency + final lowerQuery = query.toLowerCase(); + + // Use where with efficient filter + final filtered = _allContacts.where((contact) { + final name = contact.displayName.toLowerCase(); + return name.contains(lowerQuery); + }).toList(); + + setState(() { + _contactSuggestions = filtered; + }); + } + + @override + void dispose() { + _searchController.dispose(); + _tabController.removeListener(_handleTabIndex); + _tabController.dispose(); + super.dispose(); + } + + void _handleTabIndex() { + setState(() {}); + } + + 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); + // Check if widget is still mounted before updating state + if (mounted) { + _fetchContacts(); + } + } + } + } catch (e) { + debugPrint("Error updating favorite status: $e"); + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Failed to update contact favorite status')), + ); + } + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.black, + body: Column( + children: [ + // Persistent Search Bar + Padding( + padding: const EdgeInsets.only( + top: 24.0, + bottom: 10.0, + left: 16.0, + right: 16.0, + ), + child: Row( + children: [ + Expanded( + child: SearchAnchor( + builder: (BuildContext context, SearchController controller) { + return GestureDetector( + onTap: () { + controller.openView(); // Open the search view + }, + child: Container( + decoration: BoxDecoration( + color: const Color.fromARGB(255, 30, 30, 30), + borderRadius: BorderRadius.circular(12.0), + border: Border.all(color: Colors.grey.shade800, width: 1), + ), + padding: const EdgeInsets.symmetric( + vertical: 12.0, horizontal: 16.0), + child: Row( + children: [ + const Icon(Icons.search, + color: Colors.grey, size: 24.0), + const SizedBox(width: 8.0), + Text( + controller.text.isEmpty + ? 'Search contacts' + : controller.text, + style: const TextStyle( + color: Colors.grey, fontSize: 16.0), + ), + const Spacer(), + if (controller.text.isNotEmpty) + GestureDetector( + onTap: () { + controller.clear(); + _onSearchChanged(''); + }, + child: const Icon( + Icons.clear, + color: Colors.grey, + size: 24.0, + ), + ), + ], + ), + ), + ); + }, + viewOnChanged: _onSearchChanged, // Update immediately + suggestionsBuilder: + (BuildContext context, SearchController controller) { + return _contactSuggestions.map((contact) { + return ListTile( + key: ValueKey(contact.id), + title: Text(_obfuscateService.obfuscateData(contact.displayName), + style: const TextStyle(color: Colors.white)), + onTap: () { + // Clear the search text input + controller.text = ''; + + // Close the search view + controller.closeView(contact.displayName); + + // Show the ContactModal when a contact is tapped + 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) { + _fetchContacts(); + 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, + ); + }, + ); + }, + ); + }).toList(); + }, + ), + ), + // 3-dot menu + PopupMenuButton( + icon: const Icon(Icons.more_vert, color: Colors.white), + itemBuilder: (BuildContext context) => [ + const PopupMenuItem( + value: 'settings', + child: Text('Settings'), + ), + ], + onSelected: (String value) { + if (value == 'settings') { + Navigator.push( + context, + MaterialPageRoute(builder: (context) => const SettingsPage()), + ); + } + }, + ), + ], + ), + ), + // Main content with TabBarView + Expanded( + child: Stack( + children: [ + TabBarView( + controller: _tabController, + children: const [ + FavoritesPage(), + HistoryPage(), + ContactPage(), + VoicemailPage(), + ], + ), + Positioned( + right: 20, + bottom: 20, + child: FloatingActionButton( + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const CompositionPage(), + ), + ); + }, + backgroundColor: Colors.blue, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(45), + ), + child: const Icon(Icons.dialpad, color: Colors.white), + ), + ), + ], + ), + ), + ], + ), + bottomNavigationBar: Container( + color: Colors.black, + child: TabBar( + controller: _tabController, + tabs: [ + Tab( + icon: Icon(_tabController.index == 0 + ? Icons.star + : Icons.star_border)), + Tab( + icon: Icon(_tabController.index == 1 + ? Icons.access_time_filled + : Icons.access_time_outlined)), + Tab( + icon: Icon(_tabController.index == 2 + ? Icons.contacts + : Icons.contacts_outlined)), + Tab( + icon: Icon(_tabController.index == 3 + ? Icons.voicemail + : Icons.voicemail_outlined), + ), + ], + labelColor: Colors.white, + unselectedLabelColor: const Color.fromARGB(255, 158, 158, 158), + indicatorSize: TabBarIndicatorSize.label, + indicatorColor: Colors.white, + ), + ), + ); + } +} 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), - ); -}