Merge branch 'dev' of git.gmoker.com:icing/monorepo into visual-voicemail-page
This commit is contained in:
commit
d03884d00e
@ -11,12 +11,12 @@ android {
|
|||||||
ndkVersion = flutter.ndkVersion
|
ndkVersion = flutter.ndkVersion
|
||||||
|
|
||||||
compileOptions {
|
compileOptions {
|
||||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
sourceCompatibility = JavaVersion.VERSION_17
|
||||||
targetCompatibility = JavaVersion.VERSION_1_8
|
targetCompatibility = JavaVersion.VERSION_17
|
||||||
}
|
}
|
||||||
|
|
||||||
kotlinOptions {
|
kotlinOptions {
|
||||||
jvmTarget = JavaVersion.VERSION_1_8
|
jvmTarget = JavaVersion.VERSION_17
|
||||||
}
|
}
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
|
@ -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.3-all.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-all.zip
|
@ -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.1.0" apply false
|
id "com.android.application" version "8.3.2" apply false
|
||||||
id "org.jetbrains.kotlin.android" version "1.8.22" apply false
|
id "org.jetbrains.kotlin.android" version "2.0.20" apply false
|
||||||
}
|
}
|
||||||
|
|
||||||
include ":app"
|
include ":app"
|
||||||
|
@ -1,7 +1,10 @@
|
|||||||
|
// 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 '../../widgets/contact_service.dart';
|
import '../../services/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 {
|
||||||
@ -17,6 +20,9 @@ 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();
|
||||||
@ -107,54 +113,47 @@ class _CompositionPageState extends State<CompositionPage> {
|
|||||||
child: ListView(
|
child: ListView(
|
||||||
children: _filteredContacts.isNotEmpty
|
children: _filteredContacts.isNotEmpty
|
||||||
? _filteredContacts.map((contact) {
|
? _filteredContacts.map((contact) {
|
||||||
final phoneNumber = contact.phones.isNotEmpty
|
final phoneNumber = contact.phones.isNotEmpty
|
||||||
? contact.phones.first.number
|
? contact.phones.first.number
|
||||||
: 'No phone number';
|
: 'No phone number';
|
||||||
return ListTile(
|
return ListTile(
|
||||||
title: Text(
|
title: Text(
|
||||||
contact.displayName,
|
_obfuscateService.obfuscateData(contact.displayName),
|
||||||
style:
|
style: const TextStyle(color: Colors.white),
|
||||||
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(
|
||||||
),
|
mainAxisSize: MainAxisSize.min,
|
||||||
trailing: Row(
|
children: [
|
||||||
mainAxisSize: MainAxisSize.min,
|
// Call button
|
||||||
children: [
|
IconButton(
|
||||||
// Call button
|
icon: Icon(Icons.phone,
|
||||||
IconButton(
|
color: Colors.green[300],
|
||||||
icon: Icon(Icons.phone,
|
size: 20),
|
||||||
color: Colors.green[300],
|
onPressed: () {
|
||||||
size: 20),
|
_launchPhoneDialer(phoneNumber);
|
||||||
onPressed: () {
|
|
||||||
_launchPhoneDialer(phoneNumber);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
// Message button
|
|
||||||
IconButton(
|
|
||||||
icon: Icon(Icons.message,
|
|
||||||
color: Colors.blue[300],
|
|
||||||
size: 20),
|
|
||||||
onPressed: () {
|
|
||||||
_launchSms(phoneNumber);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
onTap: () {
|
|
||||||
// Handle contact selection if needed
|
|
||||||
},
|
},
|
||||||
);
|
),
|
||||||
}).toList()
|
// Message button
|
||||||
: [
|
IconButton(
|
||||||
Center(
|
icon: Icon(Icons.message,
|
||||||
child: Text('No contacts found',
|
color: Colors.blue[300],
|
||||||
style:
|
size: 20),
|
||||||
TextStyle(color: Colors.white)))
|
onPressed: () {
|
||||||
|
_launchSms(phoneNumber);
|
||||||
|
},
|
||||||
|
),
|
||||||
],
|
],
|
||||||
|
),
|
||||||
|
onTap: () {
|
||||||
|
// Handle contact selection if needed
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}).toList()
|
||||||
|
: [],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@ -202,7 +201,7 @@ class _CompositionPageState extends State<CompositionPage> {
|
|||||||
children: [
|
children: [
|
||||||
Row(
|
Row(
|
||||||
mainAxisAlignment:
|
mainAxisAlignment:
|
||||||
MainAxisAlignment.spaceEvenly,
|
MainAxisAlignment.spaceEvenly,
|
||||||
children: [
|
children: [
|
||||||
_buildDialButton('1'),
|
_buildDialButton('1'),
|
||||||
_buildDialButton('2'),
|
_buildDialButton('2'),
|
||||||
@ -211,7 +210,7 @@ class _CompositionPageState extends State<CompositionPage> {
|
|||||||
),
|
),
|
||||||
Row(
|
Row(
|
||||||
mainAxisAlignment:
|
mainAxisAlignment:
|
||||||
MainAxisAlignment.spaceEvenly,
|
MainAxisAlignment.spaceEvenly,
|
||||||
children: [
|
children: [
|
||||||
_buildDialButton('4'),
|
_buildDialButton('4'),
|
||||||
_buildDialButton('5'),
|
_buildDialButton('5'),
|
||||||
@ -220,7 +219,7 @@ class _CompositionPageState extends State<CompositionPage> {
|
|||||||
),
|
),
|
||||||
Row(
|
Row(
|
||||||
mainAxisAlignment:
|
mainAxisAlignment:
|
||||||
MainAxisAlignment.spaceEvenly,
|
MainAxisAlignment.spaceEvenly,
|
||||||
children: [
|
children: [
|
||||||
_buildDialButton('7'),
|
_buildDialButton('7'),
|
||||||
_buildDialButton('8'),
|
_buildDialButton('8'),
|
||||||
@ -229,7 +228,7 @@ class _CompositionPageState extends State<CompositionPage> {
|
|||||||
),
|
),
|
||||||
Row(
|
Row(
|
||||||
mainAxisAlignment:
|
mainAxisAlignment:
|
||||||
MainAxisAlignment.spaceEvenly,
|
MainAxisAlignment.spaceEvenly,
|
||||||
children: [
|
children: [
|
||||||
_buildDialButton('*'),
|
_buildDialButton('*'),
|
||||||
_buildDialButton('0'),
|
_buildDialButton('0'),
|
||||||
|
@ -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 '../../widgets/contact_service.dart';
|
import '../../services/contact_service.dart';
|
||||||
|
|
||||||
class ContactState extends StatefulWidget {
|
class ContactState extends StatefulWidget {
|
||||||
final Widget child;
|
final Widget child;
|
||||||
|
@ -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,6 +24,8 @@ 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();
|
||||||
@ -124,7 +126,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: TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 28,
|
fontSize: 28,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
@ -134,31 +136,25 @@ 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
|
||||||
? contact.phones.first.number
|
? _obfuscateService.obfuscateData(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: (contact.thumbnail != null &&
|
leading: ObfuscatedAvatar(
|
||||||
contact.thumbnail!.isNotEmpty)
|
imageBytes: contact.thumbnail,
|
||||||
? CircleAvatar(
|
radius: 25,
|
||||||
backgroundImage:
|
backgroundColor: avatarColor,
|
||||||
MemoryImage(contact.thumbnail!),
|
fallbackInitial: contact.displayName,
|
||||||
)
|
),
|
||||||
: CircleAvatar(
|
title: Text(
|
||||||
backgroundColor: avatarColor,
|
_obfuscateService.obfuscateData(contact.displayName),
|
||||||
child: Text(
|
style: const TextStyle(color: Colors.white),
|
||||||
contact.displayName.isNotEmpty
|
),
|
||||||
? contact.displayName[0].toUpperCase()
|
subtitle: Text(
|
||||||
: '?',
|
phoneNumber,
|
||||||
style: TextStyle(
|
style: const TextStyle(color: Colors.white70),
|
||||||
color: darken(avatarColor, 0.4)),
|
),
|
||||||
),
|
|
||||||
),
|
|
||||||
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,
|
||||||
@ -170,8 +166,8 @@ class _AlphabetScrollPageState extends State<AlphabetScrollPage> {
|
|||||||
onEdit: () async {
|
onEdit: () async {
|
||||||
if (await FlutterContacts.requestPermission()) {
|
if (await FlutterContacts.requestPermission()) {
|
||||||
final updatedContact =
|
final updatedContact =
|
||||||
await FlutterContacts.openExternalEdit(
|
await FlutterContacts.openExternalEdit(
|
||||||
contact.id);
|
contact.id);
|
||||||
if (updatedContact != null) {
|
if (updatedContact != null) {
|
||||||
await _refreshContacts();
|
await _refreshContacts();
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
@ -187,7 +183,7 @@ class _AlphabetScrollPageState extends State<AlphabetScrollPage> {
|
|||||||
.showSnackBar(
|
.showSnackBar(
|
||||||
SnackBar(
|
SnackBar(
|
||||||
content:
|
content:
|
||||||
Text('Edit canceled or failed.'),
|
Text('Edit canceled or failed.'),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
|
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 '../../../widgets/block_service.dart';
|
import '../../../services/block_service.dart';
|
||||||
|
import '../../../services/contact_service.dart';
|
||||||
|
|
||||||
class ContactModal extends StatefulWidget {
|
class ContactModal extends StatefulWidget {
|
||||||
final Contact contact;
|
final Contact contact;
|
||||||
@ -25,6 +27,7 @@ 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() {
|
||||||
@ -94,50 +97,56 @@ class _ContactModalState extends State<ContactModal> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _deleteContact() async {
|
void _deleteContact() async {
|
||||||
final bool shouldDelete = await showDialog(
|
final bool shouldDelete = await showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => AlertDialog(
|
builder: (context) => AlertDialog(
|
||||||
title: const Text('Delete Contact'),
|
title: const Text('Delete Contact'),
|
||||||
content: Text('Are you sure you want to delete ${widget.contact.displayName}?'),
|
content: Text(
|
||||||
actions: [
|
'Are you sure you want to delete ${_obfuscateService.obfuscateData(widget.contact.displayName)}?'),
|
||||||
TextButton(
|
actions: [
|
||||||
onPressed: () => Navigator.of(context).pop(false),
|
TextButton(
|
||||||
child: const Text('Cancel'),
|
onPressed: () => Navigator.of(context).pop(false),
|
||||||
),
|
child: const Text('Cancel'),
|
||||||
TextButton(
|
),
|
||||||
onPressed: () => Navigator.of(context).pop(true),
|
TextButton(
|
||||||
child: const Text('Delete'),
|
onPressed: () => Navigator.of(context).pop(true),
|
||||||
),
|
child: const Text('Delete'),
|
||||||
],
|
),
|
||||||
),
|
],
|
||||||
);
|
),
|
||||||
|
);
|
||||||
|
|
||||||
if (shouldDelete) {
|
if (shouldDelete) {
|
||||||
try {
|
try {
|
||||||
// Delete the contact
|
// Delete the contact
|
||||||
await FlutterContacts.deleteContact(widget.contact);
|
await FlutterContacts.deleteContact(widget.contact);
|
||||||
|
|
||||||
// Show success message
|
// Show success message
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(content: Text('${widget.contact.displayName} deleted')),
|
SnackBar(content: Text('${_obfuscateService.obfuscateData(widget.contact.displayName)} deleted')),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Close the modal
|
// Close the modal
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Handle errors and show a failure message
|
// Handle errors and show a failure message
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(content: Text('Failed to delete ${widget.contact.displayName}: $e')),
|
SnackBar(content: Text('Failed to delete ${widget.contact.displayName}: $e')),
|
||||||
);
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
void _shareContactAsQRCode() {
|
||||||
|
// Use the ContactService to show the QR code for the contact's vCard
|
||||||
|
ContactService().showContactQRCodeDialog(context, widget.contact);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
String email = widget.contact.emails.isNotEmpty
|
String email = widget.contact.emails.isNotEmpty
|
||||||
? widget.contact.emails.first.address
|
? _obfuscateService.obfuscateData(widget.contact.emails.first.address)
|
||||||
: 'No email';
|
: 'No email';
|
||||||
|
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
@ -151,7 +160,7 @@ void _deleteContact() async {
|
|||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.grey[900],
|
color: Colors.grey[900],
|
||||||
borderRadius:
|
borderRadius:
|
||||||
const BorderRadius.vertical(top: Radius.circular(20)),
|
const BorderRadius.vertical(top: Radius.circular(20)),
|
||||||
),
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
@ -183,7 +192,10 @@ void _deleteContact() async {
|
|||||||
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 [
|
||||||
@ -202,7 +214,7 @@ void _deleteContact() async {
|
|||||||
const 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)'),
|
||||||
),
|
),
|
||||||
const PopupMenuItem<String>(
|
const PopupMenuItem<String>(
|
||||||
value: 'set_ringtone',
|
value: 'set_ringtone',
|
||||||
@ -220,40 +232,32 @@ void _deleteContact() async {
|
|||||||
padding: const EdgeInsets.all(16.0),
|
padding: const EdgeInsets.all(16.0),
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
CircleAvatar(
|
ObfuscatedAvatar(
|
||||||
|
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),
|
||||||
child: (widget.contact.thumbnail == null ||
|
fallbackInitial: widget.contact.displayName,
|
||||||
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(
|
||||||
widget.contact.displayName,
|
_obfuscateService.obfuscateData(widget.contact.displayName),
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 24, fontWeight: FontWeight.bold),
|
fontSize: 24,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Colors.white),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const Divider(),
|
const Divider(color: Colors.grey),
|
||||||
// Contact Actions
|
// Contact Actions
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: const Icon(Icons.phone, color: Colors.green),
|
leading: const Icon(Icons.phone, color: Colors.green),
|
||||||
title: Text(phoneNumber),
|
title: Text(
|
||||||
|
_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);
|
||||||
@ -262,7 +266,10 @@ void _deleteContact() async {
|
|||||||
),
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: const Icon(Icons.message, color: Colors.blue),
|
leading: const Icon(Icons.message, color: Colors.blue),
|
||||||
title: Text(phoneNumber),
|
title: Text(
|
||||||
|
_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);
|
||||||
@ -271,14 +278,17 @@ void _deleteContact() async {
|
|||||||
),
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: const Icon(Icons.email, color: Colors.orange),
|
leading: const Icon(Icons.email, color: Colors.orange),
|
||||||
title: Text(email),
|
title: Text(
|
||||||
|
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(),
|
const Divider(color: Colors.grey),
|
||||||
// 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),
|
||||||
@ -286,8 +296,7 @@ void _deleteContact() async {
|
|||||||
children: [
|
children: [
|
||||||
// Favorite button
|
// Favorite button
|
||||||
SizedBox(
|
SizedBox(
|
||||||
width: double
|
width: double.infinity,
|
||||||
.infinity, // This makes the button take full width
|
|
||||||
child: ElevatedButton.icon(
|
child: ElevatedButton.icon(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
@ -296,35 +305,32 @@ void _deleteContact() async {
|
|||||||
icon: Icon(widget.isFavorite
|
icon: Icon(widget.isFavorite
|
||||||
? Icons.star
|
? Icons.star
|
||||||
: Icons.star_border),
|
: Icons.star_border),
|
||||||
label: Text(
|
label: Text(widget.isFavorite
|
||||||
widget.isFavorite ? 'Unfavorite' : 'Favorite'),
|
? 'Unfavorite'
|
||||||
|
: 'Favorite'),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 10), // Space between buttons
|
const SizedBox(height: 10),
|
||||||
|
|
||||||
// Edit button
|
// Edit button
|
||||||
SizedBox(
|
SizedBox(
|
||||||
width: double
|
width: double.infinity,
|
||||||
.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), // Space between buttons
|
const SizedBox(height: 10),
|
||||||
|
|
||||||
// Block/Unblock button
|
// Block/Unblock button
|
||||||
SizedBox(
|
SizedBox(
|
||||||
width: double
|
width: double.infinity,
|
||||||
.infinity, // This makes the button take full width
|
|
||||||
child: ElevatedButton.icon(
|
child: ElevatedButton.icon(
|
||||||
onPressed: _toggleBlockState,
|
onPressed: _toggleBlockState,
|
||||||
icon: Icon(
|
icon: Icon(
|
||||||
isBlocked ? Icons.block : Icons.block_flipped),
|
isBlocked ? Icons.block : Icons.block_flipped),
|
||||||
label: Text(isBlocked ? 'Unblock' : 'Block'),
|
label: Text(isBlocked ? 'Unblock' : 'Block'),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -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:qr_flutter/qr_flutter.dart';
|
import 'package:dialer/services/contact_service.dart';
|
||||||
|
|
||||||
class QRCodeButton extends StatelessWidget {
|
class QRCodeButton extends StatelessWidget {
|
||||||
final List<Contact> contacts;
|
final List<Contact> contacts;
|
||||||
@ -23,32 +23,13 @@ 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
|
||||||
? () {
|
? () {
|
||||||
showDialog(
|
// Use the ContactService to show the QR code
|
||||||
barrierColor: Colors.white24,
|
ContactService().showContactQRCodeDialog(context, selfContact!);
|
||||||
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,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,13 @@
|
|||||||
|
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 'package:dialer/widgets/color_darkener.dart';
|
import '../../services/block_service.dart';
|
||||||
|
import '../contacts/widgets/contact_modal.dart';
|
||||||
|
|
||||||
class History {
|
class History {
|
||||||
final Contact contact;
|
final Contact contact;
|
||||||
@ -14,12 +17,12 @@ class History {
|
|||||||
final int attempts;
|
final int attempts;
|
||||||
|
|
||||||
History(
|
History(
|
||||||
this.contact,
|
this.contact,
|
||||||
this.date,
|
this.date,
|
||||||
this.callType,
|
this.callType,
|
||||||
this.callStatus,
|
this.callStatus,
|
||||||
this.attempts,
|
this.attempts,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
class HistoryPage extends StatefulWidget {
|
class HistoryPage extends StatefulWidget {
|
||||||
@ -29,11 +32,14 @@ class HistoryPage extends StatefulWidget {
|
|||||||
_HistoryPageState createState() => _HistoryPageState();
|
_HistoryPageState createState() => _HistoryPageState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _HistoryPageState extends State<HistoryPage> with SingleTickerProviderStateMixin {
|
class _HistoryPageState extends State<HistoryPage>
|
||||||
|
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();
|
||||||
@ -42,6 +48,45 @@ class _HistoryPageState extends State<HistoryPage> with SingleTickerProviderStat
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
@ -63,7 +108,7 @@ class _HistoryPageState extends State<HistoryPage> with SingleTickerProviderStat
|
|||||||
setState(() {
|
setState(() {
|
||||||
histories = List.generate(
|
histories = List.generate(
|
||||||
contacts.length >= 10 ? 10 : contacts.length,
|
contacts.length >= 10 ? 10 : contacts.length,
|
||||||
(index) => History(
|
(index) => History(
|
||||||
contacts[index],
|
contacts[index],
|
||||||
DateTime.now().subtract(Duration(hours: (index + 1) * 2)),
|
DateTime.now().subtract(Duration(hours: (index + 1) * 2)),
|
||||||
index % 2 == 0 ? 'outgoing' : 'incoming',
|
index % 2 == 0 ? 'outgoing' : 'incoming',
|
||||||
@ -88,7 +133,8 @@ class _HistoryPageState extends State<HistoryPage> with SingleTickerProviderStat
|
|||||||
List<History> olderHistories = [];
|
List<History> olderHistories = [];
|
||||||
|
|
||||||
for (var history in historyList) {
|
for (var history in historyList) {
|
||||||
final callDate = DateTime(history.date.year, history.date.month, history.date.day);
|
final callDate =
|
||||||
|
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) {
|
||||||
@ -142,7 +188,8 @@ class _HistoryPageState extends State<HistoryPage> with SingleTickerProviderStat
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Filter missed calls
|
// Filter missed calls
|
||||||
List<History> missedCalls = histories.where((h) => h.callStatus == 'missed').toList();
|
List<History> missedCalls =
|
||||||
|
histories.where((h) => h.callStatus == 'missed').toList();
|
||||||
|
|
||||||
final allItems = _buildGroupedList(histories);
|
final allItems = _buildGroupedList(histories);
|
||||||
final missedItems = _buildGroupedList(missedCalls);
|
final missedItems = _buildGroupedList(missedCalls);
|
||||||
@ -206,21 +253,56 @@ class _HistoryPageState extends State<HistoryPage> with SingleTickerProviderStat
|
|||||||
return Column(
|
return Column(
|
||||||
children: [
|
children: [
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: (contact.thumbnail != null && contact.thumbnail!.isNotEmpty)
|
leading: GestureDetector(
|
||||||
? CircleAvatar(
|
onTap: () {
|
||||||
backgroundImage: MemoryImage(contact.thumbnail!),
|
// When the profile picture is tapped, show the ContactModal
|
||||||
)
|
showModalBottomSheet(
|
||||||
: CircleAvatar(
|
context: context,
|
||||||
backgroundColor: avatarColor,
|
isScrollControlled: true,
|
||||||
child: Text(
|
backgroundColor: Colors.transparent,
|
||||||
contact.displayName.isNotEmpty
|
builder: (context) {
|
||||||
? contact.displayName[0].toUpperCase()
|
return ContactModal(
|
||||||
: '?',
|
contact: contact,
|
||||||
style: TextStyle(color: darken(avatarColor, 0.4)),
|
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,
|
||||||
|
fallbackInitial: contact.displayName,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
title: Text(
|
title: Text(
|
||||||
contact.displayName,
|
_obfuscateService.obfuscateData(contact.displayName),
|
||||||
style: const TextStyle(color: Colors.white),
|
style: const TextStyle(color: Colors.white),
|
||||||
),
|
),
|
||||||
subtitle: Text(
|
subtitle: Text(
|
||||||
@ -238,18 +320,20 @@ class _HistoryPageState extends State<HistoryPage> with SingleTickerProviderStat
|
|||||||
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 =
|
final Uri callUri = Uri(
|
||||||
Uri(scheme: 'tel', path: contact.phones.first.number);
|
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(content: Text('Could not launch call')),
|
const SnackBar(
|
||||||
|
content: Text('Could not launch call')),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
const SnackBar(content: Text('Contact has no phone number')),
|
const SnackBar(
|
||||||
|
content: Text('Contact has no phone number')),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -265,56 +349,96 @@ class _HistoryPageState extends State<HistoryPage> with SingleTickerProviderStat
|
|||||||
if (isExpanded)
|
if (isExpanded)
|
||||||
Container(
|
Container(
|
||||||
color: Colors.grey[850],
|
color: Colors.grey[850],
|
||||||
child: Row(
|
child: FutureBuilder<bool>(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
future: BlockService().isNumberBlocked(
|
||||||
children: [
|
contact.phones.isNotEmpty
|
||||||
TextButton.icon(
|
? contact.phones.first.number
|
||||||
onPressed: () async {
|
: ''),
|
||||||
if (history.contact.phones.isNotEmpty) {
|
builder: (context, snapshot) {
|
||||||
final Uri smsUri =
|
final isBlocked = snapshot.data ?? false;
|
||||||
Uri(scheme: 'sms', path: history.contact.phones.first.number);
|
return Row(
|
||||||
if (await canLaunchUrl(smsUri)) {
|
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||||
await launchUrl(smsUri);
|
children: [
|
||||||
} else {
|
TextButton.icon(
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
onPressed: () async {
|
||||||
const SnackBar(content: Text('Could not send message')),
|
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: () {
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (_) =>
|
||||||
|
CallDetailsPage(history: history),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
},
|
||||||
} else {
|
icon: const Icon(Icons.info, color: Colors.white),
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
label: const Text('Details',
|
||||||
const SnackBar(content: Text('Contact has no phone number')),
|
style: TextStyle(color: Colors.white)),
|
||||||
);
|
),
|
||||||
}
|
TextButton.icon(
|
||||||
},
|
onPressed: () async {
|
||||||
icon: const Icon(Icons.message, color: Colors.white),
|
final phoneNumber = contact.phones.isNotEmpty
|
||||||
label: const Text('Message', style: TextStyle(color: Colors.white)),
|
? contact.phones.first.number
|
||||||
),
|
: null;
|
||||||
TextButton.icon(
|
if (phoneNumber == null) {
|
||||||
onPressed: () {
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
// Navigate to Call Details page
|
const SnackBar(
|
||||||
Navigator.push(
|
content:
|
||||||
context,
|
Text('Contact has no phone number')),
|
||||||
MaterialPageRoute(
|
);
|
||||||
builder: (_) => CallDetailsPage(history: history),
|
return;
|
||||||
),
|
}
|
||||||
);
|
|
||||||
},
|
if (isBlocked) {
|
||||||
icon: const Icon(Icons.info, color: Colors.white),
|
await BlockService().unblockNumber(phoneNumber);
|
||||||
label: const Text('Details', style: TextStyle(color: Colors.white)),
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
),
|
SnackBar(
|
||||||
TextButton.icon(
|
content: Text('$phoneNumber unblocked')),
|
||||||
onPressed: () {
|
);
|
||||||
// Implement block number functionality
|
} else {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
await BlockService().blockNumber(phoneNumber);
|
||||||
const SnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
content: Text('Number blocked (functionality not implemented)'),
|
SnackBar(
|
||||||
),
|
content: Text('$phoneNumber blocked')),
|
||||||
);
|
);
|
||||||
},
|
}
|
||||||
icon: const Icon(Icons.block, color: Colors.white),
|
setState(() {}); // Refresh the button state
|
||||||
label: const Text('Block', style: TextStyle(color: Colors.white)),
|
},
|
||||||
),
|
icon: Icon(
|
||||||
],
|
isBlocked ? Icons.lock_open : Icons.block,
|
||||||
|
color: Colors.white),
|
||||||
|
label: Text(isBlocked ? 'Unblock' : 'Block',
|
||||||
|
style: const TextStyle(color: Colors.white)),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@ -329,12 +453,15 @@ class _HistoryPageState extends State<HistoryPage> with SingleTickerProviderStat
|
|||||||
|
|
||||||
class CallDetailsPage extends StatelessWidget {
|
class CallDetailsPage extends StatelessWidget {
|
||||||
final History history;
|
final History history;
|
||||||
|
final ObfuscateService _obfuscateService = ObfuscateService();
|
||||||
|
|
||||||
const CallDetailsPage({Key? key, required this.history}) : super(key: key);
|
CallDetailsPage({super.key, required this.history});
|
||||||
|
|
||||||
@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,
|
||||||
@ -350,24 +477,27 @@ class CallDetailsPage extends StatelessWidget {
|
|||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
(contact.thumbnail != null && contact.thumbnail!.isNotEmpty)
|
(contact.thumbnail != null && contact.thumbnail!.isNotEmpty)
|
||||||
? CircleAvatar(
|
? ObfuscatedAvatar(
|
||||||
backgroundImage: MemoryImage(contact.thumbnail!),
|
imageBytes: contact.thumbnail,
|
||||||
radius: 30,
|
radius: 30,
|
||||||
)
|
backgroundColor: contactBg,
|
||||||
|
fallbackInitial: contact.displayName,
|
||||||
|
)
|
||||||
: CircleAvatar(
|
: CircleAvatar(
|
||||||
backgroundColor: Colors.grey[700],
|
backgroundColor:
|
||||||
radius: 30,
|
generateColorFromName(contact.displayName),
|
||||||
child: Text(
|
radius: 30,
|
||||||
contact.displayName.isNotEmpty
|
child: Text(
|
||||||
? contact.displayName[0].toUpperCase()
|
contact.displayName.isNotEmpty
|
||||||
: '?',
|
? contact.displayName[0].toUpperCase()
|
||||||
style: const TextStyle(color: Colors.white),
|
: '?',
|
||||||
),
|
style: TextStyle(color: contactLetter),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
const SizedBox(width: 16),
|
const SizedBox(width: 16),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: Text(
|
||||||
contact.displayName,
|
_obfuscateService.obfuscateData(contact.displayName),
|
||||||
style: const TextStyle(color: Colors.white, fontSize: 24),
|
style: const TextStyle(color: Colors.white, fontSize: 24),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -399,7 +529,8 @@ class CallDetailsPage extends StatelessWidget {
|
|||||||
if (contact.phones.isNotEmpty)
|
if (contact.phones.isNotEmpty)
|
||||||
DetailRow(
|
DetailRow(
|
||||||
label: 'Number:',
|
label: 'Number:',
|
||||||
value: contact.phones.first.number,
|
value: _obfuscateService
|
||||||
|
.obfuscateData(contact.phones.first.number),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@ -412,7 +543,8 @@ 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}) : super(key: key);
|
const DetailRow({Key? key, required this.label, required this.value})
|
||||||
|
: super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@ -422,7 +554,8 @@ class DetailRow extends StatelessWidget {
|
|||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
label,
|
label,
|
||||||
style: const TextStyle(color: Colors.white70, fontWeight: FontWeight.bold),
|
style: const TextStyle(
|
||||||
|
color: Colors.white70, fontWeight: FontWeight.bold),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Expanded(
|
Expanded(
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
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';
|
||||||
@ -5,8 +6,18 @@ 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 'package:dialer/features/voicemail/voicemail_page.dart';
|
import '../../services/contact_service.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 {
|
||||||
@ -14,7 +25,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() {
|
||||||
@ -129,14 +140,7 @@ class _MyHomePageState extends State<MyHomePage>
|
|||||||
suggestionsBuilder:
|
suggestionsBuilder:
|
||||||
(BuildContext context, SearchController controller) {
|
(BuildContext context, SearchController controller) {
|
||||||
return _contactSuggestions.map((contact) {
|
return _contactSuggestions.map((contact) {
|
||||||
return ListTile(
|
return _buildSuggestionTile(contact, controller);
|
||||||
key: ValueKey(contact.id),
|
|
||||||
title: Text(contact.displayName,
|
|
||||||
style: const TextStyle(color: Colors.white)),
|
|
||||||
onTap: () {
|
|
||||||
controller.closeView(contact.displayName);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}).toList();
|
}).toList();
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@ -232,6 +236,17 @@ 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 {
|
||||||
|
1
dialer/lib/globals.dart
Normal file
1
dialer/lib/globals.dart
Normal file
@ -0,0 +1 @@
|
|||||||
|
bool isStealthMode = false;
|
@ -1,13 +1,16 @@
|
|||||||
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() {
|
||||||
runApp(const MyApp());
|
const stealthFlag = String.fromEnvironment('STEALTH', defaultValue: 'false');
|
||||||
|
globals.isStealthMode = stealthFlag.toLowerCase() == 'true';
|
||||||
|
runApp(const Dialer());
|
||||||
}
|
}
|
||||||
|
|
||||||
class MyApp extends StatelessWidget {
|
class Dialer extends StatelessWidget {
|
||||||
const MyApp({super.key});
|
const Dialer({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
51
dialer/lib/services/contact_service.dart
Normal file
51
dialer/lib/services/contact_service.dart
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
91
dialer/lib/services/obfuscate_service.dart
Normal file
91
dialer/lib/services/obfuscate_service.dart
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
// 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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,31 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
@ -40,7 +40,6 @@ 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
|
||||||
|
3
dialer/stealth_local_run.sh
Executable file
3
dialer/stealth_local_run.sh
Executable file
@ -0,0 +1,3 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
echo "Running Icing Dialer in STEALTH mode..."
|
||||||
|
flutter run --dart-define=STEALTH=true
|
@ -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 MyApp());
|
await tester.pumpWidget(const Dialer());
|
||||||
|
|
||||||
// Verify that our counter starts at 0.
|
// Verify that our counter starts at 0.
|
||||||
expect(find.text('0'), findsOneWidget);
|
expect(find.text('0'), findsOneWidget);
|
||||||
|
@ -1,14 +1,15 @@
|
|||||||
ARG VER=1.23
|
FROM docker.io/golang:1.23 AS build
|
||||||
|
|
||||||
FROM "docker.io/golang:$VER" as build
|
|
||||||
WORKDIR /build/
|
WORKDIR /build/
|
||||||
ARG VER
|
COPY go.mod go.sum .
|
||||||
COPY main.go .
|
RUN go mod download
|
||||||
RUN printf "module main\ngo $VER" > go.mod && CGO_ENABLED=0 go build -o /app
|
COPY src/ .
|
||||||
|
RUN CGO_ENABLED=0 go build -o /app
|
||||||
|
|
||||||
FROM scratch
|
FROM scratch
|
||||||
COPY --from=build /app /app
|
COPY --from=build /app .
|
||||||
COPY static/ /static/
|
COPY html/ html/
|
||||||
COPY html/ /html/
|
COPY css/ css/
|
||||||
|
COPY static/ static/
|
||||||
|
COPY tmpl/ tmpl/
|
||||||
EXPOSE 3000
|
EXPOSE 3000
|
||||||
CMD ["/app"]
|
CMD ["./app"]
|
||||||
|
@ -4,3 +4,19 @@ 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/
|
||||||
|
171
website/css/about.css
Normal file
171
website/css/about.css
Normal file
@ -0,0 +1,171 @@
|
|||||||
|
: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;
|
||||||
|
}
|
16
website/css/index.css
Normal file
16
website/css/index.css
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
div {
|
||||||
|
position: absolute;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 3em;
|
||||||
|
}
|
4
website/css/style.css
Normal file
4
website/css/style.css
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
a.nostyle {
|
||||||
|
color: inherit;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
3
website/go.mod
Normal file
3
website/go.mod
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
module main
|
||||||
|
|
||||||
|
go 1.23.3
|
0
website/go.sum
Normal file
0
website/go.sum
Normal file
63
website/html/about.html
Normal file
63
website/html/about.html
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
<!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>
|
@ -1,229 +0,0 @@
|
|||||||
<!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>
|
|
@ -1,31 +1,13 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<style>
|
{{template "head.tmpl" .}}
|
||||||
.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>
|
||||||
<div id="home">
|
<a class="nostyle" href="/about">
|
||||||
<h1 class="centered">
|
<div class="title">
|
||||||
<router-link to="/description" class="no-underline">ICING</router-link>
|
<h1>ICING</h1>
|
||||||
</h1>
|
</div>
|
||||||
</div>
|
</a>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
File diff suppressed because one or more lines are too long
@ -1,32 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
@ -3,32 +3,35 @@ set -o pipefail
|
|||||||
|
|
||||||
function kapply() {
|
function kapply() {
|
||||||
for f in "$@"; do
|
for f in "$@"; do
|
||||||
kubectl apply -f \
|
kubectl apply -f <(envsubst < "manifests/$f")
|
||||||
<(envsubst "$(env | xargs printf '$%s ')" < "manifests/$f")
|
|
||||||
done
|
done
|
||||||
}
|
}; export -f kapply
|
||||||
|
|
||||||
function kcreatesec() {
|
function kcreatesec() {
|
||||||
kubectl create secret generic --save-config --dry-run=client -oyaml "$@" | kubectl apply -f-
|
kubectl create secret generic --dry-run=client -oyaml "$@" | kubectl replace -f-
|
||||||
}
|
}; export -f kcreatesec
|
||||||
|
|
||||||
function kcreatecm() {
|
function kcreatecm() {
|
||||||
kubectl create configmap --dry-run=client -oyaml "$@" | kubectl apply -f-
|
kubectl create configmap --dry-run=client -oyaml "$@" | kubectl replace -f-
|
||||||
}
|
}; export -f kcreatecm
|
||||||
|
|
||||||
function kgseckey() {
|
function kgseckey() {
|
||||||
local sec="$1"; shift
|
local sec="$1"; shift
|
||||||
local key="$1"; shift
|
local key="$1"; shift
|
||||||
|
|
||||||
kubectl get secret "$sec" -o jsonpath="{.data.$key}" | base64 -d
|
if ! kubectl get secret "$sec" -ojson | jq -re ".data.\"$key\" // empty" | base64 -d; then
|
||||||
}
|
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
|
||||||
|
|
||||||
kubectl get configmap "$cm" -o jsonpath="{.data.$key}"
|
if ! kubectl get configmap "$cm" -ojson | jq -re ".data.\"$key\" // empty"; then
|
||||||
}
|
return 1
|
||||||
|
fi
|
||||||
|
}; export -f kgcmkey
|
||||||
|
|
||||||
|
|
||||||
kapply common/app.yaml
|
kapply common/app.yaml
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
#!/bin/bash -e
|
#!/bin/bash -e
|
||||||
|
set -o pipefail
|
||||||
|
|
||||||
export NB_REPLICAS=1
|
export NB_REPLICAS=1
|
||||||
|
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
#!/bin/bash -e
|
#!/bin/bash -e
|
||||||
|
set -o pipefail
|
||||||
|
|
||||||
export NB_REPLICAS=3
|
export NB_REPLICAS=3
|
||||||
|
|
||||||
|
5
website/open.sh
Executable file
5
website/open.sh
Executable file
@ -0,0 +1,5 @@
|
|||||||
|
#!/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"
|
14
website/src/main.go
Normal file
14
website/src/main.go
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
http.HandleFunc("/", route)
|
||||||
|
generateTmpl()
|
||||||
|
if err := http.ListenAndServe(":3000", nil); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
51
website/src/route.go
Normal file
51
website/src/route.go
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
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)
|
||||||
|
}
|
31
website/src/tmpl.go
Normal file
31
website/src/tmpl.go
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
29
website/src/views.go
Normal file
29
website/src/views.go
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
3
website/tmpl/head.tmpl
Normal file
3
website/tmpl/head.tmpl
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
<title>Icing</title>
|
||||||
|
<link rel="stylesheet" href="style.css"/>
|
||||||
|
<link rel="stylesheet" href="{{.page}}.css"/>
|
19
website/tmpl/vars.tmpl
Normal file
19
website/tmpl/vars.tmpl
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
{{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}}
|
Loading…
Reference in New Issue
Block a user