774 lines
31 KiB
Dart
774 lines
31 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter/services.dart';
|
|
import 'package:another_telephony/telephony.dart';
|
|
import 'package:permission_handler/permission_handler.dart';
|
|
import 'package:flutter_contacts/flutter_contacts.dart';
|
|
import '../utils/secure_messaging_service.dart';
|
|
import '../utils/database_helper.dart';
|
|
import '../utils/contact_helper.dart';
|
|
import '../utils/secure_crypto_helper.dart';
|
|
import '../utils/app_theme.dart';
|
|
import 'chat_screen.dart';
|
|
|
|
class ComposeScreen extends StatefulWidget {
|
|
const ComposeScreen({super.key});
|
|
|
|
@override
|
|
State<ComposeScreen> createState() => _ComposeScreenState();
|
|
}
|
|
|
|
class _ComposeScreenState extends State<ComposeScreen> {
|
|
final Telephony telephony = Telephony.instance;
|
|
static const platform = MethodChannel('com.example.saba/sim_cards');
|
|
static const asymmetricModeLabel = 'رمزنگاری غیر متقارن (طولانیتر و امنتر)';
|
|
|
|
final _phoneController = TextEditingController();
|
|
final _msgController = TextEditingController();
|
|
final _keyController = TextEditingController();
|
|
final _groupNameController = TextEditingController();
|
|
|
|
final List<Map<String, String>> _selectedContacts = [];
|
|
|
|
// --- کش داخلی (لیست سبک مخاطبین) ---
|
|
// به جای Map، از کلاس Contact استفاده میکنیم که فقط ID و Name دارد
|
|
List<Contact> _contactsCache = [];
|
|
|
|
List<Map<String, dynamic>> _simCards = [];
|
|
Map<String, dynamic>? _selectedSim;
|
|
bool _loadingSims = true;
|
|
bool isSending = false;
|
|
String _selectedSecurityLevel = 'normal';
|
|
|
|
Color get primaryColor => Theme.of(context).primaryColor;
|
|
final Color backgroundColor = const Color(0xFFF5F7FA);
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_fetchSimCards();
|
|
// لود کردن لیست سبک در شروع
|
|
_loadLightContacts();
|
|
}
|
|
|
|
// دریافت لیست سبک (نام و آیدی) - بسیار سریع
|
|
Future<void> _loadLightContacts() async {
|
|
try {
|
|
final contacts = await ContactHelper.getContactsLight();
|
|
if (mounted) {
|
|
setState(() {
|
|
_contactsCache = contacts;
|
|
});
|
|
}
|
|
} catch (e) {
|
|
print("Error loading light contacts: $e");
|
|
}
|
|
}
|
|
|
|
// دکمه آپدیت: هم لیست جستجو را آپدیت میکند، هم دیتابیس را
|
|
Future<void> _forceSyncContacts({bool silent = false}) async {
|
|
if (!silent) {
|
|
showDialog(
|
|
context: context,
|
|
barrierDismissible: false,
|
|
builder: (_) => const Center(child: CircularProgressIndicator()),
|
|
);
|
|
}
|
|
|
|
// 1. آپدیت لیست جستجو (سریع)
|
|
await _loadLightContacts();
|
|
|
|
// 2. آپدیت دیتابیس برای نمایش نام در صفحه اصلی (سنگین)
|
|
await ContactHelper.syncWithDevice();
|
|
|
|
if (!silent && mounted) {
|
|
Navigator.pop(context);
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(content: Text("✅ مخاطبین بروزرسانی شدند")));
|
|
}
|
|
}
|
|
|
|
Future<void> _fetchSimCards() async {
|
|
if (!await Permission.phone.request().isGranted) {
|
|
setState(() => _loadingSims = false);
|
|
return;
|
|
}
|
|
try {
|
|
final List<dynamic> result = await platform.invokeMethod('getSimCards');
|
|
List<Map<String, dynamic>> cleanList = result.map((e) {
|
|
final Map<Object?, Object?> rawMap = e as Map<Object?, Object?>;
|
|
return rawMap.map((key, value) => MapEntry(key.toString(), value));
|
|
}).toList();
|
|
|
|
setState(() {
|
|
_simCards = cleanList;
|
|
if (_simCards.isNotEmpty) _selectedSim = _simCards[0];
|
|
_loadingSims = false;
|
|
});
|
|
} catch (e) {
|
|
setState(() => _loadingSims = false);
|
|
}
|
|
}
|
|
|
|
void _openContactSearch() async {
|
|
if (!await FlutterContacts.requestPermission(readonly: true)) return;
|
|
|
|
// اگر لیست خالی بود، سعی کن دوباره بگیری
|
|
if (_contactsCache.isEmpty) {
|
|
await _loadLightContacts();
|
|
}
|
|
|
|
if (_contactsCache.isEmpty) {
|
|
if (mounted) {
|
|
ScaffoldMessenger.of(context)
|
|
.showSnackBar(const SnackBar(content: Text("مخاطبی یافت نشد")));
|
|
}
|
|
return;
|
|
}
|
|
|
|
// باز کردن سرچ با لیست سبک
|
|
final result = await showSearch<Map<String, String>?>(
|
|
context: context,
|
|
delegate: ContactsSearchDelegate(_contactsCache),
|
|
);
|
|
|
|
if (result != null) {
|
|
setState(() {
|
|
// نرمالسازی شماره قبل از افزودن
|
|
final normalizedPhone = ContactHelper.normalizePhone(result['phone']!);
|
|
|
|
final exists = _selectedContacts.any((c) =>
|
|
ContactHelper.normalizePhone(c['phone']!) == normalizedPhone);
|
|
if (!exists) {
|
|
result['phone'] = normalizedPhone;
|
|
_selectedContacts.add(result);
|
|
}
|
|
|
|
if (_selectedContacts.length == 1) {
|
|
_phoneController.text = _selectedContacts[0]['phone']!;
|
|
} else {
|
|
_phoneController.clear();
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
void _removeContact(int index) {
|
|
setState(() {
|
|
_selectedContacts.removeAt(index);
|
|
if (_selectedContacts.length == 1) {
|
|
_phoneController.text = _selectedContacts[0]['phone']!;
|
|
} else if (_selectedContacts.isEmpty) {
|
|
_phoneController.clear();
|
|
}
|
|
});
|
|
}
|
|
|
|
String _securityLabel(String level) {
|
|
switch (level) {
|
|
case 'symmetric':
|
|
return 'رمزنگاری متقارن';
|
|
case 'asymmetric':
|
|
return asymmetricModeLabel;
|
|
default:
|
|
return 'ارسال عادی';
|
|
}
|
|
}
|
|
|
|
Widget _buildSecurityModeCard() {
|
|
const options = <String>['normal', 'symmetric', 'asymmetric'];
|
|
|
|
return AppTheme.glassWrapper(
|
|
radius: 18,
|
|
child: Container(
|
|
padding: const EdgeInsets.all(16),
|
|
decoration: BoxDecoration(
|
|
color: Colors.white.withValues(alpha: 0.03),
|
|
borderRadius: BorderRadius.circular(18),
|
|
border: Border.all(color: Colors.white.withValues(alpha: 0.1), width: 0.5),
|
|
),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
const Text(
|
|
'سطح امنیت',
|
|
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: Colors.white),
|
|
),
|
|
const SizedBox(height: 12),
|
|
Wrap(
|
|
spacing: 8,
|
|
runSpacing: 8,
|
|
children: options.map((level) {
|
|
return ChoiceChip(
|
|
label: Text(_securityLabel(level)),
|
|
selected: _selectedSecurityLevel == level,
|
|
onSelected: (_) {
|
|
setState(() {
|
|
_selectedSecurityLevel = level;
|
|
});
|
|
},
|
|
selectedColor: primaryColor,
|
|
backgroundColor: Colors.white.withValues(alpha: 0.05),
|
|
labelStyle: TextStyle(
|
|
color: _selectedSecurityLevel == level
|
|
? Colors.white
|
|
: Colors.white70,
|
|
fontWeight: FontWeight.w600,
|
|
),
|
|
);
|
|
}).toList(),
|
|
),
|
|
const SizedBox(height: 12),
|
|
Text(
|
|
_selectedSecurityLevel == 'asymmetric'
|
|
? 'در حالت $asymmetricModeLabel برنامه در صورت نیاز ابتدا تبادل کلید را از طریق پیامک انجام میدهد و سپس پیام اصلی را میفرستد.'
|
|
: _selectedSecurityLevel == 'symmetric'
|
|
? 'در این حالت باید کلید مشترک یکسانی در دو طرف وارد شده باشد.'
|
|
: 'در این حالت پیام بدون رمزنگاری اضافه ارسال میشود.',
|
|
style: const TextStyle(
|
|
color: Colors.white60,
|
|
fontSize: 12,
|
|
height: 1.5,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Future<void> _sendMessage() async {
|
|
bool isGroupMode = _selectedContacts.length > 1;
|
|
|
|
if (isGroupMode) {
|
|
if (_groupNameController.text.isEmpty) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(content: Text("لطفاً نام گروه را وارد کنید")));
|
|
return;
|
|
}
|
|
if (_msgController.text.isEmpty) {
|
|
ScaffoldMessenger.of(context)
|
|
.showSnackBar(const SnackBar(content: Text("متن پیام خالی است")));
|
|
return;
|
|
}
|
|
|
|
int groupId = await DatabaseHelper.instance
|
|
.createGroup(_groupNameController.text, _selectedContacts);
|
|
|
|
String finalMsg = _msgController.text;
|
|
if (_keyController.text.isNotEmpty) {
|
|
final crypto = SecureCryptoHelper();
|
|
finalMsg = await crypto.encryptSymmetric(_msgController.text, _keyController.text);
|
|
finalMsg = "@G:SYM|" + finalMsg; // Prefix for Group Symmetric
|
|
}
|
|
|
|
setState(() => isSending = true);
|
|
|
|
int? subId =
|
|
_selectedSim != null ? _selectedSim!['subscriptionId'] as int : null;
|
|
|
|
for (var member in _selectedContacts) {
|
|
try {
|
|
String phone = ContactHelper.normalizePhone(member['phone']!);
|
|
if (subId != null) {
|
|
await telephony.sendSms(
|
|
to: phone, message: finalMsg, subscriptionId: subId);
|
|
} else {
|
|
await telephony.sendSms(to: phone, message: finalMsg);
|
|
}
|
|
} catch (_) {}
|
|
}
|
|
|
|
await DatabaseHelper.instance.saveGroupMessage(
|
|
groupId, finalMsg, DateTime.now().millisecondsSinceEpoch);
|
|
|
|
setState(() => isSending = false);
|
|
if (mounted) Navigator.pop(context, true);
|
|
return;
|
|
}
|
|
|
|
String rawPhone = _phoneController.text;
|
|
if (rawPhone.isEmpty && _selectedContacts.isNotEmpty) {
|
|
rawPhone = _selectedContacts[0]['phone']!;
|
|
}
|
|
|
|
if (rawPhone.isEmpty || _msgController.text.isEmpty) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(content: Text("شماره و متن الزامی است")));
|
|
return;
|
|
}
|
|
|
|
final normalizedPhone = ContactHelper.normalizePhone(rawPhone);
|
|
final messageText = _msgController.text.trim();
|
|
final symmetricKey = _keyController.text.trim();
|
|
|
|
if (_selectedSecurityLevel == 'symmetric' && symmetricKey.isEmpty) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(content: Text('برای رمزنگاری متقارن، کلید لازم است')),
|
|
);
|
|
return;
|
|
}
|
|
|
|
setState(() => isSending = true);
|
|
|
|
try {
|
|
final result = await SecureMessagingService.instance.sendMessage(
|
|
normalizedPhone,
|
|
messageText,
|
|
securityLevel: _selectedSecurityLevel,
|
|
symmetricKey: _selectedSecurityLevel == 'symmetric' ? symmetricKey : null,
|
|
);
|
|
|
|
if (!mounted) return;
|
|
|
|
final sentText = result['sentText'] == true;
|
|
final notice = result['notice'] as String? ?? '';
|
|
final snackText = sentText
|
|
? (notice == 'sent_fragmented' ? 'پیام چندبخشی ارسال شد' : 'پیام ارسال شد')
|
|
: notice;
|
|
|
|
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(snackText)));
|
|
setState(() => isSending = false);
|
|
|
|
if (sentText) {
|
|
Future.delayed(const Duration(milliseconds: 500), () {
|
|
if (mounted) {
|
|
Navigator.pushReplacement(
|
|
context,
|
|
MaterialPageRoute(
|
|
builder: (_) => ChatScreen(address: normalizedPhone),
|
|
),
|
|
result: true,
|
|
);
|
|
}
|
|
});
|
|
}
|
|
return;
|
|
} catch (e) {
|
|
if (!mounted) return;
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(content: Text('خطا: $e')),
|
|
);
|
|
setState(() => isSending = false);
|
|
return;
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
bool isGroupMode = _selectedContacts.length > 1;
|
|
|
|
return Scaffold(
|
|
backgroundColor: AppTheme.darkBg,
|
|
extendBodyBehindAppBar: true,
|
|
appBar: PreferredSize(
|
|
preferredSize: const Size.fromHeight(kToolbarHeight + 8),
|
|
child: AppTheme.glassWrapper(
|
|
radius: 0,
|
|
sigma: 18,
|
|
child: AppBar(
|
|
backgroundColor: Colors.transparent,
|
|
elevation: 0,
|
|
centerTitle: true,
|
|
title: Text(isGroupMode ? "ساخت گروه جدید" : "پیام جدید",
|
|
style: const TextStyle(fontWeight: FontWeight.bold, color: Colors.white)),
|
|
foregroundColor: Colors.white,
|
|
),
|
|
),
|
|
),
|
|
body: Stack(
|
|
children: [
|
|
Positioned.fill(child: CustomPaint(painter: MeshBackgroundPainter())),
|
|
SingleChildScrollView(
|
|
padding: EdgeInsets.fromLTRB(20, MediaQuery.of(context).padding.top + kToolbarHeight + 20, 20, 40),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
children: [
|
|
if (!_loadingSims && _simCards.length > 1)
|
|
Container(
|
|
margin: const EdgeInsets.only(bottom: 20),
|
|
height: 40,
|
|
child: ListView.builder(
|
|
scrollDirection: Axis.horizontal,
|
|
itemCount: _simCards.length,
|
|
itemBuilder: (context, index) {
|
|
final sim = _simCards[index];
|
|
final isSelected = _selectedSim == sim;
|
|
return Padding(
|
|
padding: const EdgeInsets.only(right: 10),
|
|
child: ChoiceChip(
|
|
label: Text("${sim['carrierName']} (Slot ${sim['slotIndex'] + 1})"),
|
|
selected: isSelected,
|
|
onSelected: (selected) => setState(() => _selectedSim = sim),
|
|
selectedColor: primaryColor,
|
|
labelStyle: TextStyle(
|
|
color: isSelected ? Colors.white : Colors.white70,
|
|
fontSize: 13),
|
|
backgroundColor: Colors.white.withValues(alpha: 0.05),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
),
|
|
|
|
// --- بخش گیرندگان ---
|
|
AppTheme.glassWrapper(
|
|
radius: 20,
|
|
sigma: 10,
|
|
child: Container(
|
|
padding: const EdgeInsets.all(16),
|
|
decoration: BoxDecoration(
|
|
color: Colors.white.withValues(alpha: 0.03),
|
|
border: Border.all(color: Colors.white.withValues(alpha: 0.1), width: 0.5),
|
|
borderRadius: BorderRadius.circular(20),
|
|
),
|
|
child: Column(
|
|
children: [
|
|
if (_selectedContacts.isNotEmpty)
|
|
Container(
|
|
width: double.infinity,
|
|
margin: const EdgeInsets.only(bottom: 12),
|
|
child: Wrap(
|
|
spacing: 8,
|
|
runSpacing: 8,
|
|
children: _selectedContacts.asMap().entries.map((entry) {
|
|
return Chip(
|
|
avatar: CircleAvatar(
|
|
backgroundColor: primaryColor.withValues(alpha: 0.8),
|
|
child: Text(
|
|
entry.value['name']!.isNotEmpty ? entry.value['name']![0] : "?",
|
|
style: const TextStyle(color: Colors.white, fontSize: 12)),
|
|
),
|
|
label: Text(entry.value['name']!, style: const TextStyle(color: Colors.white, fontSize: 13)),
|
|
deleteIcon: const Icon(Icons.close, size: 16, color: Colors.white70),
|
|
onDeleted: () => _removeContact(entry.key),
|
|
backgroundColor: Colors.white.withValues(alpha: 0.1),
|
|
side: BorderSide.none,
|
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
|
|
);
|
|
}).toList(),
|
|
),
|
|
),
|
|
if (isGroupMode)
|
|
Padding(
|
|
padding: const EdgeInsets.only(bottom: 12),
|
|
child: TextField(
|
|
controller: _groupNameController,
|
|
style: const TextStyle(color: Colors.white),
|
|
decoration: InputDecoration(
|
|
labelText: "نام گروه",
|
|
labelStyle: const TextStyle(color: Colors.white60),
|
|
prefixIcon: const Icon(Icons.groups_rounded, color: Colors.white70),
|
|
filled: true,
|
|
fillColor: Colors.white.withValues(alpha: 0.05),
|
|
border: OutlineInputBorder(borderRadius: BorderRadius.circular(14), borderSide: BorderSide.none),
|
|
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
|
|
),
|
|
),
|
|
),
|
|
Row(
|
|
children: [
|
|
Expanded(
|
|
child: TextField(
|
|
controller: _phoneController,
|
|
keyboardType: TextInputType.phone,
|
|
enabled: !isGroupMode && _selectedContacts.isEmpty,
|
|
style: const TextStyle(color: Colors.white),
|
|
decoration: InputDecoration(
|
|
labelText: "شماره گیرنده",
|
|
labelStyle: const TextStyle(color: Colors.white60),
|
|
prefixIcon: const Icon(Icons.phone_iphone_rounded, color: Colors.white70),
|
|
filled: true,
|
|
fillColor: Colors.white.withValues(alpha: 0.05),
|
|
border: OutlineInputBorder(borderRadius: BorderRadius.circular(14), borderSide: BorderSide.none),
|
|
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(width: 8),
|
|
_buildSmallButton(Icons.sync_rounded, Colors.orangeAccent, () => _forceSyncContacts(silent: false)),
|
|
const SizedBox(width: 8),
|
|
_buildSmallButton(Icons.person_add_rounded, primaryColor, _openContactSearch),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
|
|
const SizedBox(height: 18),
|
|
|
|
// --- بخش تنظیمات امنیتی ---
|
|
if (!isGroupMode) ...[
|
|
_buildSecurityModeCard(),
|
|
const SizedBox(height: 18),
|
|
],
|
|
|
|
if (isGroupMode || _selectedSecurityLevel == 'symmetric')
|
|
AppTheme.glassWrapper(
|
|
radius: 18,
|
|
child: Container(
|
|
padding: const EdgeInsets.all(16),
|
|
decoration: BoxDecoration(
|
|
color: Colors.white.withValues(alpha: 0.03),
|
|
borderRadius: BorderRadius.circular(18),
|
|
border: Border.all(color: Colors.white.withValues(alpha: 0.1), width: 0.5),
|
|
),
|
|
child: TextField(
|
|
controller: _keyController,
|
|
style: const TextStyle(color: Colors.white),
|
|
decoration: InputDecoration(
|
|
hintText: isGroupMode ? 'کلید گروه (اختیاری)' : 'کلید رمزنگاری متقارن',
|
|
hintStyle: const TextStyle(color: Colors.white30),
|
|
icon: const Icon(Icons.vpn_key_rounded, color: Colors.orangeAccent),
|
|
border: InputBorder.none,
|
|
helperText: isGroupMode
|
|
? 'پیام بهصورت متقارن برای همه اعضا رمز میشود.'
|
|
: 'کلید باید در هر دو سمت یکسان باشد.',
|
|
helperStyle: const TextStyle(color: Colors.white38, fontSize: 11),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
|
|
if (_selectedSecurityLevel == 'asymmetric' && !isGroupMode)
|
|
Container(
|
|
padding: const EdgeInsets.all(16),
|
|
decoration: BoxDecoration(
|
|
color: Colors.indigo.withValues(alpha: 0.1),
|
|
borderRadius: BorderRadius.circular(18),
|
|
border: Border.all(color: Colors.indigo.withValues(alpha: 0.2)),
|
|
),
|
|
child: const Row(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Icon(Icons.shield_rounded, color: Colors.indigoAccent, size: 20),
|
|
SizedBox(width: 12),
|
|
Expanded(
|
|
child: Text(
|
|
'در حالت رمزنگاری غیر متقارن، تبادل کلید خودکار انجام میشود. پیامهای طولانی بهصورت چندبخشی ارسال میشوند.',
|
|
style: TextStyle(color: Colors.white70, fontSize: 12, height: 1.5),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
|
|
const SizedBox(height: 18),
|
|
|
|
// --- بخش متن پیام ---
|
|
AppTheme.glassWrapper(
|
|
radius: 20,
|
|
child: Container(
|
|
padding: const EdgeInsets.all(16),
|
|
decoration: BoxDecoration(
|
|
color: Colors.white.withValues(alpha: 0.03),
|
|
borderRadius: BorderRadius.circular(20),
|
|
border: Border.all(color: Colors.white.withValues(alpha: 0.1), width: 0.5),
|
|
),
|
|
child: TextField(
|
|
controller: _msgController,
|
|
maxLength: 600,
|
|
maxLines: 6,
|
|
minLines: 4,
|
|
style: const TextStyle(color: Colors.white),
|
|
decoration: const InputDecoration(
|
|
hintText: "متن پیام خود را بنویسید...",
|
|
hintStyle: TextStyle(color: Colors.white30),
|
|
border: InputBorder.none,
|
|
counterStyle: TextStyle(color: Colors.white38),
|
|
helperText: "استفاده از پروتکل چندبخشی در صورت نیاز.",
|
|
helperStyle: TextStyle(color: Colors.white38, fontSize: 11),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
|
|
const SizedBox(height: 32),
|
|
|
|
// --- دکمه ارسال ---
|
|
Container(
|
|
height: 56,
|
|
decoration: BoxDecoration(
|
|
borderRadius: BorderRadius.circular(18),
|
|
gradient: LinearGradient(
|
|
colors: [primaryColor, primaryColor.withValues(alpha: 0.8)],
|
|
),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: primaryColor.withValues(alpha: 0.3),
|
|
blurRadius: 15,
|
|
offset: const Offset(0, 5),
|
|
),
|
|
],
|
|
),
|
|
child: ElevatedButton.icon(
|
|
icon: isSending
|
|
? const SizedBox(width: 20, height: 20, child: CircularProgressIndicator(color: Colors.white, strokeWidth: 2))
|
|
: const Icon(Icons.send_rounded, size: 20),
|
|
label: Text(isSending ? "در حال ارسال..." : "ارسال پیام",
|
|
style: const TextStyle(fontSize: 17, fontWeight: FontWeight.bold)),
|
|
onPressed: isSending ? null : _sendMessage,
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: Colors.transparent,
|
|
foregroundColor: Colors.white,
|
|
shadowColor: Colors.transparent,
|
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(18)),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildSmallButton(IconData icon, Color color, VoidCallback onTap) {
|
|
return Container(
|
|
decoration: BoxDecoration(
|
|
color: color.withValues(alpha: 0.12),
|
|
borderRadius: BorderRadius.circular(14),
|
|
),
|
|
child: IconButton(
|
|
icon: Icon(icon, color: color, size: 22),
|
|
onPressed: onTap,
|
|
splashRadius: 24,
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
// --- Delegate جدید: ورودی List<Contact> ---
|
|
class ContactsSearchDelegate extends SearchDelegate<Map<String, String>?> {
|
|
final List<Contact> contacts; // لیست سبک
|
|
|
|
ContactsSearchDelegate(this.contacts);
|
|
|
|
@override
|
|
String? get searchFieldLabel => 'جستجو نام مخاطب...';
|
|
|
|
@override
|
|
List<Widget>? buildActions(BuildContext context) {
|
|
return [
|
|
if (query.isNotEmpty)
|
|
IconButton(icon: const Icon(Icons.clear), onPressed: () => query = ''),
|
|
];
|
|
}
|
|
|
|
@override
|
|
Widget? buildLeading(BuildContext context) {
|
|
return IconButton(
|
|
icon: const Icon(Icons.arrow_back),
|
|
onPressed: () => close(context, null),
|
|
);
|
|
}
|
|
|
|
@override
|
|
Widget buildResults(BuildContext context) => _buildList(context);
|
|
@override
|
|
Widget buildSuggestions(BuildContext context) => _buildList(context);
|
|
|
|
Widget _buildList(BuildContext context) {
|
|
if (contacts.isEmpty) return const Center(child: Text("مخاطبی یافت نشد."));
|
|
|
|
final filtered = query.isEmpty
|
|
? contacts
|
|
: contacts.where((c) {
|
|
final name = c.displayName.toLowerCase();
|
|
final q = query.toLowerCase();
|
|
return name.contains(q);
|
|
}).toList();
|
|
|
|
// بررسی اینکه آیا کوئری شبیه شماره تلفن است
|
|
final bool isQueryNumeric = query.length >= 3 && RegExp(r'^[0-9+\-*#]+$').hasMatch(query);
|
|
|
|
if (filtered.isEmpty && !isQueryNumeric)
|
|
return const Center(child: Text("نتیجهای یافت نشد."));
|
|
|
|
return ListView.separated(
|
|
itemCount: filtered.length + (isQueryNumeric ? 1 : 0),
|
|
separatorBuilder: (ctx, i) => const Divider(height: 1),
|
|
itemBuilder: (context, index) {
|
|
if (isQueryNumeric && index == 0) {
|
|
return ListTile(
|
|
leading: const CircleAvatar(
|
|
backgroundColor: Colors.green,
|
|
child: Icon(Icons.phone, color: Colors.white),
|
|
),
|
|
title: Text("افزودن شماره دستی: $query",
|
|
style: const TextStyle(fontWeight: FontWeight.bold, color: Colors.green)),
|
|
subtitle: const Text("استفاده از این شماره"),
|
|
onTap: () => close(context, {'name': query, 'phone': query}),
|
|
);
|
|
}
|
|
|
|
final contact = filtered[isQueryNumeric ? index - 1 : index];
|
|
return ListTile(
|
|
leading: CircleAvatar(
|
|
backgroundColor: Colors.indigo.shade100,
|
|
child: Text(
|
|
contact.displayName.isNotEmpty ? contact.displayName[0] : "?",
|
|
style: TextStyle(color: Colors.indigo.shade900)),
|
|
),
|
|
title: Text(contact.displayName,
|
|
style: const TextStyle(fontWeight: FontWeight.bold)),
|
|
subtitle:
|
|
const Text("برای انتخاب کلیک کنید"), // شماره هنوز معلوم نیست
|
|
onTap: () => _onContactSelect(context, contact.id),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
// --- دریافت شماره فقط زمان کلیک ---
|
|
Future<void> _onContactSelect(BuildContext context, String contactId) async {
|
|
showDialog(
|
|
context: context,
|
|
barrierDismissible: false,
|
|
builder: (_) => const Center(child: CircularProgressIndicator()));
|
|
try {
|
|
// اینجا شماره را از گوشی میگیریم
|
|
final fullContact = await ContactHelper.getFullContact(contactId);
|
|
if (context.mounted) Navigator.pop(context);
|
|
|
|
if (fullContact == null || fullContact.phones.isEmpty) {
|
|
if (context.mounted)
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(content: Text("این مخاطب شماره تلفن ندارد")));
|
|
return;
|
|
}
|
|
|
|
if (fullContact.phones.length == 1) {
|
|
close(context, {
|
|
'name': fullContact.displayName,
|
|
'phone': fullContact.phones.first.number
|
|
});
|
|
} else {
|
|
_showPhoneSelection(context, fullContact);
|
|
}
|
|
} catch (e) {
|
|
if (context.mounted) Navigator.pop(context);
|
|
}
|
|
}
|
|
|
|
void _showPhoneSelection(BuildContext context, Contact contact) {
|
|
showDialog(
|
|
context: context,
|
|
builder: (_) => SimpleDialog(
|
|
title: Text("انتخاب شماره ${contact.displayName}"),
|
|
children: contact.phones
|
|
.map((p) => SimpleDialogOption(
|
|
onPressed: () {
|
|
Navigator.pop(context);
|
|
close(context,
|
|
{'name': contact.displayName, 'phone': p.number});
|
|
},
|
|
child: Text(p.number),
|
|
))
|
|
.toList(),
|
|
),
|
|
);
|
|
}
|
|
}
|