Compare commits
No commits in common. "0ab8b696dc60c9c0ccc1dc28e4b522b4d7900ceb" and "e2ae63f07ff25a1b7b4bdfafcde088d16b042547" have entirely different histories.
0ab8b696dc
...
e2ae63f07f
@ -6,7 +6,7 @@
|
|||||||
<uses-permission android:name="android.permission.READ_BLOCKED_NUMBERS" />
|
<uses-permission android:name="android.permission.READ_BLOCKED_NUMBERS" />
|
||||||
<uses-permission android:name="android.permission.WRITE_BLOCKED_NUMBERS" />
|
<uses-permission android:name="android.permission.WRITE_BLOCKED_NUMBERS" />
|
||||||
<application
|
<application
|
||||||
android:label="Icing Dialer"
|
android:label="com.example.dialer"
|
||||||
android:name="${applicationName}"
|
android:name="${applicationName}"
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
android:enableOnBackInvokedCallback="true">
|
android:enableOnBackInvokedCallback="true">
|
||||||
|
BIN
dialer/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
Executable file → Normal file
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 544 B |
BIN
dialer/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
Executable file → Normal file
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 442 B |
BIN
dialer/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
Executable file → Normal file
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 721 B |
BIN
dialer/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
Executable file → Normal file
Before Width: | Height: | Size: 4.8 KiB After Width: | Height: | Size: 1.0 KiB |
BIN
dialer/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Executable file → Normal file
Before Width: | Height: | Size: 5.6 KiB After Width: | Height: | Size: 1.4 KiB |
@ -1,5 +0,0 @@
|
|||||||
#!/bin/bash -e
|
|
||||||
|
|
||||||
IMG=git.gmoker.com/icing/flutter:main
|
|
||||||
|
|
||||||
docker run --rm -v "$PWD:/app/" "$IMG" build apk
|
|
@ -102,25 +102,6 @@ class _ContactStateState extends State<ContactState> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
bool doesContactExist(Contact contact) {
|
|
||||||
// Example: consider it "existing" if there's a matching phone number
|
|
||||||
for (final existing in _allContacts) {
|
|
||||||
if (existing.toVCard() == contact.toVCard()) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
// for (final phone in existing.phones) {
|
|
||||||
// for (final newPhone in contact.phones) {
|
|
||||||
// // Simple exact match; you can do more advanced logic
|
|
||||||
// if (phone.normalizedNumber == newPhone.normalizedNumber) {
|
|
||||||
// return true;
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// } We might switch to finer and smarter logic later, ex: remove trailing spaces, capitals
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return _InheritedContactState(
|
return _InheritedContactState(
|
||||||
|
@ -1,14 +1,22 @@
|
|||||||
|
import 'package:android_intent_plus/android_intent.dart';
|
||||||
import 'package:dialer/widgets/qr_scanner.dart';
|
import 'package:dialer/widgets/qr_scanner.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_contacts/flutter_contacts.dart';
|
|
||||||
|
|
||||||
class AddContactButton extends StatelessWidget {
|
class AddContactButton extends StatelessWidget {
|
||||||
const AddContactButton({super.key});
|
const AddContactButton({super.key});
|
||||||
|
|
||||||
|
Future<void> createNewContact() async {
|
||||||
|
AndroidIntent intent = AndroidIntent(
|
||||||
|
action: 'android.intent.action.INSERT',
|
||||||
|
type: 'vnd.android.cursor.dir/contact',
|
||||||
|
);
|
||||||
|
await intent.launch();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return IconButton(
|
return IconButton(
|
||||||
icon: const Icon(Icons.add, color: Colors.blue),
|
icon: Icon(Icons.add, color: Colors.blue),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
@ -20,37 +28,22 @@ class AddContactButton extends StatelessWidget {
|
|||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () async {
|
onPressed: () {
|
||||||
Navigator.of(context).pop(); // close dialog
|
Navigator.of(context).pop();
|
||||||
|
Navigator.push(
|
||||||
// Go to QR Scanner
|
|
||||||
final vCardString = await Navigator.push(
|
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(builder: (context) => QRCodeScannerScreen()),
|
||||||
builder: (context) => const QRCodeScannerScreen(),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (vCardString != null && vCardString is String) {
|
|
||||||
await FlutterContacts.openExternalInsert(Contact
|
|
||||||
.fromVCard(vCardString));
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
child: const Text(
|
child: Text("Scan QR code", style: TextStyle(color: Colors.white)),
|
||||||
"Scan QR code",
|
|
||||||
style: TextStyle(color: Colors.white),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
|
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
|
createNewContact();
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
// Create a blank contact entry
|
|
||||||
await FlutterContacts.openExternalInsert();
|
|
||||||
},
|
},
|
||||||
child: const Text(
|
child: Text("Create new contact", style: TextStyle(color: Colors.white)),
|
||||||
"Create new contact",
|
|
||||||
style: TextStyle(color: Colors.white),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:mobile_scanner/mobile_scanner.dart';
|
import 'package:mobile_scanner/mobile_scanner.dart';
|
||||||
|
import 'package:flutter_contacts/flutter_contacts.dart';
|
||||||
|
|
||||||
class QRCodeScannerScreen extends StatefulWidget {
|
class QRCodeScannerScreen extends StatefulWidget {
|
||||||
const QRCodeScannerScreen({Key? key}) : super(key: key);
|
const QRCodeScannerScreen({Key? key}) : super(key: key);
|
||||||
@ -9,7 +10,8 @@ class QRCodeScannerScreen extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _QRCodeScannerScreenState extends State<QRCodeScannerScreen> {
|
class _QRCodeScannerScreenState extends State<QRCodeScannerScreen> {
|
||||||
final MobileScannerController cameraController = MobileScannerController();
|
MobileScannerController cameraController = MobileScannerController();
|
||||||
|
|
||||||
bool isScanning = true;
|
bool isScanning = true;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -18,22 +20,104 @@ class _QRCodeScannerScreenState extends State<QRCodeScannerScreen> {
|
|||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
void _showInvalidQRDialog() {
|
Future<void> _createContactFromVCard(String vCardData) async {
|
||||||
|
// Parse VCard data
|
||||||
|
final Map<String, dynamic> contactInfo = _parseVCard(vCardData);
|
||||||
|
|
||||||
|
debugPrint("contactInfo: $contactInfo");
|
||||||
|
|
||||||
|
// // Create a new contact
|
||||||
|
// Contact newContact = Contact();
|
||||||
|
//
|
||||||
|
// // Set contact's name
|
||||||
|
// newContact.name = Name(
|
||||||
|
// first: contactInfo['firstName'] ?? '',
|
||||||
|
// last: contactInfo['lastName'] ?? '',
|
||||||
|
// );
|
||||||
|
//
|
||||||
|
// // Set contact's phone numbers
|
||||||
|
// if (contactInfo['phone'] != null) {
|
||||||
|
// newContact.phones = [
|
||||||
|
// Phone(contactInfo['phone'], label: PhoneLabel.mobile),
|
||||||
|
// ];
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // Set contact's emails
|
||||||
|
// if (contactInfo['email'] != null) {
|
||||||
|
// newContact.emails = [
|
||||||
|
// Email(contactInfo['email'], label: EmailLabel.home),
|
||||||
|
// ];
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // Request permission to write contacts
|
||||||
|
// bool permission = await FlutterContacts.requestPermission(readonly: false);
|
||||||
|
// if (!permission) {
|
||||||
|
// // Handle permission denied
|
||||||
|
// ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
// SnackBar(content: Text('Contacts permission denied')),
|
||||||
|
// );
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // Save the contact
|
||||||
|
// await newContact.insert();
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> _parseVCard(String vCardData) {
|
||||||
|
// Simple parser for VCard data
|
||||||
|
final Map<String, dynamic> contactInfo = {};
|
||||||
|
|
||||||
|
final lines = vCardData.split(RegExp(r'\r?\n'));
|
||||||
|
for (var line in lines) {
|
||||||
|
if (line.startsWith('FN:')) {
|
||||||
|
contactInfo['fullName'] = line.substring(3).trim();
|
||||||
|
} else if (line.startsWith('N:')) {
|
||||||
|
final names = line.substring(2).split(';');
|
||||||
|
contactInfo['lastName'] = names[0].trim();
|
||||||
|
contactInfo['firstName'] = names.length > 1 ? names[1].trim() : '';
|
||||||
|
} else if (line.startsWith('TEL:')) {
|
||||||
|
contactInfo['phone'] = line.substring(4).trim();
|
||||||
|
} else if (line.startsWith('EMAIL:')) {
|
||||||
|
contactInfo['email'] = line.substring(6).trim();
|
||||||
|
}
|
||||||
|
// Add more fields as needed
|
||||||
|
}
|
||||||
|
|
||||||
|
return contactInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _showContactAddedDialog() {
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => AlertDialog(
|
builder: (context) => AlertDialog(
|
||||||
title: const Text('Invalid QR Code'),
|
title: Text('Contact Added'),
|
||||||
content:
|
content: Text('The contact has been added to your address book.'),
|
||||||
const Text('The scanned QR code does not contain valid vCard data.'),
|
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
Navigator.of(context).pop(); // Close dialog
|
Navigator.of(context).pop(); // Close dialog
|
||||||
setState(() {
|
Navigator.of(context).pop(); // Go back to previous screen
|
||||||
isScanning = true; // Resume scanning
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
child: const Text('Try Again'),
|
child: Text('OK'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _showInvalidQRDialog() {
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => AlertDialog(
|
||||||
|
title: Text('Invalid QR Code'),
|
||||||
|
content: Text('The scanned QR code does not contain valid contact information.'),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(context).pop(); // Close dialog
|
||||||
|
isScanning = true; // Resume scanning
|
||||||
|
},
|
||||||
|
child: Text('Try Again'),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@ -44,27 +128,32 @@ class _QRCodeScannerScreenState extends State<QRCodeScannerScreen> {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: const Text('Scan Contact QR Code'),
|
title: Text('Scan Contact QR Code'),
|
||||||
),
|
),
|
||||||
body: MobileScanner(
|
body: MobileScanner(
|
||||||
controller: cameraController,
|
controller: cameraController,
|
||||||
// allowDuplicates: false, // or true, depending on your preference
|
|
||||||
onDetect: (capture) {
|
onDetect: (capture) {
|
||||||
if (!isScanning) return;
|
if (!isScanning) return;
|
||||||
isScanning = false; // Stop multiple triggers
|
|
||||||
|
|
||||||
|
isScanning = false; // Prevent multiple scans
|
||||||
final List<Barcode> barcodes = capture.barcodes;
|
final List<Barcode> barcodes = capture.barcodes;
|
||||||
|
|
||||||
for (final barcode in barcodes) {
|
for (final barcode in barcodes) {
|
||||||
final String? code = barcode.rawValue;
|
final String? code = barcode.rawValue;
|
||||||
// If the QR code contains 'BEGIN:VCARD', let's assume it's a valid vCard
|
|
||||||
if (code != null && code.contains('BEGIN:VCARD')) {
|
if (code != null && code.contains('BEGIN:VCARD')) {
|
||||||
Navigator.pop(context, code); // pop back with the full vCard text
|
// Handle valid vCard QR code
|
||||||
return;
|
_createContactFromVCard(code);
|
||||||
|
_showContactAddedDialog(); // TODO: Be a confirmation button "do you want to add XXX to your contacts?"
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If no valid vCard was found in any of the barcodes
|
if (!barcodes.any((barcode) =>
|
||||||
_showInvalidQRDialog();
|
barcode.rawValue != null && barcode.rawValue!.contains('BEGIN:VCARD'))) {
|
||||||
|
// If no valid QR code is found
|
||||||
|
_showInvalidQRDialog();
|
||||||
|
isScanning = true; // Allow scanning again for invalid QR codes
|
||||||
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@ -11,7 +11,7 @@
|
|||||||
<key>CFBundleInfoDictionaryVersion</key>
|
<key>CFBundleInfoDictionaryVersion</key>
|
||||||
<string>6.0</string>
|
<string>6.0</string>
|
||||||
<key>CFBundleName</key>
|
<key>CFBundleName</key>
|
||||||
<string>Icing Dialer</string>
|
<string>mobile_number_example</string>
|
||||||
<key>CFBundlePackageType</key>
|
<key>CFBundlePackageType</key>
|
||||||
<string>APPL</string>
|
<string>APPL</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
|
@ -1,5 +0,0 @@
|
|||||||
#!/bin/bash -e
|
|
||||||
|
|
||||||
IMG=git.gmoker.com/icing/flutter:main
|
|
||||||
|
|
||||||
docker run --rm -p 5037:5037 -v "$PWD:/app/" "$IMG" run
|
|