Compare commits

..

No commits in common. "d03884d00ee8265c1686f4abca017542f5eaf8f4" and "9177b5a42b3c3f91063d33b6ff56ecba17b81fdd" have entirely different histories.

41 changed files with 642 additions and 1030 deletions

View File

@ -11,12 +11,12 @@ android {
ndkVersion = flutter.ndkVersion ndkVersion = flutter.ndkVersion
compileOptions { compileOptions {
sourceCompatibility = JavaVersion.VERSION_17 sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_1_8
} }
kotlinOptions { kotlinOptions {
jvmTarget = JavaVersion.VERSION_17 jvmTarget = JavaVersion.VERSION_1_8
} }
defaultConfig { defaultConfig {

View File

@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-all.zip distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-all.zip

View File

@ -18,8 +18,8 @@ pluginManagement {
plugins { plugins {
id "dev.flutter.flutter-plugin-loader" version "1.0.0" id "dev.flutter.flutter-plugin-loader" version "1.0.0"
id "com.android.application" version "8.3.2" apply false id "com.android.application" version "8.1.0" apply false
id "org.jetbrains.kotlin.android" version "2.0.20" apply false id "org.jetbrains.kotlin.android" version "1.8.22" apply false
} }
include ":app" include ":app"

View File

@ -1,10 +1,7 @@
// lib/pages/composition_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:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
import '../../services/contact_service.dart'; import '../../widgets/contact_service.dart';
import '../../services/obfuscate_service.dart'; // Import ObfuscateService
import '../contacts/widgets/add_contact_button.dart'; import '../contacts/widgets/add_contact_button.dart';
class CompositionPage extends StatefulWidget { class CompositionPage extends StatefulWidget {
@ -20,9 +17,6 @@ class _CompositionPageState extends State<CompositionPage> {
List<Contact> _filteredContacts = []; List<Contact> _filteredContacts = [];
final ContactService _contactService = ContactService(); final ContactService _contactService = ContactService();
// Instantiate the ObfuscateService
final ObfuscateService _obfuscateService = ObfuscateService();
@override @override
void initState() { void initState() {
super.initState(); super.initState();
@ -118,12 +112,14 @@ class _CompositionPageState extends State<CompositionPage> {
: 'No phone number'; : 'No phone number';
return ListTile( return ListTile(
title: Text( title: Text(
_obfuscateService.obfuscateData(contact.displayName), contact.displayName,
style: const TextStyle(color: Colors.white), style:
const TextStyle(color: Colors.white),
), ),
subtitle: Text( subtitle: Text(
_obfuscateService.obfuscateData(phoneNumber), phoneNumber,
style: const TextStyle(color: Colors.grey), style:
const TextStyle(color: Colors.grey),
), ),
trailing: Row( trailing: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
@ -153,7 +149,12 @@ class _CompositionPageState extends State<CompositionPage> {
}, },
); );
}).toList() }).toList()
: [], : [
Center(
child: Text('No contacts found',
style:
TextStyle(color: Colors.white)))
],
), ),
), ),
], ],

View File

@ -1,6 +1,6 @@
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 '../../services/contact_service.dart'; import '../../widgets/contact_service.dart';
class ContactState extends StatefulWidget { class ContactState extends StatefulWidget {
final Widget child; final Widget child;

View File

@ -1,8 +1,8 @@
import 'package:dialer/services/obfuscate_service.dart';
import 'package:dialer/widgets/username_color_generator.dart'; import 'package:dialer/widgets/username_color_generator.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 '../contact_state.dart'; import '../contact_state.dart';
import '../../../widgets/color_darkener.dart';
import 'add_contact_button.dart'; import 'add_contact_button.dart';
import 'contact_modal.dart'; import 'contact_modal.dart';
import 'share_own_qr.dart'; import 'share_own_qr.dart';
@ -24,8 +24,6 @@ class AlphabetScrollPage extends StatefulWidget {
class _AlphabetScrollPageState extends State<AlphabetScrollPage> { class _AlphabetScrollPageState extends State<AlphabetScrollPage> {
late ScrollController _scrollController; late ScrollController _scrollController;
final ObfuscateService _obfuscateService = ObfuscateService();
@override @override
void initState() { void initState() {
super.initState(); super.initState();
@ -126,7 +124,7 @@ class _AlphabetScrollPageState extends State<AlphabetScrollPage> {
vertical: 8.0, horizontal: 16.0), vertical: 8.0, horizontal: 16.0),
child: Text( child: Text(
letter, letter,
style: const TextStyle( style: TextStyle(
fontSize: 28, fontSize: 28,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: Colors.white, color: Colors.white,
@ -136,25 +134,31 @@ class _AlphabetScrollPageState extends State<AlphabetScrollPage> {
// Contact Entries // Contact Entries
...contactsForLetter.map((contact) { ...contactsForLetter.map((contact) {
String phoneNumber = contact.phones.isNotEmpty String phoneNumber = contact.phones.isNotEmpty
? _obfuscateService.obfuscateData(contact.phones.first.number) ? contact.phones.first.number
: 'No phone number'; : 'No phone number';
Color avatarColor = Color avatarColor =
generateColorFromName(contact.displayName); generateColorFromName(contact.displayName);
return ListTile( return ListTile(
leading: ObfuscatedAvatar( leading: (contact.thumbnail != null &&
imageBytes: contact.thumbnail, contact.thumbnail!.isNotEmpty)
radius: 25, ? CircleAvatar(
backgroundImage:
MemoryImage(contact.thumbnail!),
)
: CircleAvatar(
backgroundColor: avatarColor, backgroundColor: avatarColor,
fallbackInitial: contact.displayName, child: Text(
contact.displayName.isNotEmpty
? contact.displayName[0].toUpperCase()
: '?',
style: TextStyle(
color: darken(avatarColor, 0.4)),
), ),
title: Text(
_obfuscateService.obfuscateData(contact.displayName),
style: const TextStyle(color: Colors.white),
),
subtitle: Text(
phoneNumber,
style: const TextStyle(color: Colors.white70),
), ),
title: Text(contact.displayName,
style: TextStyle(color: Colors.white)),
subtitle: Text(phoneNumber,
style: TextStyle(color: Colors.white70)),
onTap: () { onTap: () {
showModalBottomSheet( showModalBottomSheet(
context: context, context: context,

View File

@ -1,10 +1,8 @@
import 'package:dialer/services/obfuscate_service.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: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 '../../../services/block_service.dart'; import '../../../widgets/block_service.dart';
import '../../../services/contact_service.dart';
class ContactModal extends StatefulWidget { class ContactModal extends StatefulWidget {
final Contact contact; final Contact contact;
@ -27,7 +25,6 @@ class ContactModal extends StatefulWidget {
class _ContactModalState extends State<ContactModal> { class _ContactModalState extends State<ContactModal> {
late String phoneNumber; late String phoneNumber;
bool isBlocked = false; bool isBlocked = false;
final ObfuscateService _obfuscateService = ObfuscateService();
@override @override
void initState() { void initState() {
@ -102,8 +99,7 @@ class _ContactModalState extends State<ContactModal> {
context: context, context: context,
builder: (context) => AlertDialog( builder: (context) => AlertDialog(
title: const Text('Delete Contact'), title: const Text('Delete Contact'),
content: Text( content: Text('Are you sure you want to delete ${widget.contact.displayName}?'),
'Are you sure you want to delete ${_obfuscateService.obfuscateData(widget.contact.displayName)}?'),
actions: [ actions: [
TextButton( TextButton(
onPressed: () => Navigator.of(context).pop(false), onPressed: () => Navigator.of(context).pop(false),
@ -124,7 +120,7 @@ class _ContactModalState extends State<ContactModal> {
// Show success message // Show success message
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('${_obfuscateService.obfuscateData(widget.contact.displayName)} deleted')), SnackBar(content: Text('${widget.contact.displayName} deleted')),
); );
// Close the modal // Close the modal
@ -138,15 +134,10 @@ class _ContactModalState extends State<ContactModal> {
} }
} }
void _shareContactAsQRCode() {
// Use the ContactService to show the QR code for the contact's vCard
ContactService().showContactQRCodeDialog(context, widget.contact);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
String email = widget.contact.emails.isNotEmpty String email = widget.contact.emails.isNotEmpty
? _obfuscateService.obfuscateData(widget.contact.emails.first.address) ? widget.contact.emails.first.address
: 'No email'; : 'No email';
return GestureDetector( return GestureDetector(
@ -192,10 +183,7 @@ class _ContactModalState extends State<ContactModal> {
onSelected: (String choice) { onSelected: (String choice) {
if (choice == 'delete') { if (choice == 'delete') {
_deleteContact(); _deleteContact();
} else if (choice == 'share') {
_shareContactAsQRCode();
} }
// Handle other choices if needed
}, },
itemBuilder: (BuildContext context) { itemBuilder: (BuildContext context) {
return [ return [
@ -232,32 +220,40 @@ class _ContactModalState extends State<ContactModal> {
padding: const EdgeInsets.all(16.0), padding: const EdgeInsets.all(16.0),
child: Column( child: Column(
children: [ children: [
ObfuscatedAvatar( CircleAvatar(
imageBytes: widget.contact.thumbnail,
radius: 50, radius: 50,
backgroundImage: (widget.contact.thumbnail != null &&
widget.contact.thumbnail!.isNotEmpty)
? MemoryImage(widget.contact.thumbnail!)
: null,
backgroundColor: backgroundColor:
generateColorFromName(widget.contact.displayName), generateColorFromName(widget.contact.displayName),
fallbackInitial: widget.contact.displayName, child: (widget.contact.thumbnail == null ||
widget.contact.thumbnail!.isEmpty)
? Text(
widget.contact.displayName.isNotEmpty
? widget.contact.displayName[0]
.toUpperCase()
: '?',
style: const TextStyle(
fontSize: 40, color: Colors.white),
)
: null,
), ),
const SizedBox(height: 10), const SizedBox(height: 10),
Text( Text(
_obfuscateService.obfuscateData(widget.contact.displayName), widget.contact.displayName,
style: const TextStyle( style: const TextStyle(
fontSize: 24, fontSize: 24, fontWeight: FontWeight.bold),
fontWeight: FontWeight.bold,
color: Colors.white),
), ),
], ],
), ),
), ),
const Divider(color: Colors.grey), const Divider(),
// Contact Actions // Contact Actions
ListTile( ListTile(
leading: const Icon(Icons.phone, color: Colors.green), leading: const Icon(Icons.phone, color: Colors.green),
title: Text( title: Text(phoneNumber),
_obfuscateService.obfuscateData(phoneNumber),
style: const TextStyle(color: Colors.white),
),
onTap: () { onTap: () {
if (widget.contact.phones.isNotEmpty) { if (widget.contact.phones.isNotEmpty) {
_launchPhoneDialer(phoneNumber); _launchPhoneDialer(phoneNumber);
@ -266,10 +262,7 @@ class _ContactModalState extends State<ContactModal> {
), ),
ListTile( ListTile(
leading: const Icon(Icons.message, color: Colors.blue), leading: const Icon(Icons.message, color: Colors.blue),
title: Text( title: Text(phoneNumber),
_obfuscateService.obfuscateData(phoneNumber),
style: const TextStyle(color: Colors.white),
),
onTap: () { onTap: () {
if (widget.contact.phones.isNotEmpty) { if (widget.contact.phones.isNotEmpty) {
_launchSms(phoneNumber); _launchSms(phoneNumber);
@ -278,17 +271,14 @@ class _ContactModalState extends State<ContactModal> {
), ),
ListTile( ListTile(
leading: const Icon(Icons.email, color: Colors.orange), leading: const Icon(Icons.email, color: Colors.orange),
title: Text( title: Text(email),
email,
style: const TextStyle(color: Colors.white),
),
onTap: () { onTap: () {
if (widget.contact.emails.isNotEmpty) { if (widget.contact.emails.isNotEmpty) {
_launchEmail(email); _launchEmail(email);
} }
}, },
), ),
const Divider(color: Colors.grey), const Divider(),
// Favorite, Edit, and Block/Unblock Buttons // Favorite, Edit, and Block/Unblock Buttons
Padding( Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0), padding: const EdgeInsets.symmetric(horizontal: 16.0),
@ -296,7 +286,8 @@ class _ContactModalState extends State<ContactModal> {
children: [ children: [
// Favorite button // Favorite button
SizedBox( SizedBox(
width: double.infinity, width: double
.infinity, // This makes the button take full width
child: ElevatedButton.icon( child: ElevatedButton.icon(
onPressed: () { onPressed: () {
Navigator.of(context).pop(); Navigator.of(context).pop();
@ -305,25 +296,28 @@ class _ContactModalState extends State<ContactModal> {
icon: Icon(widget.isFavorite icon: Icon(widget.isFavorite
? Icons.star ? Icons.star
: Icons.star_border), : Icons.star_border),
label: Text(widget.isFavorite label: Text(
? 'Unfavorite' widget.isFavorite ? 'Unfavorite' : 'Favorite'),
: 'Favorite'),
), ),
), ),
const SizedBox(height: 10), const SizedBox(height: 10), // Space between buttons
// Edit button // Edit button
SizedBox( SizedBox(
width: double.infinity, width: double
.infinity, // This makes the button take full width
child: ElevatedButton.icon( child: ElevatedButton.icon(
onPressed: () => widget.onEdit(), onPressed: () => widget.onEdit(),
icon: const Icon(Icons.edit), icon: const Icon(Icons.edit),
label: const Text('Edit Contact'), label: const Text('Edit Contact'),
), ),
), ),
const SizedBox(height: 10), const SizedBox(height: 10), // Space between buttons
// Block/Unblock button // Block/Unblock button
SizedBox( SizedBox(
width: double.infinity, width: double
.infinity, // This makes the button take full width
child: ElevatedButton.icon( child: ElevatedButton.icon(
onPressed: _toggleBlockState, onPressed: _toggleBlockState,
icon: Icon( icon: Icon(

View File

@ -1,7 +1,7 @@
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_contacts/contact.dart'; import 'package:flutter_contacts/contact.dart';
import 'package:dialer/services/contact_service.dart'; import 'package:qr_flutter/qr_flutter.dart';
class QRCodeButton extends StatelessWidget { class QRCodeButton extends StatelessWidget {
final List<Contact> contacts; final List<Contact> contacts;
@ -23,12 +23,31 @@ class QRCodeButton extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return IconButton( return IconButton(
icon: Icon(Icons.qr_code, color: selfContact != null ? Colors.blue : Colors.grey), icon: Icon(Icons.qr_code, color: selfContact != null ? Colors.blue : Colors.grey),
onPressed: selfContact != null onPressed: selfContact != null
? () { ? () {
// Use the ContactService to show the QR code showDialog(
ContactService().showContactQRCodeDialog(context, selfContact!); barrierColor: Colors.white24,
context: context,
barrierDismissible: true,
builder: (BuildContext context) {
return AlertDialog(
backgroundColor: Colors.black,
content: SizedBox(
width: 200,
height: 220,
child: QrImageView(
data: selfContact!.toVCard(),
version: QrVersions.auto,
backgroundColor: Colors.white, // Ensure QR code is visible on black background
size: 200.0,
),
),
);
},
);
} }
: null, : null,
); );

View File

@ -1,13 +1,10 @@
import 'package:dialer/services/obfuscate_service.dart';
import 'package:dialer/widgets/color_darkener.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'; import 'package:intl/intl.dart';
import 'package:url_launcher/url_launcher.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/username_color_generator.dart';
import '../../services/block_service.dart'; import 'package:dialer/widgets/color_darkener.dart';
import '../contacts/widgets/contact_modal.dart';
class History { class History {
final Contact contact; final Contact contact;
@ -32,14 +29,11 @@ class HistoryPage extends StatefulWidget {
_HistoryPageState createState() => _HistoryPageState(); _HistoryPageState createState() => _HistoryPageState();
} }
class _HistoryPageState extends State<HistoryPage> class _HistoryPageState extends State<HistoryPage> with SingleTickerProviderStateMixin {
with SingleTickerProviderStateMixin {
List<History> histories = []; List<History> histories = [];
bool loading = true; bool loading = true;
int? _expandedIndex; int? _expandedIndex;
final ObfuscateService _obfuscateService = ObfuscateService();
@override @override
void didChangeDependencies() { void didChangeDependencies() {
super.didChangeDependencies(); super.didChangeDependencies();
@ -48,45 +42,6 @@ class _HistoryPageState extends State<HistoryPage>
} }
} }
Future<void> _refreshContacts() async {
final contactState = ContactState.of(context);
try {
// Refresh contacts or fetch them again
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 {
// Ensure you have the necessary permissions to fetch contact details
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(); // Refresh the contact list
} 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')),
);
}
}
Future<void> _buildHistories() async { Future<void> _buildHistories() async {
final contactState = ContactState.of(context); final contactState = ContactState.of(context);
if (contactState.loading) { if (contactState.loading) {
@ -133,8 +88,7 @@ class _HistoryPageState extends State<HistoryPage>
List<History> olderHistories = []; List<History> olderHistories = [];
for (var history in historyList) { for (var history in historyList) {
final callDate = final callDate = DateTime(history.date.year, history.date.month, history.date.day);
DateTime(history.date.year, history.date.month, history.date.day);
if (callDate == today) { if (callDate == today) {
todayHistories.add(history); todayHistories.add(history);
} else if (callDate == yesterday) { } else if (callDate == yesterday) {
@ -188,8 +142,7 @@ class _HistoryPageState extends State<HistoryPage>
} }
// Filter missed calls // Filter missed calls
List<History> missedCalls = List<History> missedCalls = histories.where((h) => h.callStatus == 'missed').toList();
histories.where((h) => h.callStatus == 'missed').toList();
final allItems = _buildGroupedList(histories); final allItems = _buildGroupedList(histories);
final missedItems = _buildGroupedList(missedCalls); final missedItems = _buildGroupedList(missedCalls);
@ -253,56 +206,21 @@ class _HistoryPageState extends State<HistoryPage>
return Column( return Column(
children: [ children: [
ListTile( ListTile(
leading: GestureDetector( leading: (contact.thumbnail != null && contact.thumbnail!.isNotEmpty)
onTap: () { ? CircleAvatar(
// When the profile picture is tapped, show the ContactModal backgroundImage: MemoryImage(contact.thumbnail!),
showModalBottomSheet( )
context: context, : CircleAvatar(
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,
);
},
);
},
child: ObfuscatedAvatar(
imageBytes: contact.thumbnail,
radius: 25,
backgroundColor: avatarColor, backgroundColor: avatarColor,
fallbackInitial: contact.displayName, child: Text(
contact.displayName.isNotEmpty
? contact.displayName[0].toUpperCase()
: '?',
style: TextStyle(color: darken(avatarColor, 0.4)),
), ),
), ),
title: Text( title: Text(
_obfuscateService.obfuscateData(contact.displayName), contact.displayName,
style: const TextStyle(color: Colors.white), style: const TextStyle(color: Colors.white),
), ),
subtitle: Text( subtitle: Text(
@ -320,20 +238,18 @@ class _HistoryPageState extends State<HistoryPage>
icon: const Icon(Icons.phone, color: Colors.green), icon: const Icon(Icons.phone, color: Colors.green),
onPressed: () async { onPressed: () async {
if (contact.phones.isNotEmpty) { if (contact.phones.isNotEmpty) {
final Uri callUri = Uri( final Uri callUri =
scheme: 'tel', path: contact.phones.first.number); Uri(scheme: 'tel', path: contact.phones.first.number);
if (await canLaunchUrl(callUri)) { if (await canLaunchUrl(callUri)) {
await launchUrl(callUri); await launchUrl(callUri);
} else { } else {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
const SnackBar( const SnackBar(content: Text('Could not launch call')),
content: Text('Could not launch call')),
); );
} }
} else { } else {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
const SnackBar( const SnackBar(content: Text('Contact has no phone number')),
content: Text('Contact has no phone number')),
); );
} }
}, },
@ -349,96 +265,56 @@ class _HistoryPageState extends State<HistoryPage>
if (isExpanded) if (isExpanded)
Container( Container(
color: Colors.grey[850], color: Colors.grey[850],
child: FutureBuilder<bool>( child: Row(
future: BlockService().isNumberBlocked(
contact.phones.isNotEmpty
? contact.phones.first.number
: ''),
builder: (context, snapshot) {
final isBlocked = snapshot.data ?? false;
return Row(
mainAxisAlignment: MainAxisAlignment.spaceAround, mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [ children: [
TextButton.icon( TextButton.icon(
onPressed: () async { onPressed: () async {
if (history.contact.phones.isNotEmpty) { if (history.contact.phones.isNotEmpty) {
final Uri smsUri = Uri( final Uri smsUri =
scheme: 'sms', Uri(scheme: 'sms', path: history.contact.phones.first.number);
path: history.contact.phones.first.number);
if (await canLaunchUrl(smsUri)) { if (await canLaunchUrl(smsUri)) {
await launchUrl(smsUri); await launchUrl(smsUri);
} else { } else {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
const SnackBar( const SnackBar(content: Text('Could not send message')),
content:
Text('Could not send message')),
); );
} }
} else { } else {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
const SnackBar( const SnackBar(content: Text('Contact has no phone number')),
content:
Text('Contact has no phone number')),
); );
} }
}, },
icon: icon: const Icon(Icons.message, color: Colors.white),
const Icon(Icons.message, color: Colors.white), label: const Text('Message', style: TextStyle(color: Colors.white)),
label: const Text('Message',
style: TextStyle(color: Colors.white)),
), ),
TextButton.icon( TextButton.icon(
onPressed: () { onPressed: () {
// Navigate to Call Details page
Navigator.push( Navigator.push(
context, context,
MaterialPageRoute( MaterialPageRoute(
builder: (_) => builder: (_) => CallDetailsPage(history: history),
CallDetailsPage(history: history),
), ),
); );
}, },
icon: const Icon(Icons.info, color: Colors.white), icon: const Icon(Icons.info, color: Colors.white),
label: const Text('Details', label: const Text('Details', style: TextStyle(color: Colors.white)),
style: TextStyle(color: Colors.white)),
), ),
TextButton.icon( TextButton.icon(
onPressed: () async { onPressed: () {
final phoneNumber = contact.phones.isNotEmpty // Implement block number functionality
? contact.phones.first.number
: null;
if (phoneNumber == null) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
const SnackBar( const SnackBar(
content: content: Text('Number blocked (functionality not implemented)'),
Text('Contact has no phone number')), ),
); );
return;
}
if (isBlocked) {
await BlockService().unblockNumber(phoneNumber);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('$phoneNumber unblocked')),
);
} else {
await BlockService().blockNumber(phoneNumber);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('$phoneNumber blocked')),
);
}
setState(() {}); // Refresh the button state
}, },
icon: Icon( icon: const Icon(Icons.block, color: Colors.white),
isBlocked ? Icons.lock_open : Icons.block, label: const Text('Block', style: TextStyle(color: Colors.white)),
color: Colors.white),
label: Text(isBlocked ? 'Unblock' : 'Block',
style: const TextStyle(color: Colors.white)),
), ),
], ],
);
},
), ),
), ),
], ],
@ -453,15 +329,12 @@ class _HistoryPageState extends State<HistoryPage>
class CallDetailsPage extends StatelessWidget { class CallDetailsPage extends StatelessWidget {
final History history; final History history;
final ObfuscateService _obfuscateService = ObfuscateService();
CallDetailsPage({super.key, required this.history}); const CallDetailsPage({Key? key, required this.history}) : super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final contact = history.contact; final contact = history.contact;
final contactBg = generateColorFromName(contact.displayName);
final contactLetter = darken(contactBg);
return Scaffold( return Scaffold(
backgroundColor: Colors.black, backgroundColor: Colors.black,
@ -477,27 +350,24 @@ class CallDetailsPage extends StatelessWidget {
Row( Row(
children: [ children: [
(contact.thumbnail != null && contact.thumbnail!.isNotEmpty) (contact.thumbnail != null && contact.thumbnail!.isNotEmpty)
? ObfuscatedAvatar( ? CircleAvatar(
imageBytes: contact.thumbnail, backgroundImage: MemoryImage(contact.thumbnail!),
radius: 30, radius: 30,
backgroundColor: contactBg,
fallbackInitial: contact.displayName,
) )
: CircleAvatar( : CircleAvatar(
backgroundColor: backgroundColor: Colors.grey[700],
generateColorFromName(contact.displayName),
radius: 30, radius: 30,
child: Text( child: Text(
contact.displayName.isNotEmpty contact.displayName.isNotEmpty
? contact.displayName[0].toUpperCase() ? contact.displayName[0].toUpperCase()
: '?', : '?',
style: TextStyle(color: contactLetter), style: const TextStyle(color: Colors.white),
), ),
), ),
const SizedBox(width: 16), const SizedBox(width: 16),
Expanded( Expanded(
child: Text( child: Text(
_obfuscateService.obfuscateData(contact.displayName), contact.displayName,
style: const TextStyle(color: Colors.white, fontSize: 24), style: const TextStyle(color: Colors.white, fontSize: 24),
), ),
), ),
@ -529,8 +399,7 @@ class CallDetailsPage extends StatelessWidget {
if (contact.phones.isNotEmpty) if (contact.phones.isNotEmpty)
DetailRow( DetailRow(
label: 'Number:', label: 'Number:',
value: _obfuscateService value: contact.phones.first.number,
.obfuscateData(contact.phones.first.number),
), ),
], ],
), ),
@ -543,8 +412,7 @@ class DetailRow extends StatelessWidget {
final String label; final String label;
final String value; final String value;
const DetailRow({Key? key, required this.label, required this.value}) const DetailRow({Key? key, required this.label, required this.value}) : super(key: key);
: super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -554,8 +422,7 @@ class DetailRow extends StatelessWidget {
children: [ children: [
Text( Text(
label, label,
style: const TextStyle( style: const TextStyle(color: Colors.white70, fontWeight: FontWeight.bold),
color: Colors.white70, fontWeight: FontWeight.bold),
), ),
const SizedBox(width: 8), const SizedBox(width: 8),
Expanded( Expanded(

View File

@ -1,4 +1,3 @@
import 'package:dialer/services/obfuscate_service.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:dialer/features/contacts/contact_page.dart'; import 'package:dialer/features/contacts/contact_page.dart';
import 'package:dialer/features/favorites/favorites_page.dart'; import 'package:dialer/features/favorites/favorites_page.dart';
@ -6,18 +5,8 @@ import 'package:dialer/features/history/history_page.dart';
import 'package:dialer/features/composition/composition.dart'; import 'package:dialer/features/composition/composition.dart';
import 'package:flutter_contacts/flutter_contacts.dart'; import 'package:flutter_contacts/flutter_contacts.dart';
import 'package:dialer/features/settings/settings.dart'; import 'package:dialer/features/settings/settings.dart';
import '../../services/contact_service.dart'; import 'package:dialer/features/voicemail/voicemail_page.dart';
import '../../widgets/contact_service.dart';
const bool _privacyMode = bool.fromEnvironment('privacy-mode', defaultValue: false);
String _maskName(String name) {
if (!_privacyMode) return name;
final parts = name.split(' ');
return parts.map((part) {
if (part.length < 2) return part;
return '${part[0]}${'*' * (part.length - 1)}';
}).join(' ');
}
class _MyHomePageState extends State<MyHomePage> class _MyHomePageState extends State<MyHomePage>
with SingleTickerProviderStateMixin { with SingleTickerProviderStateMixin {
@ -25,7 +14,7 @@ class _MyHomePageState extends State<MyHomePage>
List<Contact> _allContacts = []; List<Contact> _allContacts = [];
List<Contact> _contactSuggestions = []; List<Contact> _contactSuggestions = [];
final ContactService _contactService = ContactService(); final ContactService _contactService = ContactService();
final ObfuscateService _obfuscateService = ObfuscateService();
@override @override
void initState() { void initState() {
@ -140,7 +129,14 @@ class _MyHomePageState extends State<MyHomePage>
suggestionsBuilder: suggestionsBuilder:
(BuildContext context, SearchController controller) { (BuildContext context, SearchController controller) {
return _contactSuggestions.map((contact) { return _contactSuggestions.map((contact) {
return _buildSuggestionTile(contact, controller); return ListTile(
key: ValueKey(contact.id),
title: Text(contact.displayName,
style: const TextStyle(color: Colors.white)),
onTap: () {
controller.closeView(contact.displayName);
},
);
}).toList(); }).toList();
}, },
), ),
@ -236,17 +232,6 @@ class _MyHomePageState extends State<MyHomePage>
), ),
); );
} }
Widget _buildSuggestionTile(Contact contact, SearchController controller) {
final maskedName = _maskName(contact.displayName);
return ListTile(
key: ValueKey(contact.id),
title: Text(maskedName, style: const TextStyle(color: Colors.white)),
onTap: () {
controller.closeView(maskedName);
},
);
}
} }
class MyHomePage extends StatefulWidget { class MyHomePage extends StatefulWidget {

View File

@ -1 +0,0 @@
bool isStealthMode = false;

View File

@ -1,16 +1,13 @@
import 'package:dialer/features/home/home_page.dart'; import 'package:dialer/features/home/home_page.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:dialer/features/contacts/contact_state.dart'; import 'package:dialer/features/contacts/contact_state.dart';
import 'globals.dart' as globals;
void main() { void main() {
const stealthFlag = String.fromEnvironment('STEALTH', defaultValue: 'false'); runApp(const MyApp());
globals.isStealthMode = stealthFlag.toLowerCase() == 'true';
runApp(const Dialer());
} }
class Dialer extends StatelessWidget { class MyApp extends StatelessWidget {
const Dialer({super.key}); const MyApp({super.key});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {

View File

@ -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<List<Contact>> fetchContacts() async {
if (await FlutterContacts.requestPermission()) {
return await FlutterContacts.getContacts(
withProperties: true,
withThumbnail: true,
withAccounts: true,
withGroups: true,
withPhoto: true);
}
return [];
}
Future<List<Contact>> fetchFavoriteContacts() async {
List<Contact> contacts = await fetchContacts();
return contacts.where((contact) => contact.isStarred).toList();
}
Future<void> 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,
),
),
);
},
);
}
}

View File

@ -1,91 +0,0 @@
// lib/services/obfuscate_service.dart
import 'package:dialer/widgets/color_darkener.dart';
import '../../globals.dart' as globals;
import 'dart:ui';
import 'dart:typed_data';
import 'package:flutter/material.dart';
class ObfuscateService {
// Private constructor
ObfuscateService._privateConstructor();
// Singleton instance
static final ObfuscateService _instance = ObfuscateService._privateConstructor();
// Factory constructor to return the same instance
factory ObfuscateService() {
return _instance;
}
// Public method to obfuscate data
String obfuscateData(String data) {
if (globals.isStealthMode) {
return _obfuscateData(data);
} else {
return data;
}
}
// Private helper method for obfuscation logic
String _obfuscateData(String data) {
if (data.isNotEmpty) {
// Ensure the string has at least two characters to obfuscate
if (data.length == 1) {
return '${data[0]}';
} else {
return '${data[0]}...${data[data.length - 1]}';
}
}
return '';
}
}
class ObfuscatedAvatar extends StatelessWidget {
final Uint8List? imageBytes;
final double radius;
final Color backgroundColor;
final String? fallbackInitial;
const ObfuscatedAvatar({
Key? key,
required this.imageBytes,
this.radius = 25,
this.backgroundColor = Colors.grey,
this.fallbackInitial,
}) : super(key: key);
@override
Widget build(BuildContext context) {
if (imageBytes != null && imageBytes!.isNotEmpty) {
return ClipOval(
child: ImageFiltered(
imageFilter: globals.isStealthMode
? ImageFilter.blur(sigmaX: 10, sigmaY: 10)
: ImageFilter.blur(sigmaX: 0, sigmaY: 0),
child: Image.memory(
imageBytes!,
fit: BoxFit.cover,
width: radius * 2,
height: radius * 2,
),
),
);
} else {
return CircleAvatar(
radius: radius,
backgroundColor: backgroundColor,
child: Text(
fallbackInitial != null && fallbackInitial!.isNotEmpty
? fallbackInitial![0].toUpperCase()
: '?',
style: TextStyle(
color: darken(backgroundColor),
fontSize: radius,
),
),
);
}
}
}

View File

@ -0,0 +1,31 @@
import 'package:flutter_contacts/flutter_contacts.dart';
// Service to manage contact-related operations
class ContactService {
Future<List<Contact>> fetchContacts() async {
if (await FlutterContacts.requestPermission()) {
return await FlutterContacts.getContacts(
withProperties: true,
withThumbnail: true,
withAccounts: true,
withGroups: true,
withPhoto: true);
}
return [];
}
Future<List<Contact>> fetchFavoriteContacts() async {
// Fetch all contacts
List<Contact> contacts = await fetchContacts();
// Filter contacts to only include those with isStarred: true
List<Contact> favoriteContacts =
contacts.where((contact) => contact.isStarred).toList();
return favoriteContacts;
}
Future<void> addNewContact(Contact contact) async {
await FlutterContacts.insertContact(contact);
}
}

View File

@ -40,6 +40,7 @@ dependencies:
permission_handler: ^11.3.1 # For handling permissions permission_handler: ^11.3.1 # For handling permissions
cached_network_image: ^3.2.3 # For caching contact images cached_network_image: ^3.2.3 # For caching contact images
qr_flutter: ^4.1.0 qr_flutter: ^4.1.0
android_intent_plus: ^5.2.0
camera: ^0.10.0+2 camera: ^0.10.0+2
mobile_scanner: ^6.0.2 mobile_scanner: ^6.0.2
pretty_qr_code: ^3.3.0 pretty_qr_code: ^3.3.0

View File

@ -1,3 +0,0 @@
#!/usr/bin/env bash
echo "Running Icing Dialer in STEALTH mode..."
flutter run --dart-define=STEALTH=true

View File

@ -13,7 +13,7 @@ import 'package:dialer/main.dart';
void main() { void main() {
testWidgets('Counter increments smoke test', (WidgetTester tester) async { testWidgets('Counter increments smoke test', (WidgetTester tester) async {
// Build our app and trigger a frame. // Build our app and trigger a frame.
await tester.pumpWidget(const Dialer()); await tester.pumpWidget(const MyApp());
// Verify that our counter starts at 0. // Verify that our counter starts at 0.
expect(find.text('0'), findsOneWidget); expect(find.text('0'), findsOneWidget);

View File

@ -1,15 +1,14 @@
FROM docker.io/golang:1.23 AS build ARG VER=1.23
FROM "docker.io/golang:$VER" as build
WORKDIR /build/ WORKDIR /build/
COPY go.mod go.sum . ARG VER
RUN go mod download COPY main.go .
COPY src/ . RUN printf "module main\ngo $VER" > go.mod && CGO_ENABLED=0 go build -o /app
RUN CGO_ENABLED=0 go build -o /app
FROM scratch FROM scratch
COPY --from=build /app . COPY --from=build /app /app
COPY html/ html/ COPY static/ /static/
COPY css/ css/ COPY html/ /html/
COPY static/ static/
COPY tmpl/ tmpl/
EXPOSE 3000 EXPOSE 3000
CMD ["./app"] CMD ["/app"]

View File

@ -4,19 +4,3 @@ services:
build: . build: .
ports: ports:
- "3000:3000" - "3000:3000"
develop:
watch:
- action: rebuild
path: src/
- action: sync+restart
path: tmpl/
target: tmpl/
- action: sync+restart
path: html/
target: html/
- action: sync
path: css/
target: css/
- action: sync
path: static/
target: static/

View File

@ -1,171 +0,0 @@
:root {
--primary-color: #000000;
--background-color: #f5f5f5;
--text-color: #333;
--secondary-text-color: #777;
}
.content {
margin: 20px auto;
max-width: 900px;
padding: 40px;
background-color: var(--background-color);
color: var(--text-color);
border-radius: 8px;
font-family: 'Open Sans', Arial, sans-serif;
position: relative;
overflow: hidden;
}
.title {
font-size: 2.5em;
color: var(--primary-color);
margin-bottom: 30px;
text-align: center;
animation: fadeInDown 1s ease;
}
.section-title {
font-size: 1.8em;
color: var(--primary-color);
margin-top: 40px;
margin-bottom: 20px;
position: relative;
animation: fadeInLeft 1s ease;
}
.section-title::after {
content: '';
width: 50px;
height: 3px;
background-color: var(--primary-color);
position: absolute;
bottom: -10px;
left: 0;
}
p, li {
line-height: 1.6;
font-size: 1.1em;
animation: fadeIn 1s ease;
}
ul {
margin-left: 20px;
list-style-type: disc;
}
.features ul li {
margin-bottom: 10px;
}
.team-list {
list-style-type: none;
padding: 0;
}
.team-list li {
margin-bottom: 8px;
font-weight: bold;
color: var(--text-color);
}
.back-link-container {
text-align: center;
margin-top: 40px;
animation: fadeInUp 1s ease;
}
.back-link {
text-decoration: none;
color: var(--primary-color);
font-weight: bold;
border: 2px solid var(--primary-color);
padding: 10px 20px;
border-radius: 5px;
transition: background-color 0.3s, color 0.3s;
}
.back-link:hover {
background-color: var(--primary-color);
color: #fff;
}
@keyframes fadeInDown {
from {
opacity: 0;
transform: translateY(-20px);
} to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes fadeInLeft {
from {
opacity: 0;
transform: translateX(-20px);
} to {
opacity: 1;
transform: translateX(0);
}
}
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(20px);
} to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes fadeIn {
from {
opacity: 0;
} to {
opacity: 1;
}
}
@media (max-width: 768px) {
.content {
padding: 20px;
}
.title {
font-size: 2em;
}
.section-title {
font-size: 1.5em;
}
p, li {
font-size: 1em;
}
}
@media (max-width: 480px) {
.content {
padding: 15px;
}
.title {
font-size: 1.8em;
}
.section-title {
font-size: 1.2em;
}
p, li {
font-size: 0.9em;
}
}
a {
color: var(--primary-color);
text-decoration: none;
}

View File

@ -1,16 +0,0 @@
body {
margin: 0;
}
div {
position: absolute;
height: 100%;
width: 100%;
}
.title {
display: flex;
align-items: center;
justify-content: center;
font-size: 3em;
}

View File

@ -1,4 +0,0 @@
a.nostyle {
color: inherit;
cursor: pointer;
}

View File

@ -1,3 +0,0 @@
module main
go 1.23.3

View File

View File

@ -1,63 +0,0 @@
<!DOCTYPE html>
<html>
<head>
{{template "head.tmpl". }}
</head>
<body>
<div id="description" class="content">
<h1 class="title">What is Icing?</h1>
<div class="project-overview">
<p>
Icing is a simple, lightweight, and efficient dialer designed to
replace your everyday phone app. It ensures end-to-end encryption of
telephone communications by implementing a home-made, analogic-based
voice encryption. Inspired by SRTP (Secure Real-time Transport
Protocol), using ECDH (Elliptic Curve Diffie-Hellman).
</p>
</div>
<div class="features">
<h2 class="section-title">Key Features</h2>
<ul>
<li>
<strong>End-to-End Encryption:</strong> Secure your calls with
robust encryption protocols.
</li>
<li>
<strong>Transparent:</strong> If your peer doesn't use Icing, the
call remains completely normal.
</li>
<li>
<strong>Analogic-based:</strong> An open-source, exportable,
protocol that <strong>works without internet.</strong>
</li>
</ul>
</div>
<div class="how-it-works">
<h2 class="section-title">How It Works</h2>
<p>
Icing generates a cryptographic key pair for you. Share your public key
with a neat QR code.
</p>
<p>
During a call between two Icing users, voices are encrypted,
compressed, and transmitted via the telephone network using the Icing
Acoustic Protocol.
</p>
</div>
<div class="team">
<h2 class="section-title">The Team</h2>
<ul class="team-list">
<li>{{template "stephane"}}</li>
<li>{{template "alexis"}}</li>
<li>{{template "ange"}}</li>
<li>{{template "bartosz"}}</li>
<li>{{template "florian"}}</li>
</ul>
</div>
</div>
</body>
</html>

View File

@ -0,0 +1,229 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Icing</title>
<!-- <link rel="stylesheet" href="style.css"/> -->
<style>
/* Theme colors */
:root {
--primary-color: #000000; /* Green accent color */
--background-color: #f5f5f5; /* Light background */
--text-color: #333; /* Dark text */
--secondary-text-color: #777; /* Secondary text color */
}
/* Base styles */
.content {
margin: 20px auto;
max-width: 900px;
padding: 40px;
background-color: var(--background-color);
color: var(--text-color);
border-radius: 8px;
font-family: 'Open Sans', Arial, sans-serif;
position: relative;
overflow: hidden;
}
.title {
font-size: 2.5em;
color: var(--primary-color);
margin-bottom: 30px;
text-align: center;
animation: fadeInDown 1s ease;
}
.section-title {
font-size: 1.8em;
color: var(--primary-color);
margin-top: 40px;
margin-bottom: 20px;
position: relative;
animation: fadeInLeft 1s ease;
}
.section-title::after {
content: '';
width: 50px;
height: 3px;
background-color: var(--primary-color);
position: absolute;
bottom: -10px;
left: 0;
}
p, li {
line-height: 1.6;
font-size: 1.1em;
animation: fadeIn 1s ease;
}
ul {
margin-left: 20px;
list-style-type: disc;
}
.features ul li {
margin-bottom: 10px;
}
.team-list {
list-style-type: none;
padding: 0;
}
.team-list li {
margin-bottom: 8px;
font-weight: bold;
color: var(--text-color);
}
.back-link-container {
text-align: center;
margin-top: 40px;
animation: fadeInUp 1s ease;
}
.back-link {
text-decoration: none;
color: var(--primary-color);
font-weight: bold;
border: 2px solid var(--primary-color);
padding: 10px 20px;
border-radius: 5px;
transition: background-color 0.3s, color 0.3s;
}
.back-link:hover {
background-color: var(--primary-color);
color: #fff;
}
@keyframes fadeInDown {
from {
opacity: 0;
transform: translateY(-20px);
} to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes fadeInLeft {
from {
opacity: 0;
transform: translateX(-20px);
} to {
opacity: 1;
transform: translateX(0);
}
}
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(20px);
} to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes fadeIn {
from {
opacity: 0;
} to {
opacity: 1;
}
}
/* Responsive Design */
@media (max-width: 768px) {
.content {
padding: 20px;
}
.title {
font-size: 2em;
}
.section-title {
font-size: 1.5em;
}
p, li {
font-size: 1em;
}
}
@media (max-width: 480px) {
.content {
padding: 15px;
}
.title {
font-size: 1.8em;
}
.section-title {
font-size: 1.2em;
}
p, li {
font-size: 0.9em;
}
}
a {
color: var(--primary-color);
text-decoration: none;
}
</style>
</head>
<body>
<div id="description" class="content">
<h1 class="title">Project Description</h1>
<div class="project-overview">
<h2 class="section-title">What is Icing?</h2>
<p>
Icing is a simple, lightweight, and efficient dialer designed to replace your everyday phone app. It ensures end-to-end encryption of telephone communications by implementing a home-made, analogic-based voice encryption. Inspired by SRTP (Secure Real-time Transport Protocol), using ECDH (Elliptic Curve Diffie-Hellman).
</p>
</div>
<div class="features">
<h2 class="section-title">Key Features</h2>
<ul>
<li><strong>End-to-End Encryption:</strong> Secure your calls with robust encryption protocols.</li>
<li><strong>Transparent:</strong> If your peer doesn't use Icing, the call remains completely normal.</li>
<li><strong>Analogic-based:</strong> An open-source, exportable, protocol that <strong>works without internet.</strong></li>
</ul>
</div>
<div class="how-it-works">
<h2 class="section-title">How It Works</h2>
<p>
Icing generates a cryptographic key pair for you. Share your public key with a neat QR code.
</p>
<p>
During a call between two Icing users, voices are encrypted, compressed, and transmitted via the telephone network using the Icing Acoustic Protocol.
</p>
</div>
<div class="team">
<h2 class="section-title">Our Team</h2>
<p>
We are a team of five dedicated individuals working on this solution:
</p>
<ul class="team-list">
<li><a href="https://github.com/AlexisDanlos/" target="_blank">Alexis Danlos</a></li>
<li><a href="https://github.com/AustralEpitech/" target="_blank">AustralEpitech</a></li>
<li><a href="https://github.com/Bartoszkk/" target="_blank">Bartoszkk</a></li>
<li><a href="https://github.com/FlorianGRIFFON/" target="_blank">Florian GRIFFON</a></li>
<li><a href="https://github.com/STCB/" target="_blank">STCB</a></li>
</ul>
</div>
</div>
</body>
</html>

View File

@ -1,13 +1,31 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head>
{{template "head.tmpl" .}} <style>
.centered {
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
font-size: 3em;
margin: 0;
}
.no-underline {
text-decoration: none;
color: inherit;
}
body {
margin: 0;
}
</style>
</head> </head>
<body> <body>
<a class="nostyle" href="/about"> <div id="home">
<div class="title"> <h1 class="centered">
<h1>ICING</h1> <router-link to="/description" class="no-underline">ICING</router-link>
</h1>
</div> </div>
</a>
</body> </body>
</html> </html>

11
website/html/style.css Normal file

File diff suppressed because one or more lines are too long

32
website/main.go Normal file
View File

@ -0,0 +1,32 @@
package main
import (
"log"
"net/http"
"path/filepath"
)
func route(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/style.css" {
http.ServeFile(w, r, "/html/style.css")
return
}
if len(r.URL.Path) > len("/static/") && r.URL.Path[:len("/static/")] == "/static/" {
http.ServeFile(w, r, r.URL.Path)
return
}
if r.URL.Path == "/" {
http.ServeFile(w, r, "/html/index.html")
return
}
http.ServeFile(w, r, filepath.Join("/html", r.URL.Path + ".html"))
}
func main() {
http.HandleFunc("/", route)
err := http.ListenAndServe(":3000", nil)
if err != nil {
log.Fatal(err)
}
}

View File

@ -3,35 +3,32 @@ set -o pipefail
function kapply() { function kapply() {
for f in "$@"; do for f in "$@"; do
kubectl apply -f <(envsubst < "manifests/$f") kubectl apply -f \
<(envsubst "$(env | xargs printf '$%s ')" < "manifests/$f")
done done
}; export -f kapply }
function kcreatesec() { function kcreatesec() {
kubectl create secret generic --dry-run=client -oyaml "$@" | kubectl replace -f- kubectl create secret generic --save-config --dry-run=client -oyaml "$@" | kubectl apply -f-
}; export -f kcreatesec }
function kcreatecm() { function kcreatecm() {
kubectl create configmap --dry-run=client -oyaml "$@" | kubectl replace -f- kubectl create configmap --dry-run=client -oyaml "$@" | kubectl apply -f-
}; export -f kcreatecm }
function kgseckey() { function kgseckey() {
local sec="$1"; shift local sec="$1"; shift
local key="$1"; shift local key="$1"; shift
if ! kubectl get secret "$sec" -ojson | jq -re ".data.\"$key\" // empty" | base64 -d; then kubectl get secret "$sec" -o jsonpath="{.data.$key}" | base64 -d
return 1 }
fi
}; export -f kgseckey
function kgcmkey() { function kgcmkey() {
local cm="$1"; shift local cm="$1"; shift
local key="$1"; shift local key="$1"; shift
if ! kubectl get configmap "$cm" -ojson | jq -re ".data.\"$key\" // empty"; then kubectl get configmap "$cm" -o jsonpath="{.data.$key}"
return 1 }
fi
}; export -f kgcmkey
kapply common/app.yaml kapply common/app.yaml

View File

@ -1,5 +1,4 @@
#!/bin/bash -e #!/bin/bash -e
set -o pipefail
export NB_REPLICAS=1 export NB_REPLICAS=1

View File

@ -1,5 +1,4 @@
#!/bin/bash -e #!/bin/bash -e
set -o pipefail
export NB_REPLICAS=3 export NB_REPLICAS=3

View File

@ -1,5 +0,0 @@
#!/bin/bash
branch="$(git describe --contains --all HEAD)"
xdg-open "https://$branch.g-eip-700-tls-7-1-eip-stephane.corbiere.icing.k8s.gmoker.com"

View File

@ -1,14 +0,0 @@
package main
import (
"log"
"net/http"
)
func main() {
http.HandleFunc("/", route)
generateTmpl()
if err := http.ListenAndServe(":3000", nil); err != nil {
log.Fatal(err)
}
}

View File

@ -1,51 +0,0 @@
package main
import (
"context"
"fmt"
"net/http"
"regexp"
"slices"
)
type URLParam struct{}
var routes = []struct {
methods []string
regex *regexp.Regexp
handler http.HandlerFunc
}{
{[]string{"GET"}, url(""), index},
{[]string{"GET"}, url("/static/.+"), static},
{[]string{"GET"}, url("/(.+\\.css)"), css},
{[]string{"GET"}, url("/([^/]+)"), html},
}
func url(s string) *regexp.Regexp {
return regexp.MustCompile("^" + s + "/?$")
}
func getParam(r *http.Request, i int) string {
return r.Context().Value(URLParam{}).([]string)[i]
}
func route(w http.ResponseWriter, r *http.Request) {
for _, rt := range routes {
matches := rt.regex.FindStringSubmatch(r.URL.Path)
if len(matches) > 0 {
if !slices.Contains(rt.methods, r.Method) {
w.Header().Set("Allow", r.Method)
http.Error(
w, "405 method not allowed", http.StatusMethodNotAllowed,
)
return
}
fmt.Println(r.Method, r.URL.Path)
rt.handler(w, r.WithContext(
context.WithValue(r.Context(), URLParam{}, matches[1:]),
))
return
}
}
http.NotFound(w, r)
}

View File

@ -1,31 +0,0 @@
package main
import (
"bytes"
"html/template"
"path/filepath"
"regexp"
)
var TMPL map[string][]byte
func generateTmpl() {
files, _ := filepath.Glob("html/*.html")
re := regexp.MustCompile("html/(.+).html")
pages := make([]string, len(files))
for i, f := range files {
pages[i] = re.FindStringSubmatch(f)[1]
}
TMPL = make(map[string][]byte, len(files))
for i, f := range files {
b := new(bytes.Buffer)
t, _ := template.ParseFiles(f)
t.ParseGlob("tmpl/*.tmpl")
t.Execute(b, map[string]any{
"page": pages[i],
"pages": pages,
})
TMPL[pages[i]] = b.Bytes()
}
}

View File

@ -1,29 +0,0 @@
package main
import (
"context"
"net/http"
"path/filepath"
)
func index(w http.ResponseWriter, r *http.Request) {
html(w, r.WithContext(
context.WithValue(r.Context(), URLParam{}, []string{"index"}),
))
}
func static(w http.ResponseWriter, r *http.Request) {
http.ServeFile(w, r, r.URL.Path)
}
func css(w http.ResponseWriter, r *http.Request) {
http.ServeFile(w, r, filepath.Join("css", getParam(r, 0)))
}
func html(w http.ResponseWriter, r *http.Request) {
if t, found := TMPL[getParam(r, 0)]; found {
w.Write(t)
} else {
http.NotFound(w, r)
}
}

View File

@ -1,3 +0,0 @@
<title>Icing</title>
<link rel="stylesheet" href="style.css"/>
<link rel="stylesheet" href="{{.page}}.css"/>

View File

@ -1,19 +0,0 @@
{{define "alexis"}}
<a href="https://github.com/AlexisDanlos" target="_blank">Alexis DANLOS</a>
{{end}}
{{define "ange"}}
<a href="https://yw5n.com" target="_blank">Ange DUHAYON</a>
{{end}}
{{define "bartosz"}}
<a href="https://github.com/Bartoszkk" target="_blank">Bartosz MICHALAK</a>
{{end}}
{{define "florian"}}
<a href="https://github.com/FlorianGRIFFON" target="_blank">Florian GRIFFON</a>
{{end}}
{{define "stephane"}}
<a href="https://github.com/STCB/" target="_blank">Stephane CORBIERE</a>
{{end}}