Compare commits

...

5 Commits

Author SHA1 Message Date
ae001a86cb feat: build + run scripts
All checks were successful
/ mirror (push) Successful in 4s
2024-12-20 09:33:08 +00:00
2904744fe2 Add docker build apk 2024-12-20 09:33:08 +00:00
e05880c9d8 contact-modal-delete-contact (#19)
All checks were successful
/ mirror (push) Successful in 4s
Reviewed-on: #19
Co-authored-by: Florian Griffon <florian.griffon@epitech.eu>
Co-committed-by: Florian Griffon <florian.griffon@epitech.eu>
2024-12-20 09:20:17 +00:00
e8aba933d0 Cryptographic Keys and History changes (#8)
All checks were successful
/ mirror (push) Successful in 4s
Co-authored-by: stcb <21@stcb.cc>
Reviewed-on: #8
Co-authored-by: Bartosz <bartosz.michalak@epitech.eu>
Co-committed-by: Bartosz <bartosz.michalak@epitech.eu>
2024-12-20 09:13:05 +00:00
518e49b3e6 feat: blocked page (#16)
All checks were successful
/ mirror (push) Successful in 4s
Reviewed-on: #16
Co-authored-by: Florian Griffon <florian.griffon@epitech.eu>
Co-committed-by: Florian Griffon <florian.griffon@epitech.eu>
2024-12-18 14:34:53 +00:00
21 changed files with 1020 additions and 253 deletions

View File

@ -5,6 +5,7 @@ gradle-wrapper.jar
/gradlew.bat /gradlew.bat
/local.properties /local.properties
GeneratedPluginRegistrant.java GeneratedPluginRegistrant.java
gradle.properties
# Remember to never publicly share your keystore. # Remember to never publicly share your keystore.
# See https://flutter.dev/to/reference-keystore # See https://flutter.dev/to/reference-keystore

View File

@ -3,6 +3,8 @@
<uses-permission android:name="android.permission.WRITE_CONTACTS"/> <uses-permission android:name="android.permission.WRITE_CONTACTS"/>
<uses-permission android:name="android.permission.CALL_PHONE" /> <uses-permission android:name="android.permission.CALL_PHONE" />
<uses-permission android:name="android.permission.SEND_SMS" /> <uses-permission android:name="android.permission.SEND_SMS" />
<uses-permission android:name="android.permission.READ_BLOCKED_NUMBERS" />
<uses-permission android:name="android.permission.WRITE_BLOCKED_NUMBERS" />
<uses-feature android:name="android.hardware.camera" android:required="false" /> <uses-feature android:name="android.hardware.camera" android:required="false" />
<!-- The INTERNET permission is required for development. Specifically, <!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application the Flutter tool needs it to communicate with the running application

View File

@ -3,6 +3,8 @@
<uses-permission android:name="android.permission.WRITE_CONTACTS"/> <uses-permission android:name="android.permission.WRITE_CONTACTS"/>
<uses-permission android:name="android.permission.CALL_PHONE" /> <uses-permission android:name="android.permission.CALL_PHONE" />
<uses-permission android:name="android.permission.SEND_SMS" /> <uses-permission android:name="android.permission.SEND_SMS" />
<uses-permission android:name="android.permission.READ_BLOCKED_NUMBERS" />
<uses-permission android:name="android.permission.WRITE_BLOCKED_NUMBERS" />
<application <application
android:label="com.example.dialer" android:label="com.example.dialer"
android:name="${applicationName}" android:name="${applicationName}"

View File

@ -4,6 +4,8 @@
<uses-permission android:name="android.permission.CALL_PHONE" /> <uses-permission android:name="android.permission.CALL_PHONE" />
<uses-permission android:name="android.permission.SEND_SMS" /> <uses-permission android:name="android.permission.SEND_SMS" />
<uses-permission android:name="android.permission.CAMERA"/> <uses-permission android:name="android.permission.CAMERA"/>
<uses-permission android:name="android.permission.READ_BLOCKED_NUMBERS" />
<uses-permission android:name="android.permission.WRITE_BLOCKED_NUMBERS" />
<uses-feature android:name="android.hardware.camera" android:required="false" /> <uses-feature android:name="android.hardware.camera" android:required="false" />
<!-- The INTERNET permission is required for development. Specifically, <!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application the Flutter tool needs it to communicate with the running application

View File

@ -2,5 +2,3 @@ org.gradle.jvmargs=-Xmx4G -XX:MaxMetaspaceSize=2G -XX:+HeapDumpOnOutOfMemoryErro
android.useAndroidX=true android.useAndroidX=true
android.enableJetifier=true android.enableJetifier=true
dev.steenbakker.mobile_scanner.useUnbundled=true dev.steenbakker.mobile_scanner.useUnbundled=true
org.gradle.java.home=/usr/lib/jvm/java-17-openjdk-amd64
#org.gradle.java.home=/usr/lib/jvm/java-17-openjdk-17.0.13.0.11-3.fc41.x86_64

5
dialer/build.sh Executable file
View File

@ -0,0 +1,5 @@
#!/bin/bash -e
IMG=git.gmoker.com/icing/flutter:main
docker run --rm -v "$PWD:/app/" "$IMG" build apk

View File

@ -2,8 +2,9 @@ import 'package:flutter/material.dart';
import 'package:flutter_contacts/flutter_contacts.dart'; import 'package:flutter_contacts/flutter_contacts.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
import 'package:dialer/widgets/username_color_generator.dart'; import 'package:dialer/widgets/username_color_generator.dart';
import '../../../widgets/block_service.dart';
class ContactModal extends StatelessWidget { class ContactModal extends StatefulWidget {
final Contact contact; final Contact contact;
final Function onEdit; final Function onEdit;
final Function onToggleFavorite; final Function onToggleFavorite;
@ -17,6 +18,55 @@ class ContactModal extends StatelessWidget {
required this.isFavorite, required this.isFavorite,
}); });
@override
_ContactModalState createState() => _ContactModalState();
}
class _ContactModalState extends State<ContactModal> {
late String phoneNumber;
bool isBlocked = false;
@override
void initState() {
super.initState();
phoneNumber = widget.contact.phones.isNotEmpty
? widget.contact.phones.first.number
: 'No phone number';
_checkIfBlocked();
}
Future<void> _checkIfBlocked() async {
if (phoneNumber != 'No phone number') {
bool blocked = await BlockService().isNumberBlocked(phoneNumber);
setState(() {
isBlocked = blocked;
});
}
}
Future<void> _toggleBlockState() async {
if (phoneNumber == 'No phone number') {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('No phone number to block or unblock')),
);
} else if (isBlocked) {
await BlockService().unblockNumber(phoneNumber);
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 (phoneNumber != 'No phone number') {
_checkIfBlocked();
}
Navigator.of(context).pop();
}
void _launchPhoneDialer(String phoneNumber) async { void _launchPhoneDialer(String phoneNumber) async {
final uri = Uri(scheme: 'tel', path: phoneNumber); final uri = Uri(scheme: 'tel', path: phoneNumber);
if (await canLaunchUrl(uri)) { if (await canLaunchUrl(uri)) {
@ -44,13 +94,51 @@ class ContactModal extends StatelessWidget {
} }
} }
void _deleteContact() async {
final bool shouldDelete = await showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Delete Contact'),
content: Text('Are you sure you want to delete ${widget.contact.displayName}?'),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(false),
child: const Text('Cancel'),
),
TextButton(
onPressed: () => Navigator.of(context).pop(true),
child: const Text('Delete'),
),
],
),
);
if (shouldDelete) {
try {
// Delete the contact
await FlutterContacts.deleteContact(widget.contact);
// Show success message
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('${widget.contact.displayName} deleted')),
);
// 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')),
);
}
}
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
String phoneNumber = contact.phones.isNotEmpty String email = widget.contact.emails.isNotEmpty
? contact.phones.first.number ? widget.contact.emails.first.address
: 'No phone number'; : 'No email';
String email =
contact.emails.isNotEmpty ? contact.emails.first.address : 'No email';
return GestureDetector( return GestureDetector(
onTap: () => Navigator.of(context).pop(), onTap: () => Navigator.of(context).pop(),
@ -59,17 +147,16 @@ class ContactModal extends StatelessWidget {
child: GestureDetector( child: GestureDetector(
onTap: () {}, onTap: () {},
child: FractionallySizedBox( child: FractionallySizedBox(
heightFactor: 0.7,
child: Container( child: Container(
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.grey[900], color: Colors.grey[900],
borderRadius: BorderRadius.vertical(top: Radius.circular(20)), borderRadius:
const BorderRadius.vertical(top: Radius.circular(20)),
), ),
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
// Modal Handle // Modal Handle and Three-Dot Menu
// Top Bar with Handle and Three-Dot Menu
Stack( Stack(
children: [ children: [
Align( Align(
@ -91,31 +178,33 @@ class ContactModal extends StatelessWidget {
child: Padding( child: Padding(
padding: const EdgeInsets.only(top: 10, right: 10), padding: const EdgeInsets.only(top: 10, right: 10),
child: PopupMenuButton<String>( child: PopupMenuButton<String>(
icon: Icon(Icons.more_vert, color: Colors.white), icon: const Icon(Icons.more_vert,
color: Colors.white),
onSelected: (String choice) { onSelected: (String choice) {
print( if (choice == 'delete') {
'Selected: $choice'); // Placeholder for menu actions _deleteContact();
}
}, },
itemBuilder: (BuildContext context) { itemBuilder: (BuildContext context) {
return <PopupMenuEntry<String>>[ return [
PopupMenuItem<String>( const PopupMenuItem<String>(
value: 'show_associated_contacts', value: 'show_associated_contacts',
child: Text('Show associated contacts'), child: Text('Show associated contacts'),
), ),
PopupMenuItem<String>( const PopupMenuItem<String>(
value: 'delete', value: 'delete',
child: Text('Delete'), child: Text('Delete'),
), ),
PopupMenuItem<String>( const PopupMenuItem<String>(
value: 'share', value: 'share',
child: Text('Share (via QR code)'), child: Text('Share (via QR code)'),
), ),
PopupMenuItem<String>( const PopupMenuItem<String>(
value: 'create_shortcut', value: 'create_shortcut',
child: child:
Text('Create shortcut (to home screen)'), Text('Create shortcut (to home screen)'),
), ),
PopupMenuItem<String>( const PopupMenuItem<String>(
value: 'set_ringtone', value: 'set_ringtone',
child: Text('Set ringtone'), child: Text('Set ringtone'),
), ),
@ -126,7 +215,6 @@ class ContactModal extends StatelessWidget {
), ),
], ],
), ),
// Contact Profile // Contact Profile
Padding( Padding(
padding: const EdgeInsets.all(16.0), padding: const EdgeInsets.all(16.0),
@ -134,88 +222,114 @@ class ContactModal extends StatelessWidget {
children: [ children: [
CircleAvatar( CircleAvatar(
radius: 50, radius: 50,
backgroundImage: (contact.thumbnail != null && backgroundImage: (widget.contact.thumbnail != null &&
contact.thumbnail!.isNotEmpty) widget.contact.thumbnail!.isNotEmpty)
? MemoryImage(contact.thumbnail!) ? MemoryImage(widget.contact.thumbnail!)
: null, : null,
backgroundColor: backgroundColor:
generateColorFromName(contact.displayName), generateColorFromName(widget.contact.displayName),
child: (contact.thumbnail == null || child: (widget.contact.thumbnail == null ||
contact.thumbnail!.isEmpty) widget.contact.thumbnail!.isEmpty)
? Text( ? Text(
contact.displayName.isNotEmpty widget.contact.displayName.isNotEmpty
? contact.displayName[0].toUpperCase() ? widget.contact.displayName[0]
.toUpperCase()
: '?', : '?',
style: TextStyle( style: const TextStyle(
fontSize: 40, color: Colors.white), fontSize: 40, color: Colors.white),
) )
: null, : null,
), ),
SizedBox(height: 10), const SizedBox(height: 10),
Text( Text(
contact.displayName, widget.contact.displayName,
style: TextStyle( style: const TextStyle(
fontSize: 24, fontWeight: FontWeight.bold), fontSize: 24, fontWeight: FontWeight.bold),
), ),
], ],
), ),
), ),
const Divider(),
// Contact Actions // Contact Actions
Divider(),
ListTile( ListTile(
leading: Icon(Icons.phone, color: Colors.green), leading: const Icon(Icons.phone, color: Colors.green),
title: Text(phoneNumber), title: Text(phoneNumber),
onTap: () { onTap: () {
if (contact.phones.isNotEmpty) { if (widget.contact.phones.isNotEmpty) {
_launchPhoneDialer(phoneNumber); _launchPhoneDialer(phoneNumber);
} }
}, },
), ),
ListTile( ListTile(
leading: Icon(Icons.message, color: Colors.blue), leading: const Icon(Icons.message, color: Colors.blue),
title: Text(phoneNumber), title: Text(phoneNumber),
onTap: () { onTap: () {
if (contact.phones.isNotEmpty) { if (widget.contact.phones.isNotEmpty) {
_launchSms(phoneNumber); _launchSms(phoneNumber);
} }
}, },
), ),
ListTile( ListTile(
leading: Icon(Icons.email, color: Colors.orange), leading: const Icon(Icons.email, color: Colors.orange),
title: Text(email), title: Text(email),
onTap: () { onTap: () {
if (contact.emails.isNotEmpty) { if (widget.contact.emails.isNotEmpty) {
_launchEmail(email); _launchEmail(email);
} }
}, },
), ),
Divider(), const Divider(),
// Favorite and Edit Buttons // Favorite, Edit, and Block/Unblock Buttons
Padding( Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0), padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Row( child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
ElevatedButton.icon( // Favorite button
onPressed: () { SizedBox(
Navigator.of(context).pop(); width: double
onToggleFavorite(); .infinity, // This makes the button take full width
}, child: ElevatedButton.icon(
icon: Icon(contact.isStarred onPressed: () {
? Icons.star Navigator.of(context).pop();
: Icons.star_border), widget.onToggleFavorite();
label: Text( },
contact.isStarred ? 'Unfavorite' : 'Favorite'), icon: Icon(widget.isFavorite
? Icons.star
: Icons.star_border),
label: Text(
widget.isFavorite ? 'Unfavorite' : 'Favorite'),
),
), ),
ElevatedButton.icon( const SizedBox(height: 10), // Space between buttons
onPressed: () => onEdit(),
icon: Icon(Icons.edit), // Edit button
label: Text('Edit Contact'), SizedBox(
width: double
.infinity, // This makes the button take full width
child: ElevatedButton.icon(
onPressed: () => widget.onEdit(),
icon: const Icon(Icons.edit),
label: const Text('Edit Contact'),
),
),
const SizedBox(height: 10), // Space between buttons
// Block/Unblock button
SizedBox(
width: double
.infinity, // This makes the button take full width
child: ElevatedButton.icon(
onPressed: _toggleBlockState,
icon: Icon(
isBlocked ? Icons.block : Icons.block_flipped),
label: Text(isBlocked ? 'Unblock' : 'Block'),
),
), ),
], ],
), ),
), ),
SizedBox(height: 16),
const SizedBox(height: 16),
], ],
), ),
), ),

View File

@ -1,14 +1,15 @@
// history_page.dart
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_contacts/flutter_contacts.dart'; import 'package:flutter_contacts/flutter_contacts.dart';
import 'package:intl/intl.dart'; // For date formatting import 'package:intl/intl.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:dialer/features/contacts/contact_state.dart'; import 'package:dialer/features/contacts/contact_state.dart';
import 'package:dialer/widgets/username_color_generator.dart';
import 'package:dialer/widgets/color_darkener.dart';
class History { class History {
final Contact contact; final Contact contact;
final DateTime date; final DateTime date;
final String callType; // 'incoming' or 'outgoing' final String callType; // 'incoming' or 'outgoing'
final String callStatus; // 'missed' or 'answered' final String callStatus; // 'missed' or 'answered'
final int attempts; final int attempts;
@ -28,9 +29,10 @@ class HistoryPage extends StatefulWidget {
_HistoryPageState createState() => _HistoryPageState(); _HistoryPageState createState() => _HistoryPageState();
} }
class _HistoryPageState extends State<HistoryPage> { class _HistoryPageState extends State<HistoryPage> with SingleTickerProviderStateMixin {
List<History> histories = []; List<History> histories = [];
bool loading = true; bool loading = true;
int? _expandedIndex;
@override @override
void didChangeDependencies() { void didChangeDependencies() {
@ -51,7 +53,6 @@ class _HistoryPageState extends State<HistoryPage> {
} }
List<Contact> contacts = contactState.contacts; List<Contact> contacts = contactState.contacts;
// Ensure there are enough contacts
if (contacts.isEmpty) { if (contacts.isEmpty) {
setState(() { setState(() {
loading = false; loading = false;
@ -59,7 +60,6 @@ class _HistoryPageState extends State<HistoryPage> {
return; return;
} }
// Build histories using the contacts
setState(() { setState(() {
histories = List.generate( histories = List.generate(
contacts.length >= 10 ? 10 : contacts.length, contacts.length >= 10 ? 10 : contacts.length,
@ -75,6 +75,47 @@ class _HistoryPageState extends State<HistoryPage> {
}); });
} }
List _buildGroupedList(List<History> historyList) {
// Sort histories by date (most recent first)
historyList.sort((a, b) => b.date.compareTo(a.date));
final now = DateTime.now();
final today = DateTime(now.year, now.month, now.day);
final yesterday = today.subtract(const Duration(days: 1));
List<History> todayHistories = [];
List<History> yesterdayHistories = [];
List<History> olderHistories = [];
for (var history in historyList) {
final callDate = DateTime(history.date.year, history.date.month, history.date.day);
if (callDate == today) {
todayHistories.add(history);
} else if (callDate == yesterday) {
yesterdayHistories.add(history);
} else {
olderHistories.add(history);
}
}
// Combine them with headers
final items = <dynamic>[];
if (todayHistories.isNotEmpty) {
items.add('Today');
items.addAll(todayHistories);
}
if (yesterdayHistories.isNotEmpty) {
items.add('Yesterday');
items.addAll(yesterdayHistories);
}
if (olderHistories.isNotEmpty) {
items.add('Older');
items.addAll(olderHistories);
}
return items;
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final contactState = ContactState.of(context); final contactState = ContactState.of(context);
@ -82,9 +123,6 @@ class _HistoryPageState extends State<HistoryPage> {
if (loading || contactState.loading) { if (loading || contactState.loading) {
return Scaffold( return Scaffold(
backgroundColor: Colors.black, backgroundColor: Colors.black,
appBar: AppBar(
title: const Text('History'),
),
body: const Center( body: const Center(
child: CircularProgressIndicator(), child: CircularProgressIndicator(),
), ),
@ -94,9 +132,6 @@ class _HistoryPageState extends State<HistoryPage> {
if (histories.isEmpty) { if (histories.isEmpty) {
return Scaffold( return Scaffold(
backgroundColor: Colors.black, backgroundColor: Colors.black,
appBar: AppBar(
title: const Text('History'),
),
body: const Center( body: const Center(
child: Text( child: Text(
'No call history available.', 'No call history available.',
@ -106,46 +141,298 @@ class _HistoryPageState extends State<HistoryPage> {
); );
} }
// Filter missed calls
List<History> missedCalls = histories.where((h) => h.callStatus == 'missed').toList();
final allItems = _buildGroupedList(histories);
final missedItems = _buildGroupedList(missedCalls);
return DefaultTabController(
length: 2,
child: Scaffold(
backgroundColor: Colors.black,
appBar: PreferredSize(
preferredSize: const Size.fromHeight(kToolbarHeight),
child: Container(
color: Colors.black,
child: const TabBar(
tabs: [
Tab(text: 'All Calls'),
Tab(text: 'Missed Calls'),
],
indicatorColor: Colors.white,
),
),
),
body: TabBarView(
children: [
// All Calls
_buildListView(allItems),
// Missed Calls
_buildListView(missedItems),
],
),
),
);
}
Widget _buildListView(List items) {
return ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) {
final item = items[index];
if (item is String) {
// This is a header item
return Container(
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16),
color: Colors.grey[900],
child: Text(
item,
style: const TextStyle(
color: Colors.white70,
fontWeight: FontWeight.bold,
),
),
);
} else if (item is History) {
final history = item;
final contact = history.contact;
final isExpanded = _expandedIndex == index;
// Generate the avatar color
Color avatarColor = generateColorFromName(contact.displayName);
return Column(
children: [
ListTile(
leading: (contact.thumbnail != null && contact.thumbnail!.isNotEmpty)
? CircleAvatar(
backgroundImage: MemoryImage(contact.thumbnail!),
)
: CircleAvatar(
backgroundColor: avatarColor,
child: Text(
contact.displayName.isNotEmpty
? contact.displayName[0].toUpperCase()
: '?',
style: TextStyle(color: darken(avatarColor, 0.4)),
),
),
title: Text(
contact.displayName,
style: const TextStyle(color: Colors.white),
),
subtitle: Text(
'${history.callType} - ${history.callStatus} - ${DateFormat('MMM dd, hh:mm a').format(history.date)}',
style: const TextStyle(color: Colors.grey),
),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
'${history.attempts}x',
style: const TextStyle(color: Colors.white),
),
IconButton(
icon: const Icon(Icons.phone, color: Colors.green),
onPressed: () async {
if (contact.phones.isNotEmpty) {
final Uri callUri =
Uri(scheme: 'tel', path: contact.phones.first.number);
if (await canLaunchUrl(callUri)) {
await launchUrl(callUri);
} else {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Could not launch call')),
);
}
} else {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Contact has no phone number')),
);
}
},
),
],
),
onTap: () {
setState(() {
_expandedIndex = isExpanded ? null : index;
});
},
),
if (isExpanded)
Container(
color: Colors.grey[850],
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
TextButton.icon(
onPressed: () async {
if (history.contact.phones.isNotEmpty) {
final Uri smsUri =
Uri(scheme: 'sms', path: history.contact.phones.first.number);
if (await canLaunchUrl(smsUri)) {
await launchUrl(smsUri);
} else {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Could not send message')),
);
}
} else {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Contact has no phone number')),
);
}
},
icon: const Icon(Icons.message, color: Colors.white),
label: const Text('Message', style: TextStyle(color: Colors.white)),
),
TextButton.icon(
onPressed: () {
// Navigate to Call Details page
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => CallDetailsPage(history: history),
),
);
},
icon: const Icon(Icons.info, color: Colors.white),
label: const Text('Details', style: TextStyle(color: Colors.white)),
),
TextButton.icon(
onPressed: () {
// Implement block number functionality
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Number blocked (functionality not implemented)'),
),
);
},
icon: const Icon(Icons.block, color: Colors.white),
label: const Text('Block', style: TextStyle(color: Colors.white)),
),
],
),
),
],
);
}
return const SizedBox.shrink();
},
);
}
}
class CallDetailsPage extends StatelessWidget {
final History history;
const CallDetailsPage({Key? key, required this.history}) : super(key: key);
@override
Widget build(BuildContext context) {
final contact = history.contact;
return Scaffold( return Scaffold(
backgroundColor: Colors.black, backgroundColor: Colors.black,
appBar: AppBar( appBar: AppBar(
title: const Text('History'), title: const Text('Call Details'),
backgroundColor: Colors.black,
), ),
body: ListView.builder( body: Padding(
itemCount: histories.length, padding: const EdgeInsets.all(16.0),
itemBuilder: (context, index) { child: Column(
final history = histories[index]; children: [
final contact = history.contact; // Display Contact Name and Thumbnail
Row(
children: [
(contact.thumbnail != null && contact.thumbnail!.isNotEmpty)
? CircleAvatar(
backgroundImage: MemoryImage(contact.thumbnail!),
radius: 30,
)
: CircleAvatar(
backgroundColor: Colors.grey[700],
radius: 30,
child: Text(
contact.displayName.isNotEmpty
? contact.displayName[0].toUpperCase()
: '?',
style: const TextStyle(color: Colors.white),
),
),
const SizedBox(width: 16),
Expanded(
child: Text(
contact.displayName,
style: const TextStyle(color: Colors.white, fontSize: 24),
),
),
],
),
const SizedBox(height: 24),
return ListTile( // Display call type, status, date, attempts
leading: (contact.thumbnail != null && contact.thumbnail!.isNotEmpty) DetailRow(
? CircleAvatar( label: 'Call Type:',
backgroundImage: MemoryImage(contact.thumbnail!), value: history.callType,
) ),
: CircleAvatar( DetailRow(
child: Text( label: 'Call Status:',
contact.displayName.isNotEmpty value: history.callStatus,
? contact.displayName[0] ),
: '?', DetailRow(
label: 'Date:',
value: DateFormat('MMM dd, yyyy - hh:mm a').format(history.date),
),
DetailRow(
label: 'Attempts:',
value: '${history.attempts}',
),
const SizedBox(height: 24),
// If you have more details like duration, contact number, etc.
if (contact.phones.isNotEmpty)
DetailRow(
label: 'Number:',
value: contact.phones.first.number,
), ),
), ],
title: Text( ),
contact.displayName, ),
style: const TextStyle(color: Colors.white), );
), }
subtitle: Text( }
'${history.callType} - ${history.callStatus} - ${DateFormat('MMM dd, hh:mm a').format(history.date)}',
style: const TextStyle(color: Colors.grey), class DetailRow extends StatelessWidget {
), final String label;
trailing: Text( final String value;
'${history.attempts}x',
style: const TextStyle(color: Colors.white), const DetailRow({Key? key, required this.label, required this.value}) : super(key: key);
),
onTap: () { @override
// Handle tap event if needed Widget build(BuildContext context) {
}, return Padding(
); padding: const EdgeInsets.symmetric(vertical: 8.0),
}, child: Row(
children: [
Text(
label,
style: const TextStyle(color: Colors.white70, fontWeight: FontWeight.bold),
),
const SizedBox(width: 8),
Expanded(
child: Text(
value,
style: const TextStyle(color: Colors.white),
textAlign: TextAlign.right,
),
),
],
), ),
); );
} }

View File

@ -18,7 +18,8 @@ class _MyHomePageState extends State<MyHomePage>
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_tabController = TabController(length: 4, vsync: this, initialIndex: 1); // Set the TabController length to 3
_tabController = TabController(length: 3, vsync: this, initialIndex: 1);
_tabController.addListener(_handleTabIndex); _tabController.addListener(_handleTabIndex);
_fetchContacts(); _fetchContacts();
} }
@ -69,68 +70,97 @@ class _MyHomePageState extends State<MyHomePage>
left: 16.0, left: 16.0,
right: 16.0, right: 16.0,
), ),
child: Container( child: Row(
decoration: BoxDecoration( children: [
color: const Color.fromARGB(255, 30, 30, 30), Expanded(
borderRadius: BorderRadius.circular(12.0), child: Container(
border: Border( decoration: BoxDecoration(
top: BorderSide(color: Colors.grey.shade800, width: 1), color: const Color.fromARGB(255, 30, 30, 30),
left: BorderSide(color: Colors.grey.shade800, width: 1), borderRadius: BorderRadius.circular(12.0),
right: BorderSide(color: Colors.grey.shade800, width: 1), border: Border(
bottom: BorderSide(color: Colors.grey.shade800, width: 2), top: BorderSide(color: Colors.grey.shade800, width: 1),
), left: BorderSide(color: Colors.grey.shade800, width: 1),
), right: BorderSide(color: Colors.grey.shade800, width: 1),
child: SearchAnchor( bottom:
builder: (BuildContext context, SearchController controller) { BorderSide(color: Colors.grey.shade800, width: 2),
return SearchBar(
controller: controller,
padding: WidgetStateProperty.all<EdgeInsetsGeometry>(
const EdgeInsets.only(
top: 6.0,
bottom: 6.0,
left: 16.0,
right: 16.0,
), ),
), ),
onTap: () { child: SearchAnchor(
controller.openView(); builder:
_onSearchChanged(''); (BuildContext context, SearchController controller) {
}, return SearchBar(
backgroundColor: WidgetStateProperty.all( controller: controller,
const Color.fromARGB(255, 30, 30, 30)), padding:
hintText: 'Search contacts', MaterialStateProperty.all<EdgeInsetsGeometry>(
hintStyle: WidgetStateProperty.all( const EdgeInsets.only(
const TextStyle(color: Colors.grey, fontSize: 16.0), top: 6.0,
), bottom: 6.0,
leading: const Icon( left: 16.0,
Icons.search, right: 16.0,
color: Colors.grey, ),
size: 24.0, ),
), onTap: () {
shape: WidgetStateProperty.all<RoundedRectangleBorder>( controller.openView();
RoundedRectangleBorder( _onSearchChanged('');
borderRadius: BorderRadius.circular(12.0), },
), backgroundColor: MaterialStateProperty.all(
), const Color.fromARGB(255, 30, 30, 30)),
); hintText: 'Search contacts',
}, hintStyle: MaterialStateProperty.all(
viewOnChanged: (query) { const TextStyle(color: Colors.grey, fontSize: 16.0),
_onSearchChanged(query); ),
}, leading: const Icon(
suggestionsBuilder: Icons.search,
(BuildContext context, SearchController controller) { color: Colors.grey,
return _contactSuggestions.map((contact) { size: 24.0,
return ListTile( ),
key: ValueKey(contact.id), shape:
title: Text(contact.displayName, MaterialStateProperty.all<RoundedRectangleBorder>(
style: const TextStyle(color: Colors.white)), RoundedRectangleBorder(
onTap: () { borderRadius: BorderRadius.circular(12.0),
controller.closeView(contact.displayName); ),
),
);
}, },
); viewOnChanged: (query) {
}).toList(); _onSearchChanged(query);
}, },
), suggestionsBuilder:
(BuildContext context, SearchController controller) {
return _contactSuggestions.map((contact) {
return ListTile(
key: ValueKey(contact.id),
title: Text(contact.displayName,
style: const TextStyle(color: Colors.white)),
onTap: () {
controller.closeView(contact.displayName);
},
);
}).toList();
},
),
),
),
// 3-dot menu
PopupMenuButton<String>(
icon: const Icon(Icons.more_vert, color: Colors.white),
itemBuilder: (BuildContext context) => [
const PopupMenuItem<String>(
value: 'settings',
child: Text('Settings'),
),
],
onSelected: (String value) {
if (value == 'settings') {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const SettingsPage()),
);
}
},
),
],
), ),
), ),
// Main content with TabBarView // Main content with TabBarView
@ -143,7 +173,6 @@ class _MyHomePageState extends State<MyHomePage>
FavoritesPage(), FavoritesPage(),
HistoryPage(), HistoryPage(),
ContactPage(), ContactPage(),
SettingsPage(), // Add your SettingsPage here
], ],
), ),
Positioned( Positioned(
@ -187,10 +216,6 @@ class _MyHomePageState extends State<MyHomePage>
icon: Icon(_tabController.index == 2 icon: Icon(_tabController.index == 2
? Icons.contacts ? Icons.contacts
: Icons.contacts_outlined)), : Icons.contacts_outlined)),
Tab(
icon: Icon(_tabController.index == 3 // Corrected index
? Icons.settings
: Icons.settings_outlined)),
], ],
labelColor: Colors.white, labelColor: Colors.white,
unselectedLabelColor: const Color.fromARGB(255, 158, 158, 158), unselectedLabelColor: const Color.fromARGB(255, 158, 158, 158),

View File

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

View File

@ -1,20 +1,19 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'key_storage.dart';
class DeleteKeyPairPage extends StatelessWidget { class DeleteKeyPairPage extends StatelessWidget {
const DeleteKeyPairPage({super.key}); const DeleteKeyPairPage({super.key});
void _deleteKeyPair(BuildContext context) { Future<void> _deleteKeyPair(BuildContext context) async {
// Key deletion logic (not implemented here) final keyStorage = KeyStorage();
// ... await keyStorage.deleteKeys();
// Show confirmation message
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
const SnackBar( const SnackBar(
content: Text('The key pair has been deleted.'), content: Text('The key pair has been deleted.'),
), ),
); );
// Navigate back or update the UI as needed
Navigator.pop(context); Navigator.pop(context);
} }

View File

@ -3,6 +3,8 @@ import 'dart:typed_data';
import 'dart:convert'; import 'dart:convert';
import 'package:pointycastle/export.dart' as crypto; import 'package:pointycastle/export.dart' as crypto;
import 'package:file_picker/file_picker.dart'; import 'package:file_picker/file_picker.dart';
import 'dart:io';
import 'key_storage.dart';
class ExportPrivateKeyPage extends StatefulWidget { class ExportPrivateKeyPage extends StatefulWidget {
const ExportPrivateKeyPage({super.key}); const ExportPrivateKeyPage({super.key});
@ -15,55 +17,81 @@ class _ExportPrivateKeyPageState extends State<ExportPrivateKeyPage> {
final TextEditingController _passwordController = TextEditingController(); final TextEditingController _passwordController = TextEditingController();
Future<void> _exportPrivateKey() async { Future<void> _exportPrivateKey() async {
// Replace with your actual private key retrieval logic final keyStorage = KeyStorage();
final String privateKeyPem = 'Your private key here'; final privateKeyPem = await keyStorage.getPrivateKey();
// Get the password from the user input if (privateKeyPem == null) {
final password = _passwordController.text; // Show error message if there's no key
if (password.isEmpty) { ScaffoldMessenger.of(context).showSnackBar(
// Show error message const SnackBar(
content: Text('No private key found to export.'),
),
);
return;
}
final password = _passwordController.text;
if (password.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Please enter a password.'),
),
);
return; return;
} }
// Encrypt the private key using AES-256
final encryptedData = _encryptPrivateKey(privateKeyPem, password); final encryptedData = _encryptPrivateKey(privateKeyPem, password);
// Let the user pick a file location
final outputFile = await FilePicker.platform.saveFile( final outputFile = await FilePicker.platform.saveFile(
dialogTitle: 'Save encrypted private key', dialogTitle: 'Save encrypted private key',
fileName: 'private_key_encrypted.aes', fileName: 'private_key_encrypted.aes',
); );
if (outputFile != null) { if (outputFile != null) {
// Write the encrypted data to the file try {
// Use appropriate file I/O methods (not shown here) final file = File(outputFile);
// ... await file.writeAsBytes(encryptedData);
// Show a confirmation dialog or message
showDialog( showDialog(
context: context, context: context,
builder: (context) => AlertDialog( builder: (context) => AlertDialog(
title: const Text('Key Exported'), title: const Text('Key Exported'),
content: const Text('The encrypted private key has been exported successfully.'), content: const Text('The encrypted private key has been exported successfully.'),
actions: [ actions: [
TextButton( TextButton(
onPressed: () => Navigator.pop(context), onPressed: () => Navigator.pop(context),
child: const Text('OK'), child: const Text('OK'),
), ),
], ],
), ),
); );
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Failed to write file: $e'),
),
);
}
} }
} }
Uint8List _encryptPrivateKey(String privateKey, String password) { Uint8List _encryptPrivateKey(String privateKey, String password) {
// Encryption logic using AES-256 // Derive a key from the password using PBKDF2
final key = crypto.PBKDF2KeyDerivator(crypto.HMac(crypto.SHA256Digest(), 64)) final derivator = crypto.PBKDF2KeyDerivator(
.process(Uint8List.fromList(utf8.encode(password))); crypto.HMac(crypto.SHA256Digest(), 64),
);
final params = crypto.PaddedBlockCipherParameters( final salt = Uint8List.fromList(utf8.encode('some_salt')); // In production, use a random salt and store it securely
crypto.ParametersWithIV(crypto.KeyParameter(key), Uint8List(16)), // Initialization Vector derivator.init(crypto.Pbkdf2Parameters(salt, 1000, 32));
final key = derivator.process(Uint8List.fromList(utf8.encode(password)));
// Initialize AES-CBC cipher with PKCS7 padding
final iv = Uint8List(16); // zero IV for example, in production use random IV and store it
final params = crypto.PaddedBlockCipherParameters<crypto.ParametersWithIV<crypto.KeyParameter>, Null>(
crypto.ParametersWithIV<crypto.KeyParameter>(crypto.KeyParameter(key), iv),
null, null,
); );
final cipher = crypto.PaddedBlockCipher('AES/CBC/PKCS7'); final cipher = crypto.PaddedBlockCipher('AES/CBC/PKCS7');
cipher.init(true, params); cipher.init(true, params);

View File

@ -4,12 +4,12 @@ import 'dart:math';
import 'dart:convert'; import 'dart:convert';
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:asn1lib/asn1lib.dart'; import 'package:asn1lib/asn1lib.dart';
import 'key_storage.dart';
class GenerateNewKeyPairPage extends StatelessWidget { class GenerateNewKeyPairPage extends StatelessWidget {
const GenerateNewKeyPairPage({super.key}); const GenerateNewKeyPairPage({super.key});
Future<Map<String, String>> _generateKeyPair() async { Future<Map<String, String>> _generateKeyPair() async {
// Key generation logic using pointycastle
final keyParams = crypto.RSAKeyGeneratorParameters( final keyParams = crypto.RSAKeyGeneratorParameters(
BigInt.parse('65537'), BigInt.parse('65537'),
2048, 2048,
@ -17,8 +17,6 @@ class GenerateNewKeyPairPage extends StatelessWidget {
); );
final secureRandom = crypto.FortunaRandom(); final secureRandom = crypto.FortunaRandom();
// Seed the random number generator
final random = Random.secure(); final random = Random.secure();
final seeds = List<int>.generate(32, (_) => random.nextInt(256)); final seeds = List<int>.generate(32, (_) => random.nextInt(256));
secureRandom.seed(crypto.KeyParameter(Uint8List.fromList(seeds))); secureRandom.seed(crypto.KeyParameter(Uint8List.fromList(seeds)));
@ -31,11 +29,12 @@ class GenerateNewKeyPairPage extends StatelessWidget {
final publicKey = pair.publicKey as crypto.RSAPublicKey; final publicKey = pair.publicKey as crypto.RSAPublicKey;
final privateKey = pair.privateKey as crypto.RSAPrivateKey; final privateKey = pair.privateKey as crypto.RSAPrivateKey;
// Convert keys to PEM format
final publicKeyPem = _encodePublicKeyToPemPKCS1(publicKey); final publicKeyPem = _encodePublicKeyToPemPKCS1(publicKey);
final privateKeyPem = _encodePrivateKeyToPemPKCS1(privateKey); final privateKeyPem = _encodePrivateKeyToPemPKCS1(privateKey);
// Save keys securely (not implemented here) // Save keys securely
final keyStorage = KeyStorage();
await keyStorage.saveKeys(publicKey: publicKeyPem, privateKey: privateKeyPem);
return {'publicKey': publicKeyPem, 'privateKey': privateKeyPem}; return {'publicKey': publicKeyPem, 'privateKey': privateKeyPem};
} }
@ -52,7 +51,8 @@ class GenerateNewKeyPairPage extends StatelessWidget {
Uint8List _encodePublicKeyToDer(crypto.RSAPublicKey publicKey) { Uint8List _encodePublicKeyToDer(crypto.RSAPublicKey publicKey) {
final algorithmSeq = ASN1Sequence(); final algorithmSeq = ASN1Sequence();
algorithmSeq.add(ASN1ObjectIdentifier.fromName('rsaEncryption')); // Create the OID directly with the arcs
algorithmSeq.add(ASN1ObjectIdentifier([1, 2, 840, 113549, 1, 1, 1]));
algorithmSeq.add(ASN1Null()); algorithmSeq.add(ASN1Null());
final publicKeySeq = ASN1Sequence(); final publicKeySeq = ASN1Sequence();
@ -84,8 +84,8 @@ class GenerateNewKeyPairPage extends StatelessWidget {
} }
String _formatPem(Uint8List bytes, String label) { String _formatPem(Uint8List bytes, String label) {
final base64 = base64Encode(bytes); final base64Data = base64Encode(bytes);
final chunks = RegExp('.{1,64}').allMatches(base64).map((m) => m.group(0)!); final chunks = RegExp('.{1,64}').allMatches(base64Data).map((m) => m.group(0)!);
return '-----BEGIN $label-----\n${chunks.join('\n')}\n-----END $label-----'; return '-----BEGIN $label-----\n${chunks.join('\n')}\n-----END $label-----';
} }
@ -99,13 +99,12 @@ class GenerateNewKeyPairPage extends StatelessWidget {
body: Center( body: Center(
child: ElevatedButton( child: ElevatedButton(
onPressed: () async { onPressed: () async {
final keys = await _generateKeyPair(); await _generateKeyPair();
// Display a confirmation dialog or message
showDialog( showDialog(
context: context, context: context,
builder: (context) => AlertDialog( builder: (context) => AlertDialog(
title: const Text('Keys Generated'), title: const Text('Keys Generated'),
content: const Text('The new key pair has been generated successfully.'), content: const Text('The new key pair has been generated and stored securely.'),
actions: [ actions: [
TextButton( TextButton(
onPressed: () => Navigator.pop(context), onPressed: () => Navigator.pop(context),

View File

@ -0,0 +1,28 @@
// key_storage.dart
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
class KeyStorage {
static const _publicKeyKey = 'public_key';
static const _privateKeyKey = 'private_key';
final FlutterSecureStorage _storage = const FlutterSecureStorage();
Future<void> saveKeys({required String publicKey, required String privateKey}) async {
await _storage.write(key: _publicKeyKey, value: publicKey);
await _storage.write(key: _privateKeyKey, value: privateKey);
}
Future<String?> getPublicKey() async {
return await _storage.read(key: _publicKeyKey);
}
Future<String?> getPrivateKey() async {
return await _storage.read(key: _privateKeyKey);
}
Future<void> deleteKeys() async {
await _storage.delete(key: _publicKeyKey);
await _storage.delete(key: _privateKeyKey);
}
}

View File

@ -1,9 +1,9 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:dialer/features/settings/key/show_public_key_qr.dart'; import 'show_public_key_qr.dart';
import 'package:dialer/features/settings/key/show_public_key_text.dart'; import 'show_public_key_text.dart';
import 'package:dialer/features/settings/key/generate_new_key_pair.dart'; import 'generate_new_key_pair.dart';
import 'package:dialer/features/settings/key/export_private_key.dart'; import 'export_private_key.dart';
import 'package:dialer/features/settings/key/delete_key_pair.dart'; import 'delete_key_pair.dart';
class KeyManagementPage extends StatelessWidget { class KeyManagementPage extends StatelessWidget {
const KeyManagementPage({super.key}); const KeyManagementPage({super.key});
@ -34,14 +34,13 @@ class KeyManagementPage extends StatelessWidget {
MaterialPageRoute(builder: (context) => const ExportPrivateKeyPage()), MaterialPageRoute(builder: (context) => const ExportPrivateKeyPage()),
); );
break; break;
case 'Delete a key pair, warning POPUP': case 'Delete a key pair':
Navigator.push( Navigator.push(
context, context,
MaterialPageRoute(builder: (context) => const DeleteKeyPairPage()), MaterialPageRoute(builder: (context) => const DeleteKeyPairPage()),
); );
break; break;
default: default:
// Handle default or unknown options
break; break;
} }
} }
@ -53,7 +52,7 @@ class KeyManagementPage extends StatelessWidget {
'Display public key as QR code', 'Display public key as QR code',
'Generate a new key pair', 'Generate a new key pair',
'Export private key to password-encrypted file (AES 256)', 'Export private key to password-encrypted file (AES 256)',
'Delete a key pair, warning POPUP', 'Delete a key pair',
]; ];
return Scaffold( return Scaffold(

View File

@ -1,26 +1,48 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:pretty_qr_code/pretty_qr_code.dart'; import 'package:pretty_qr_code/pretty_qr_code.dart';
import 'key_storage.dart';
class DisplayPublicKeyQRCodePage extends StatelessWidget { class DisplayPublicKeyQRCodePage extends StatelessWidget {
const DisplayPublicKeyQRCodePage({super.key}); const DisplayPublicKeyQRCodePage({super.key});
Future<String?> _loadPublicKey() async {
final keyStorage = KeyStorage();
return keyStorage.getPublicKey();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
// Replace with your actual public key retrieval logic
final String publicKey = 'Your public key here';
return Scaffold( return Scaffold(
backgroundColor: Colors.black, backgroundColor: Colors.black,
appBar: AppBar( appBar: AppBar(
title: const Text('Public Key in QR Code'), title: const Text('Public Key in QR Code'),
), ),
body: Center( body: FutureBuilder<String?>(
child: PrettyQr( future: _loadPublicKey(),
data: publicKey, builder: (context, snapshot) {
size: 250, if (snapshot.connectionState == ConnectionState.waiting) {
roundEdges: true, return const Center(child: CircularProgressIndicator());
elementColor: Colors.white, }
),
final publicKey = snapshot.data;
if (publicKey == null) {
return const Center(
child: Text(
'No public key found.',
style: TextStyle(color: Colors.white),
),
);
}
return Center(
child: PrettyQr(
data: publicKey,
size: 250,
roundEdges: true,
elementColor: Colors.white,
),
);
},
), ),
); );
} }

View File

@ -1,27 +1,49 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'key_storage.dart';
class DisplayPublicKeyTextPage extends StatelessWidget { class DisplayPublicKeyTextPage extends StatelessWidget {
const DisplayPublicKeyTextPage({super.key}); const DisplayPublicKeyTextPage({super.key});
Future<String?> _loadPublicKey() async {
final keyStorage = KeyStorage();
return await keyStorage.getPublicKey();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
// Replace with your actual public key retrieval logic
final String publicKey = 'Your public key here';
return Scaffold( return Scaffold(
backgroundColor: Colors.black, backgroundColor: Colors.black,
appBar: AppBar( appBar: AppBar(
title: const Text('Public Key as Text'), title: const Text('Public Key as Text'),
), ),
body: Center( body: FutureBuilder<String?>(
child: Padding( future: _loadPublicKey(),
padding: const EdgeInsets.all(16.0), builder: (context, snapshot) {
child: SelectableText( if (snapshot.connectionState == ConnectionState.waiting) {
publicKey, return const Center(child: CircularProgressIndicator());
style: const TextStyle(color: Colors.white), }
textAlign: TextAlign.center,
), final publicKey = snapshot.data;
), if (publicKey == null) {
return const Center(
child: Text(
'No public key found.',
style: TextStyle(color: Colors.white),
),
);
}
return Center(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: SelectableText(
publicKey,
style: const TextStyle(color: Colors.white),
textAlign: TextAlign.center,
),
),
);
},
), ),
); );
} }

View File

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

View File

@ -0,0 +1,52 @@
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<void> blockNumber(String number) async {
if (number.isEmpty) return;
final prefs = await SharedPreferences.getInstance();
List<String> 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<void> unblockNumber(String number) async {
if (number.isEmpty) return;
final prefs = await SharedPreferences.getInstance();
List<String> 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<bool> isNumberBlocked(String number) async {
if (number.isEmpty) return false;
final prefs = await SharedPreferences.getInstance();
List<String> blockedNumbers = prefs.getStringList('blockedNumbers') ?? [];
return blockedNumbers.contains(number);
}
}

View File

@ -34,7 +34,7 @@ dependencies:
# The following adds the Cupertino Icons font to your application. # The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons. # Use with the CupertinoIcons class for iOS style icons.
url_launcher: ^6.1.9 # To manage system dialer. Call, message... shared_preferences: ^2.3.3 # Local storage (no critical data)
cupertino_icons: ^1.0.8 cupertino_icons: ^1.0.8
flutter_contacts: ^1.1.9+2 flutter_contacts: ^1.1.9+2
permission_handler: ^11.3.1 # For handling permissions permission_handler: ^11.3.1 # For handling permissions
@ -45,9 +45,11 @@ dependencies:
mobile_scanner: ^6.0.2 mobile_scanner: ^6.0.2
pretty_qr_code: ^3.3.0 pretty_qr_code: ^3.3.0
pointycastle: ^3.4.0 pointycastle: ^3.4.0
file_picker: ^5.2.5 file_picker: ^8.1.6
asn1lib: ^1.0.0 asn1lib: ^1.0.0
intl_utils: ^2.0.7 intl_utils: ^2.0.7
url_launcher: ^6.3.1
flutter_secure_storage: ^9.0.0
mobile_number: mobile_number:
path: packages/mobile_number path: packages/mobile_number

5
dialer/run.sh Executable file
View File

@ -0,0 +1,5 @@
#!/bin/bash -e
IMG=git.gmoker.com/icing/flutter:main
docker run --rm -p 5037:5037 -v "$PWD:/app/" "$IMG" run