USER
اريد تحسين قائمة عرض جهات الاتصال المحددة لشك افضل واكثر ملائمة واريد عند النقر على زر تحديث قائمة التحويل ان يتم اظهار الارقام المحددة في قائمة وتحديد لكل جهه المبلغ المخصص له ثم يتم الاضافة دفعة واحدة
import 'dart:async';
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:telephony/telephony.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:flutter_contacts/flutter_contacts.dart';
import 'package:url_launcher/url_launcher.dart';
void main() => runApp(SMSApp());
void onBackgroundMessage(SmsMessage message) {
debugPrint("رسالة واردة في الخلفية: ${message.body}");
}
class SMSApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'تحويل رصيد متعدد',
debugShowCheckedModeBanner: false,
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(
seedColor: const Color(0xFF1A73E8),
primary: const Color(0xFF1A73E8),
secondary: const Color(0xFF4285F4),
tertiary: const Color(0xFF34A853),
background: const Color(0xFFF8F9FA),
surface: Colors.white,
onPrimary: Colors.white,
onSecondary: Colors.white,
onBackground: const Color(0xFF202124),
onSurface: const Color(0xFF202124),
),
useMaterial3: true,
fontFamily: 'Tajawal',
appBarTheme: AppBarTheme(
backgroundColor: const Color(0xFF1A73E8),
foregroundColor: Colors.white,
elevation: 0,
centerTitle: true,
titleTextStyle: const TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
),
iconTheme: const IconThemeData(color: Colors.white),
),
cardTheme: CardTheme(
color: Colors.white,
elevation: 2,
margin: const EdgeInsets.all(8),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
surfaceTintColor: Colors.white,
),
elevatedButtonTheme: ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF1A73E8),
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
elevation: 0,
textStyle: const TextStyle(
fontWeight: FontWeight.w600,
fontSize: 16,
),
),
),
outlinedButtonTheme: OutlinedButtonThemeData(
style: OutlinedButton.styleFrom(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
side: const BorderSide(width: 1.5, color: Color(0xFF1A73E8)),
textStyle: const TextStyle(fontWeight: FontWeight.w600),
),
),
textButtonTheme: TextButtonThemeData(
style: TextButton.styleFrom(
foregroundColor: const Color(0xFF1A73E8),
textStyle: const TextStyle(fontWeight: FontWeight.w600),
),
),
snackBarTheme: SnackBarThemeData(
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
backgroundColor: const Color(0xFF1A73E8),
contentTextStyle: const TextStyle(color: Colors.white),
),
inputDecorationTheme: InputDecorationTheme(
filled: true,
fillColor: Colors.grey.shade50,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide.none,
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: const BorderSide(color: Color(0xFF1A73E8), width: 2),
),
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 14,
),
),
dialogTheme: DialogTheme(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
backgroundColor: Colors.white,
elevation: 8,
surfaceTintColor: Colors.white,
),
listTileTheme: ListTileThemeData(
iconColor: const Color(0xFF1A73E8),
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
dividerTheme: DividerThemeData(
color: Colors.grey.shade200,
thickness: 1,
space: 0,
),
progressIndicatorTheme: const ProgressIndicatorThemeData(
color: Color(0xFF1A73E8),
),
chipTheme: ChipThemeData(
backgroundColor: Colors.grey.shade100,
selectedColor: const Color(0xFF1A73E8),
secondarySelectedColor: const Color(0xFF1A73E8),
disabledColor: Colors.grey.shade300,
labelStyle: const TextStyle(color: Colors.black),
secondaryLabelStyle: const TextStyle(color: Colors.white),
brightness: Brightness.light,
padding: EdgeInsets.symmetric(horizontal: 12, vertical: 4),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
),
),
home: SMSHomePage(),
);
}
}
class TransferItem {
String number;
int amount;
String? pin;
TransferItem({required this.number, required this.amount, this.pin});
Map<String, dynamic> toMap() {
return {'number': number, 'amount': amount, 'pin': pin};
}
factory TransferItem.fromMap(Map<String, dynamic> map) {
return TransferItem(
number: map['number'],
amount: map['amount'],
pin: map['pin'],
);
}
}
extension ContactExtensions on Contact {
String initials() {
if (displayName.isEmpty) return '';
final parts = displayName.trim().split(RegExp(r'\s+'));
if (parts.length == 1) {
return parts[0].substring(0, 1).toUpperCase();
} else {
return (parts[0].substring(0, 1) + parts[1].substring(0, 1))
.toUpperCase();
}
}
}
class SMSHomePage extends StatefulWidget {
@override
_SMSHomePageState createState() => _SMSHomePageState();
}
class _SMSHomePageState extends State<SMSHomePage> with WidgetsBindingObserver {
final Telephony _telephony = Telephony.instance;
SharedPreferences? _prefs;
List<TransferItem> _transferList = [];
String _selectedMethod = 'yemenMobile';
bool _isTransferring = false;
List<Map<String, dynamic>> _transferLogs = [];
List<String> _savedNumbers = [];
List<int> _savedAmounts = [];
bool _didShowFavoritesDialog = false;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
_initApp();
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
Future<void> _initApp() async {
_prefs = await SharedPreferences.getInstance();
// Load amounts first
List<String>? amountsStr = _prefs?.getStringList('savedAmounts');
if (amountsStr != null) {
_savedAmounts =
amountsStr
.map((e) => int.tryParse(e) ?? 0)
.where((e) => e > 0)
.toList();
}
// Show dialog to input amounts if empty, only once per run
if (_savedAmounts.isEmpty && !_didShowFavoritesDialog) {
_didShowFavoritesDialog = true;
await Future.delayed(Duration.zero);
await _showFavoritesDialog();
}
List<String>? savedTransfers = _prefs?.getStringList('transferList');
if (savedTransfers != null && savedTransfers.isNotEmpty) {
_transferList =
savedTransfers
.map(
(e) => TransferItem.fromMap(
json.decode(e) as Map<String, dynamic>,
),
)
.toList();
} else {
_transferList = [];
}
_selectedMethod = _prefs?.getString('selectedMethod') ?? 'yemenMobile';
_savedNumbers = _prefs?.getStringList('savedNumbers') ?? [];
await _requestPermissions();
if (mounted) setState(() {});
}
Future<bool> _requestPermissions() async {
final phoneStatus = await Permission.phone.status;
final contactsStatus = await Permission.contacts.status;
if (!phoneStatus.isGranted || !contactsStatus.isGranted) {
final status = await [Permission.phone, Permission.contacts].request();
if (!status[Permission.phone]!.isGranted ||
!status[Permission.contacts]!.isGranted) {
_showSnackBar(
'يجب منح أذونات المكالمات وجهات الاتصال لاستخدام التطبيق',
);
return false;
}
}
return true;
}
Future<void> _saveTransferList() async {
final list = _transferList.map((e) => json.encode(e.toMap())).toList();
await _prefs?.setStringList('transferList', list);
}
Future<void> _saveSelectedMethod() async {
await _prefs?.setString('selectedMethod', _selectedMethod);
}
Future<void> _saveSavedNumbers() async {
await _prefs?.setStringList('savedNumbers', _savedNumbers);
}
Future<void> _saveSavedAmounts() async {
List<String> strList = _savedAmounts.map((e) => e.toString()).toList();
await _prefs?.setStringList('savedAmounts', strList);
}
void _showSnackBar(String msg) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(msg),
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
),
);
}
// دالة للحصول على اسم جهة اتصال من رقم هاتف مع البحث في جهات الاتصال (للمختصر)
Future<String?> _getContactNameFromNumber(String number) async {
final hasPermission = await Permission.contacts.status;
if (!hasPermission.isGranted) {
final status = await Permission.contacts.request();
if (!status.isGranted) return null;
}
final contacts = await FlutterContacts.getContacts(withProperties: true);
for (var contact in contacts) {
for (var phone in contact.phones) {
final normalizedPhone = phone.number.replaceAll(RegExp(r'[^\d+]'), '');
final normalizedNumber = number.replaceAll(RegExp(r'[^\d+]'), '');
if (normalizedPhone == normalizedNumber) {
return contact.displayName;
}
}
}
return null;
}
// شاشة مفضلة المبالغ: إدخال و تعديل و حذف المبالغ
Future<void> _showFavoritesDialog() async {
final amountsController = TextEditingController();
await showDialog(
context: context,
barrierDismissible: false,
builder: (context) {
return StatefulBuilder(
builder: (context, setDialogState) {
return AlertDialog(
title: const Text('إعداد المبالغ المفضلة'),
content: SizedBox(
width: double.maxFinite,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
if (_savedAmounts.isEmpty)
const Padding(
padding: EdgeInsets.all(8.0),
child: Text(
'الرجاء إدخال المبالغ التي ترغب باستخدامها لاختيارها عند إضافة جهة اتصال',
textAlign: TextAlign.center,
),
),
if (_savedAmounts.isNotEmpty)
Wrap(
spacing: 8,
runSpacing: 4,
children:
_savedAmounts.map((amt) {
return Chip(
label: Text('$amt'),
deleteIcon: const Icon(Icons.close),
onDeleted: () {
setDialogState(() {
_savedAmounts.remove(amt);
});
},
);
}).toList(),
),
const SizedBox(height: 16),
TextField(
controller: amountsController,
decoration: InputDecoration(
labelText: 'إضافة مبلغ جديد',
prefixIcon: Icon(Icons.add),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
),
suffixIcon: IconButton(
icon: const Icon(Icons.add_circle_outline),
onPressed: () {
final input = amountsController.text.trim();
final numAmount = int.tryParse(input);
if (numAmount == null || numAmount <= 0) {
_showSnackBar(
'يرجى إدخال مبلغ صحيح وكبير من صفر',
);
return;
}
if (_savedAmounts.contains(numAmount)) {
_showSnackBar('هذا المبلغ موجود بالفعل');
return;
}
setDialogState(() {
_savedAmounts.add(numAmount);
_savedAmounts.sort();
amountsController.clear();
});
},
),
),
keyboardType: TextInputType.number,
onSubmitted: (value) {
final numAmount = int.tryParse(value.trim());
if (numAmount == null || numAmount <= 0) {
_showSnackBar('يرجى إدخال مبلغ صحيح وكبير من صفر');
return;
}
if (_savedAmounts.contains(numAmount)) {
_showSnackBar('هذا المبلغ موجود بالفعل');
return;
}
setDialogState(() {
_savedAmounts.add(numAmount);
_savedAmounts.sort();
amountsController.clear();
});
},
),
],
),
),
actions: [
TextButton(
onPressed: () {
Navigator.pop(context);
},
child: const Text('تخطي'),
),
ElevatedButton(
onPressed: () async {
if (_savedAmounts.isEmpty) {
_showSnackBar('الرجاء إدخال مبلغ واحد على الأقل');
return;
}
await _saveSavedAmounts();
Navigator.pop(context);
},
child: const Text('حفظ'),
),
],
);
},
);
},
);
}
// شاشة اختيار المبالغ عند إضافة جهة اتصال
Future<int?> _pickAmountDialog() async {
if (_savedAmounts.isEmpty) {
_showSnackBar('لا توجد مبالغ محفوظة. الرجاء إضافتها في الإعدادات.');
return null;
}
int? selectedAmount;
await showModalBottomSheet(
context: context,
builder: (context) {
return SafeArea(
child: Container(
padding: const EdgeInsets.all(16),
child: Wrap(
spacing: 12,
children:
_savedAmounts.map((amount) {
return ChoiceChip(
label: Text('$amount'),
selected: selectedAmount == amount,
onSelected: (_) {
selectedAmount = amount;
Navigator.pop(context);
},
);
}).toList(),
),
),
);
},
);
return selectedAmount;
}
void _showSavedNumbersSelection() async {
final hasPermission = await _requestPermissions();
if (!hasPermission) return;
final contacts = await FlutterContacts.getContacts(
withProperties: true,
withThumbnail: true,
);
List<String> selectedNumbers = List.from(_savedNumbers);
TextEditingController searchController = TextEditingController();
String searchQuery = '';
showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (context) {
return StatefulBuilder(
builder: (context, setModal) {
final filteredContacts =
(searchQuery.isEmpty)
? contacts
: contacts.where((contact) {
final contactNameLower =
contact.displayName.toLowerCase();
final queryLower = searchQuery.toLowerCase();
final phonesMatch = contact.phones.any(
(phone) =>
phone.number.toLowerCase().contains(queryLower),
);
return contactNameLower.contains(queryLower) ||
phonesMatch;
}).toList();
return Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
borderRadius: const BorderRadius.vertical(
top: Radius.circular(24),
),
),
height: MediaQuery.of(context).size.height * 0.75,
padding: EdgeInsets.only(
bottom: MediaQuery.of(context).viewInsets.bottom,
),
child: Column(
children: [
Padding(
padding: const EdgeInsets.all(16),
child: Row(
children: [
Expanded(
child: TextField(
controller: searchController,
decoration: InputDecoration(
prefixIcon: Icon(Icons.search),
hintText: 'ابحث في جهات الاتصال أو الأرقام',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
),
isDense: true,
contentPadding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 12,
),
),
onChanged: (value) {
setModal(() {
searchQuery = value.trim();
});
},
),
),
IconButton(
icon: Icon(
Icons.close,
color: Theme.of(context).colorScheme.onSurface,
),
onPressed: () => Navigator.pop(context),
),
],
),
),
const Divider(height: 1),
Expanded(
child:
filteredContacts.isEmpty
? Center(
child: Text(
'لا توجد جهات اتصال تطابق البحث',
style: TextStyle(
color: Theme.of(
context,
).colorScheme.onSurface.withOpacity(0.6),
),
),
)
: ListView.builder(
itemCount: filteredContacts.length,
itemBuilder: (context, index) {
final contact = filteredContacts[index];
final phones = contact.phones;
return Card(
margin: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 6,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
elevation: 3,
child: Padding(
padding: const EdgeInsets.symmetric(
vertical: 8,
horizontal: 12,
),
child: Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
ListTile(
contentPadding: EdgeInsets.zero,
leading: CircleAvatar(
radius: 24,
backgroundImage:
(contact.thumbnail != null &&
contact
.thumbnail!
.isNotEmpty)
? MemoryImage(
contact.thumbnail!,
)
: null,
child:
(contact.thumbnail == null ||
contact
.thumbnail!
.isEmpty)
? Text(
contact.initials(),
style: TextStyle(
fontWeight:
FontWeight.bold,
color:
Theme.of(context)
.colorScheme
.onPrimary,
),
)
: null,
backgroundColor:
Theme.of(
context,
).colorScheme.primary,
),
title: Text(
contact.displayName,
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
),
const SizedBox(height: 8),
Column(
children:
phones.map((phone) {
final normalized = phone.number
.replaceAll(
RegExp(r'[^\d+]'),
'',
);
final isSelected =
selectedNumbers.any((num) {
final n = num.replaceAll(
RegExp(r'[^\d+]'),
'',
);
return n == normalized;
});
return CheckboxListTile(
contentPadding:
EdgeInsets.zero,
value: isSelected,
title: Text(phone.number),
controlAffinity:
ListTileControlAffinity
.leading,
onChanged: (val) {
setModal(() {
if (val == true) {
if (!selectedNumbers.any(
(num) {
final n =
num.replaceAll(
RegExp(
r'[^\d+]',
),
'',
);
return n ==
normalized;
},
)) {
selectedNumbers.add(
phone.number,
);
}
} else {
selectedNumbers
.removeWhere((num) {
final n =
num.replaceAll(
RegExp(
r'[^\d+]',
),
'',
);
return n ==
normalized;
});
}
});
},
);
}).toList(),
),
],
),
),
);
},
),
),
Padding(
padding: const EdgeInsets.all(16),
child: Row(
children: [
Expanded(
child: OutlinedButton(
onPressed: () => Navigator.pop(context),
child: const Text('إلغاء'),
),
),
const SizedBox(width: 12),
Expanded(
child: ElevatedButton(
onPressed: () async {
_savedNumbers = List.from(selectedNumbers);
await _saveSavedNumbers();
Navigator.pop(context);
_showSnackBar('تم حفظ الأرقام المختارة');
// فور حفظ الأرقام يمكن تحديث قائمة التحويل
},
child: const Text('حفظ'),
),
),
],
),
),
],
),
);
},
);
},
);
}
Future<void> _addTransfersFromSavedNumbers() async {
for (var number in _savedNumbers) {
if (!_transferList.any((element) => element.number == number)) {
int chosenAmount = 0;
chosenAmount = await _pickAmountDialog() ?? 0;
if (chosenAmount <= 0) {
_showSnackBar('تم تجاهل رقم $number لأنه لم يتم اختيار مبلغ صالح');
continue;
}
_transferList.add(
TransferItem(number: number, amount: chosenAmount, pin: '1234'),
);
}
}
await _saveTransferList();
setState(() {});
}
void _removeTransferAt(int index) async {
setState(() {
_transferList.removeAt(index);
});
await _saveTransferList();
}
void _editTransfer(int index) {
var item = _transferList[index];
int? selectedAmount = item.amount;
final pinController = TextEditingController(text: item.pin ?? '1234');
showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (context) {
return StatefulBuilder(
builder: (context, setModal) {
return Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
borderRadius: const BorderRadius.vertical(
top: Radius.circular(24),
),
),
padding: EdgeInsets.only(
bottom: MediaQuery.of(context).viewInsets.bottom,
top: 16,
left: 16,
right: 16,
),
child: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"تعديل التحويل",
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 18,
color: Theme.of(context).colorScheme.onSurface,
),
),
IconButton(
icon: Icon(
Icons.close,
color: Theme.of(context).colorScheme.onSurface,
),
onPressed: () => Navigator.pop(context),
),
],
),
SizedBox(height: 8),
Text(
item.number,
style: TextStyle(
color: Theme.of(
context,
).colorScheme.onSurface.withOpacity(0.7),
fontWeight: FontWeight.w600,
),
),
const Divider(height: 24),
Align(
alignment: Alignment.centerRight,
child: Text(
'اختر المبلغ',
style: TextStyle(
color: Theme.of(context).colorScheme.onSurface,
fontWeight: FontWeight.w600,
),
),
),
const SizedBox(height: 8),
Wrap(
spacing: 8,
children:
_savedAmounts.map((amt) {
return ChoiceChip(
label: Text('$amt'),
selected: selectedAmount == amt,
onSelected: (_) {
setModal(() {
selectedAmount = amt;
});
},
);
}).toList(),
),
if (_selectedMethod == 'yemenMobile' ||
_selectedMethod == 'sabafon')
Padding(
padding: const EdgeInsets.fromLTRB(0, 24, 0, 24),
child: TextField(
controller: pinController,
keyboardType: TextInputType.number,
decoration: InputDecoration(
labelText: "الرقم السري",
prefixIcon: Icon(Icons.lock_outline),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
),
),
obscureText: true,
),
)
else
const SizedBox(height: 24),
Row(
children: [
Expanded(
child: OutlinedButton(
onPressed: () => Navigator.pop(context),
child: const Text("إلغاء"),
),
),
const SizedBox(width: 12),
Expanded(
child: ElevatedButton(
onPressed: () async {
if (selectedAmount == null ||
selectedAmount! <= 0) {
_showSnackBar(
'يرجى اختيار مبلغ صحيح أكبر من صفر',
);
return;
}
setState(() {
_transferList[index].amount = selectedAmount!;
if (_selectedMethod == 'yemenMobile' ||
_selectedMethod == 'sabafon') {
final pinStr = pinController.text.trim();
_transferList[index].pin =
pinStr.isEmpty ? '1234' : pinStr;
} else {
_transferList[index].pin = null;
}
});
await _saveTransferList();
Navigator.pop(context);
},
child: const Text("حفظ"),
),
),
],
),
],
),
),
);
},
);
},
);
}
Widget _buildTransferMethodMenu() {
final Map<String, String> methodsMap = {
'yemenMobile': 'يمن موبايل',
'y': 'واي',
'sabafon': 'سبأفون',
'you': 'يو',
};
return PopupMenuButton<String>(
icon: const Icon(Icons.compare_arrows),
tooltip: 'طريقة التحويل',
onSelected: (value) async {
setState(() => _selectedMethod = value);
await _saveSelectedMethod();
if (_selectedMethod == 'yemenMobile' || _selectedMethod == 'sabafon') {
for (var item in _transferList) {
item.pin ??= '1234';
}
} else {
for (var item in _transferList) {
item.pin = null;
}
}
setState(() {});
},
itemBuilder:
(_) =>
methodsMap.entries.map((entry) {
return PopupMenuItem(
value: entry.key,
child: Text(entry.value),
);
}).toList(),
);
}
String _buildUssdCode(TransferItem item) {
final number = item.number;
final amount = item.amount.toString();
final pin = item.pin ?? '1234';
switch (_selectedMethod) {
case 'yemenMobile':
return '*888*$amount*$number*$pin#';
case 'y':
return '*109*$amount*$number#';
case 'sabafon':
return '*123*$pin*$number*$amount#';
case 'you':
return '*130*$number*$amount#';
default:
return '';
}
}
Future<void> _startTransferSequence() async {
if (_transferList.isEmpty) {
_showSnackBar("لا توجد معاملات للتحويل");
return;
}
if (!await Permission.phone.isGranted) {
final status = await Permission.phone.request();
if (!status.isGranted) {
_showSnackBar("يجب منح إذن الاتصال لإجراء تحويل الرصيد");
return;
}
}
setState(() => _isTransferring = true);
_transferLogs.clear();
for (var i = 0; i < _transferList.length; i++) {
var item = _transferList[i];
String ussdCode = _buildUssdCode(item);
final encodedUssd = Uri.encodeComponent(ussdCode);
final Uri uri = Uri.parse('tel:$encodedUssd');
_showSnackBar(
'جاري تحويل ${_getMethodName(_selectedMethod)}: ${item.amount} إلى ${item.number} (${i + 1}/${_transferList.length})',
);
try {
if (await canLaunchUrl(uri)) {
await launchUrl(uri);
_transferLogs.add({
'number': item.number,
'amount': item.amount,
'success': true,
'method': _selectedMethod,
});
await Future.delayed(const Duration(seconds: 10));
} else {
_transferLogs.add({
'number': item.number,
'amount': item.amount,
'success': false,
'method': _selectedMethod,
'error': 'تعذر تشغيل رمز USSD',
});
}
} catch (e) {
_transferLogs.add({
'number': item.number,
'amount': item.amount,
'success': false,
'method': _selectedMethod,
'error': e.toString(),
});
}
}
setState(() => _isTransferring = false);
_showSnackBar("تمت جميع عمليات التحويل");
}
String _getMethodName(String method) {
switch (method) {
case 'yemenMobile':
return 'يمن موبايل';
case 'y':
return 'واي';
case 'sabafon':
return 'سبأفون';
case 'you':
return 'يو';
default:
return method;
}
}
void _showTransferLogsDialog() {
showDialog(
context: context,
builder: (context) {
return Dialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding: const EdgeInsets.all(16),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'سجل عمليات التحويل',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Theme.of(context).colorScheme.onSurface,
),
),
IconButton(
icon: Icon(Icons.close),
onPressed: () => Navigator.pop(context),
),
],
),
),
const Divider(height: 1),
SizedBox(
height: 300,
width: double.maxFinite,
child:
_transferLogs.isEmpty
? Center(
child: Text(
'لا توجد عمليات تحويل حتى الآن',
style: TextStyle(
color: Theme.of(
context,
).colorScheme.onSurface.withOpacity(0.6),
),
),
)
: ListView.builder(
itemCount: _transferLogs.length,
itemBuilder: (_, i) {
final log = _transferLogs[i];
final success = log['success'] as bool;
return ListTile(
leading: Container(
width: 36,
height: 36,
decoration: BoxDecoration(
color:
success
? Colors.green.shade50
: Colors.red.shade50,
shape: BoxShape.circle,
),
child: Icon(
success ? Icons.check : Icons.error,
color: success ? Colors.green : Colors.red,
),
),
title: Text(
'${_getMethodName(log['method'])} - ${log['number']}',
style: TextStyle(
fontWeight: FontWeight.w500,
color:
Theme.of(context).colorScheme.onSurface,
),
),
subtitle: Text(
'المبلغ: ${log['amount']}',
style: TextStyle(
color: Theme.of(
context,
).colorScheme.onSurface.withOpacity(0.6),
),
),
trailing:
!success
? Tooltip(
message:
log['error'] ?? 'خطأ غير معروف',
child: Icon(
Icons.info_outline,
color: Colors.red,
),
)
: null,
);
},
),
),
Padding(
padding: const EdgeInsets.all(16),
child: ElevatedButton(
onPressed: () => Navigator.pop(context),
child: const Text('إغلاق'),
),
),
],
),
);
},
);
}
void _showAmountsManagementDialog() async {
final amountsController = TextEditingController();
await showDialog(
context: context,
builder: (context) {
return StatefulBuilder(
builder: (context, setDialogState) {
return AlertDialog(
title: const Text('إدارة المبالغ المحفوظة'),
content: SizedBox(
width: double.maxFinite,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
if (_savedAmounts.isEmpty)
const Padding(
padding: EdgeInsets.all(8.0),
child: Text(
'لا توجد مبالغ محفوظة حالياً',
textAlign: TextAlign.center,
),
),
if (_savedAmounts.isNotEmpty)
Wrap(
spacing: 8,
runSpacing: 4,
children:
_savedAmounts.map((amt) {
return Chip(
label: Text('$amt'),
deleteIcon: const Icon(Icons.close),
onDeleted: () {
setDialogState(() {
_savedAmounts.remove(amt);
});
},
);
}).toList(),
),
const SizedBox(height: 16),
TextField(
controller: amountsController,
decoration: InputDecoration(
labelText: 'إضافة مبلغ جديد',
prefixIcon: Icon(Icons.add),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
),
suffixIcon: IconButton(
icon: const Icon(Icons.add_circle_outline),
onPressed: () {
final input = amountsController.text.trim();
final numAmount = int.tryParse(input);
if (numAmount == null || numAmount <= 0) {
_showSnackBar(
'يرجى إدخال مبلغ صحيح وكبير من صفر',
);
return;
}
if (_savedAmounts.contains(numAmount)) {
_showSnackBar('هذا المبلغ موجود بالفعل');
return;
}
setDialogState(() {
_savedAmounts.add(numAmount);
_savedAmounts.sort();
amountsController.clear();
});
},
),
),
keyboardType: TextInputType.number,
onSubmitted: (value) {
final numAmount = int.tryParse(value.trim());
if (numAmount == null || numAmount <= 0) {
_showSnackBar('يرجى إدخال مبلغ صحيح وكبير من صفر');
return;
}
if (_savedAmounts.contains(numAmount)) {
_showSnackBar('هذا المبلغ موجود بالفعل');
return;
}
setDialogState(() {
_savedAmounts.add(numAmount);
_savedAmounts.sort();
amountsController.clear();
});
},
),
],
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('إغلاق'),
),
ElevatedButton(
onPressed: () async {
await _saveSavedAmounts();
Navigator.pop(context);
_showSnackBar('تم حفظ المبالغ بنجاح');
},
child: const Text('حفظ'),
),
],
);
},
);
},
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('تحويل رصيد متعدد'),
actions: [
IconButton(
icon: const Icon(Icons.contacts),
tooltip: 'إدارة الأرقام المحفوظة',
onPressed: _showSavedNumbersSelection,
),
IconButton(
icon: const Icon(Icons.attach_money),
tooltip: 'إدارة المبالغ المحفوظة',
onPressed: _showAmountsManagementDialog,
),
_buildTransferMethodMenu(),
],
),
floatingActionButton: FloatingActionButton.extended(
icon: const Icon(Icons.send_rounded),
label:
_isTransferring
? Row(
mainAxisSize: MainAxisSize.min,
children: const [
SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
color: Colors.white,
strokeWidth: 2,
),
),
SizedBox(width: 8),
Text('جار التحويل...'),
],
)
: const Text('إجراء التحويلات'),
onPressed: _isTransferring ? null : _startTransferSequence,
),
body: Column(
children: [
Card(
margin: const EdgeInsets.all(12),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: [
Row(
children: [
Icon(
Icons.info_outline,
color: Theme.of(context).colorScheme.primary,
),
const SizedBox(width: 8),
Text(
'طريقة التحويل الحالية',
style: TextStyle(
fontWeight: FontWeight.bold,
color: Theme.of(context).colorScheme.onSurface,
),
),
],
),
const SizedBox(height: 8),
Text(
_getMethodName(_selectedMethod),
style: TextStyle(
fontSize: 16,
color: Theme.of(
context,
).colorScheme.onSurface.withOpacity(0.8),
),
),
const SizedBox(height: 12),
ElevatedButton.icon(
icon: const Icon(Icons.refresh),
label: const Text('تحديث قائمة التحويل'),
onPressed: () async {
await _addTransfersFromSavedNumbers();
_showSnackBar('تم تحديث قائمة التحويل بالأرقام المحفوظة');
},
),
],
),
),
),
// ------------- عرض المختصر (Chips) لجهات الاتصال المحددة --------------------
if (_savedNumbers.isNotEmpty)
Container(
margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
alignment: Alignment.centerRight,
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children:
_savedNumbers.map((number) {
return FutureBuilder<String?>(
future: _getContactNameFromNumber(number),
builder: (context, snapshot) {
final displayName = snapshot.data ?? number;
return Padding(
padding: const EdgeInsets.only(left: 6),
child: Chip(
label: Text(
displayName,
style: TextStyle(
color:
Theme.of(context)
.chipTheme
.secondaryLabelStyle
?.color ??
Colors.white,
),
),
deleteIcon: Icon(
Icons.close,
color: Colors.white,
),
onDeleted: () async {
setState(() {
_savedNumbers.remove(number);
});
await _saveSavedNumbers();
_transferList.removeWhere(
(item) => item.number == number,
);
await _saveTransferList();
_showSnackBar('تم إزالة $displayName');
},
backgroundColor:
Theme.of(context).chipTheme.selectedColor,
elevation: 1,
),
);
},
);
}).toList(),
),
),
),
const Divider(height: 1),
Expanded(
child:
_transferList.isEmpty
? Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.phone_iphone_outlined,
size: 64,
color: Theme.of(
context,
).colorScheme.onSurface.withOpacity(0.3),
),
const SizedBox(height: 16),
Text(
'لا توجد عمليات تحويل',
style: TextStyle(
fontSize: 18,
color: Theme.of(
context,
).colorScheme.onSurface.withOpacity(0.6),
),
),
const SizedBox(height: 8),
Text(
'أضف الأرقام المحفوظة أولاً',
style: TextStyle(
color: Theme.of(
context,
).colorScheme.onSurface.withOpacity(0.5),
),
),
],
),
)
: ListView.builder(
itemCount: _transferList.length,
itemBuilder: (context, index) {
final item = _transferList[index];
return Dismissible(
key: Key('${item.number}_$index'),
background: Container(
margin: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 6,
),
decoration: BoxDecoration(
color: Colors.red.shade50,
borderRadius: BorderRadius.circular(16),
),
alignment: Alignment.centerRight,
padding: const EdgeInsets.only(right: 24),
child: Icon(
Icons.delete,
color: Colors.red.shade400,
),
),
onDismissed: (direction) => _removeTransferAt(index),
child: Card(
margin: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 6,
),
child: ListTile(
leading: Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: Theme.of(
context,
).colorScheme.primary.withOpacity(0.1),
shape: BoxShape.circle,
),
child: Icon(
Icons.phone_android,
color: Theme.of(context).colorScheme.primary,
),
),
title: Text(
item.number,
style: TextStyle(
fontWeight: FontWeight.w500,
color:
Theme.of(context).colorScheme.onSurface,
),
),
subtitle: Text(
'المبلغ: ${item.amount}',
style: TextStyle(
color: Theme.of(
context,
).colorScheme.onSurface.withOpacity(0.6),
),
),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: Icon(
Icons.edit,
color:
Theme.of(context).colorScheme.primary,
),
onPressed: () => _editTransfer(index),
tooltip: 'تعديل',
),
IconButton(
icon: Icon(
Icons.delete_forever,
color: Colors.red.shade400,
),
onPressed: () => _removeTransferAt(index),
tooltip: 'حذف',
),
],
),
),
),
);
},
),
),
if (_transferLogs.isNotEmpty)
Padding(
padding: const EdgeInsets.all(12),
child: ElevatedButton.icon(
icon: const Icon(Icons.history),
label: const Text('عرض سجل التحويلات'),
onPressed: _showTransferLogsDialog,
),
),
],
),
);
}
}
ملاحظة اريد الكود الكامل دفعة واحدة لاقوم باستخدامة مباشرةASSISTANT
قمت بتحسين عرض جهات الاتصال المحددة لتكون أكثر ترتيباً وجمالاً، كما أضفت شاشة تظهر عند الضغط على زر "تحديث قائمة التحويل"، تتيح عرض الأرقام المُحددة مع إمكانية تعديل المبلغ لكل جهة اتصال قبل الإضافة دفعة واحدة إلى قائمة التحويل. وهذا يجعل العملية أكثر سلاسة ويسمح بتحديد المبالغ قبل الإضافة.
الكود كامل مع التعديلات المطلوبة مع الشرح الداخلي في التعليقات:
```dart
import 'dart:async';
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:telephony/telephony.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:flutter_contacts/flutter_contacts.dart';
import 'package:url_launcher/url_launcher.dart';
void main() => runApp(SMSApp());
void onBackgroundMessage(SmsMessage message) {
debugPrint("رسالة واردة في الخلفية: ${message.body}");
}
class SMSApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'تحويل رصيد متعدد',
debugShowCheckedModeBanner: false,
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(
seedColor: const Color(0xFF1A73E8),
primary: const Color(0xFF1A73E8),
secondary: const Color(0xFF4285F4),
tertiary: const Color(0xFF34A853),
background: const Color(0xFFF8F9FA),
surface: Colors.white,
onPrimary: Colors.white,
onSecondary: Colors.white,
onBackground: const Color(0xFF202124),
onSurface: const Color(0xFF202124),
),
useMaterial3: true,
fontFamily: 'Tajawal',
appBarTheme: AppBarTheme(
backgroundColor: const Color(0xFF1A73E8),
foregroundColor: Colors.white,
elevation: 0,
centerTitle: true,
titleTextStyle: const TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
),
iconTheme: const IconThemeData(color: Colors.white),
),
cardTheme: CardTheme(
color: Colors.white,
elevation: 2,
margin: const EdgeInsets.all(8),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
surfaceTintColor: Colors.white,
),
elevatedButtonTheme: ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF1A73E8),
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
elevation: 0,
textStyle: const TextStyle(
fontWeight: FontWeight.w600,
fontSize: 16,
),
),
),
outlinedButtonTheme: OutlinedButtonThemeData(
style: OutlinedButton.styleFrom(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
side: const BorderSide(width: 1.5, color: Color(0xFF1A73E8)),
textStyle: const TextStyle(fontWeight: FontWeight.w600),
),
),
textButtonTheme: TextButtonThemeData(
style: TextButton.styleFrom(
foregroundColor: const Color(0xFF1A73E8),
textStyle: const TextStyle(fontWeight: FontWeight.w600),
),
),
snackBarTheme: SnackBarThemeData(
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
backgroundColor: const Color(0xFF1A73E8),
contentTextStyle: const TextStyle(color: Colors.white),
),
inputDecorationTheme: InputDecorationTheme(
filled: true,
fillColor: Colors.grey.shade50,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide.none,
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: const BorderSide(color: Color(0xFF1A73E8), width: 2),
),
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 14,
),
),
dialogTheme: DialogTheme(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
backgroundColor: Colors.white,
elevation: 8,
surfaceTintColor: Colors.white,
),
listTileTheme: ListTileThemeData(
iconColor: const Color(0xFF1A73E8),
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
dividerTheme: DividerThemeData(
color: Colors.grey.shade200,
thickness: 1,
space: 0,
),
progressIndicatorTheme: const ProgressIndicatorThemeData(
color: Color(0xFF1A73E8),
),
chipTheme: ChipThemeData(
backgroundColor: Colors.grey.shade100,
selectedColor: const Color(0xFF1A73E8),
secondarySelectedColor: const Color(0xFF1A73E8),
disabledColor: Colors.grey.shade300,
labelStyle: const TextStyle(color: Colors.black),
secondaryLabelStyle: const TextStyle(color: Colors.white),
brightness: Brightness.light,
padding: EdgeInsets.symmetric(horizontal: 12, vertical: 4),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
),
),
home: SMSHomePage(),
);
}
}
class TransferItem {
String number;
int amount;
String? pin;
TransferItem({required this.number, required this.amount, this.pin});
Map<String, dynamic> toMap() {
return {'number': number, 'amount': amount, 'pin': pin};
}
factory TransferItem.fromMap(Map<String, dynamic> map) {
return TransferItem(
number: map['number'],
amount: map['amount'],
pin: map['pin'],
);
}
}
extension ContactExtensions on Contact {
String initials() {
if (displayName.isEmpty) return '';
final parts = displayName.trim().split(RegExp(r'\s+'));
if (parts.length == 1) {
return parts[0].substring(0, 1).toUpperCase();
} else {
return (parts[0].substring(0, 1) + parts[1].substring(0, 1))
.toUpperCase();
}
}
}
// المساحة التخزينية المؤقتة لتحفظ مبالغ مؤقتاً في شاشة التحديث
class NumberAmount {
String number;
String? displayName;
int amount;
NumberAmount({required this.number, this.displayName, required this.amount});
}
class SMSHomePage extends StatefulWidget {
@override
_SMSHomePageState createState() => _SMSHomePageState();
}
class _SMSHomePageState extends State<SMSHomePage> with WidgetsBindingObserver {
final Telephony _telephony = Telephony.instance;
SharedPreferences? _prefs;
List<TransferItem> _transferList = [];
String _selectedMethod = 'yemenMobile';
bool _isTransferring = false;
List<Map<String, dynamic>> _transferLogs = [];
List<String> _savedNumbers = [];
List<int> _savedAmounts = [];
bool _didShowFavoritesDialog = false;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
_initApp();
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
Future<void> _initApp() async {
_prefs = await SharedPreferences.getInstance();
// Load amounts first
List<String>? amountsStr = _prefs?.getStringList('savedAmounts');
if (amountsStr != null) {
_savedAmounts =
amountsStr.map((e) => int.tryParse(e) ?? 0).where((e) => e > 0).toList();
}
// Show dialog to input amounts if empty, only once per run
if (_savedAmounts.isEmpty && !_didShowFavoritesDialog) {
_didShowFavoritesDialog = true;
await Future.delayed(Duration.zero);
await _showFavoritesDialog();
}
List<String>? savedTransfers = _prefs?.getStringList('transferList');
if (savedTransfers != null && savedTransfers.isNotEmpty) {
_transferList = savedTransfers
.map(
(e) => TransferItem.fromMap(
json.decode(e) as Map<String, dynamic>,
),
)
.toList();
} else {
_transferList = [];
}
_selectedMethod = _prefs?.getString('selectedMethod') ?? 'yemenMobile';
_savedNumbers = _prefs?.getStringList('savedNumbers') ?? [];
await _requestPermissions();
if (mounted) setState(() {});
}
Future<bool> _requestPermissions() async {
final phoneStatus = await Permission.phone.status;
final contactsStatus = await Permission.contacts.status;
if (!phoneStatus.isGranted || !contactsStatus.isGranted) {
final status = await [Permission.phone, Permission.contacts].request();
if (!status[Permission.phone]!.isGranted ||
!status[Permission.contacts]!.isGranted) {
_showSnackBar(
'يجب منح أذونات المكالمات وجهات الاتصال لاستخدام التطبيق',
);
return false;
}
}
return true;
}
Future<void> _saveTransferList() async {
final list = _transferList.map((e) => json.encode(e.toMap())).toList();
await _prefs?.setStringList('transferList', list);
}
Future<void> _saveSelectedMethod() async {
await _prefs?.setString('selectedMethod', _selectedMethod);
}
Future<void> _saveSavedNumbers() async {
await _prefs?.setStringList('savedNumbers', _savedNumbers);
}
Future<void> _saveSavedAmounts() async {
List<String> strList = _savedAmounts.map((e) => e.toString()).toList();
await _prefs?.setStringList('savedAmounts', strList);
}
void _showSnackBar(String msg) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(msg),
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
),
);
}
Future<String?> _getContactNameFromNumber(String number) async {
final hasPermission = await Permission.contacts.status;
if (!hasPermission.isGranted) {
final status = await Permission.contacts.request();
if (!status.isGranted) return null;
}
final contacts = await FlutterContacts.getContacts(withProperties: true);
for (var contact in contacts) {
for (var phone in contact.phones) {
final normalizedPhone = phone.number.replaceAll(RegExp(r'[^\d+]'), '');
final normalizedNumber = number.replaceAll(RegExp(r'[^\d+]'), '');
if (normalizedPhone == normalizedNumber) {
return contact.displayName;
}
}
}
return null;
}
Future<void> _showFavoritesDialog() async {
final amountsController = TextEditingController();
await showDialog(
context: context,
barrierDismissible: false,
builder: (context) {
return StatefulBuilder(
builder: (context, setDialogState) {
return AlertDialog(
title: const Text('إعداد المبالغ المفضلة'),
content: SizedBox(
width: double.maxFinite,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
if (_savedAmounts.isEmpty)
const Padding(
padding: EdgeInsets.all(8.0),
child: Text(
'الرجاء إدخال المبالغ التي ترغب باستخدامها لاختيارها عند إضافة جهة اتصال',
textAlign: TextAlign.center,
),
),
if (_savedAmounts.isNotEmpty)
Wrap(
spacing: 8,
runSpacing: 4,
children: _savedAmounts.map((amt) {
return Chip(
label: Text('$amt'),
deleteIcon: const Icon(Icons.close),
onDeleted: () {
setDialogState(() {
_savedAmounts.remove(amt);
});
},
);
}).toList(),
),
const SizedBox(height: 16),
TextField(
controller: amountsController,
decoration: InputDecoration(
labelText: 'إضافة مبلغ جديد',
prefixIcon: const Icon(Icons.add),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
),
suffixIcon: IconButton(
icon: const Icon(Icons.add_circle_outline),
onPressed: () {
final input = amountsController.text.trim();
final numAmount =
int.tryParse(input);
if (numAmount == null || numAmount <= 0) {
_showSnackBar(
'يرجى إدخال مبلغ صحيح وكبير من صفر',
);
return;
}
if (_savedAmounts.contains(numAmount)) {
_showSnackBar('هذا المبلغ موجود بالفعل');
return;
}
setDialogState(() {
_savedAmounts.add(numAmount);
_savedAmounts.sort();
amountsController.clear();
});
},
),
),
keyboardType: TextInputType.number,
onSubmitted: (value) {
final numAmount =
int.tryParse(value.trim());
if (numAmount == null || numAmount <= 0) {
_showSnackBar(
'يرجى إدخال مبلغ صحيح وكبير من صفر');
return;
}
if (_savedAmounts.contains(numAmount)) {
_showSnackBar('هذا المبلغ موجود بالفعل');
return;
}
setDialogState(() {
_savedAmounts.add(numAmount);
_savedAmounts.sort();
amountsController.clear();
});
},
),
],
),
),
actions: [
TextButton(
onPressed: () {
Navigator.pop(context);
},
child: const Text('تخطي'),
),
ElevatedButton(
onPressed: () async {
if (_savedAmounts.isEmpty) {
_showSnackBar('الرجاء إدخال مبلغ واحد على الأقل');
return;
}
await _saveSavedAmounts();
Navigator.pop(context);
},
child: const Text('حفظ'),
),
],
);
},
);
},
);
}
Future<int?> _pickAmountDialog() async {
if (_savedAmounts.isEmpty) {
_showSnackBar('لا توجد مبالغ محفوظة. الرجاء إضافتها في الإعدادات.');
return null;
}
int? selectedAmount;
await showModalBottomSheet(
context: context,
builder: (context) {
return SafeArea(
child: Container(
padding: const EdgeInsets.all(16),
child: Wrap(
spacing: 12,
children: _savedAmounts.map((amount) {
return ChoiceChip(
label: Text('$amount'),
selected: selectedAmount == amount,
onSelected: (_) {
selectedAmount = amount;
Navigator.pop(context);
},
);
}).toList(),
),
),
);
},
);
return selectedAmount;
}
void _showSavedNumbersSelection() async {
final hasPermission = await _requestPermissions();
if (!hasPermission) return;
final contacts = await FlutterContacts.getContacts(
withProperties: true,
withThumbnail: true,
);
List<String> selectedNumbers = List.from(_savedNumbers);
TextEditingController searchController = TextEditingController();
String searchQuery = '';
showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (context) {
return StatefulBuilder(
builder: (context, setModal) {
final filteredContacts = (searchQuery.isEmpty)
? contacts
: contacts.where((contact) {
final contactNameLower =
contact.displayName.toLowerCase();
final queryLower = searchQuery.toLowerCase();
final phonesMatch = contact.phones.any(
(phone) =>
phone.number.toLowerCase().contains(queryLower),
);
return contactNameLower.contains(queryLower) || phonesMatch;
}).toList();
return Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
borderRadius: const BorderRadius.vertical(
top: Radius.circular(24),
),
),
height: MediaQuery.of(context).size.height * 0.75,
padding: EdgeInsets.only(
bottom: MediaQuery.of(context).viewInsets.bottom,
),
child: Column(
children: [
Padding(
padding: const EdgeInsets.all(16),
child: Row(
children: [
Expanded(
child: TextField(
controller: searchController,
decoration: InputDecoration(
prefixIcon: const Icon(Icons.search),
hintText: 'ابحث في جهات الاتصال أو الأرقام',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
),
isDense: true,
contentPadding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 12,
),
),
onChanged: (value) {
setModal(() {
searchQuery = value.trim();
});
},
),
),
IconButton(
icon: Icon(
Icons.close,
color: Theme.of(context).colorScheme.onSurface,
),
onPressed: () => Navigator.pop(context),
),
],
),
),
const Divider(height: 1),
Expanded(
child: filteredContacts.isEmpty
? Center(
child: Text(
'لا توجد جهات اتصال تطابق البحث',
style: TextStyle(
color: Theme.of(context)
.colorScheme
.onSurface
.withOpacity(0.6),
),
),
)
: ListView.builder(
itemCount: filteredContacts.length,
itemBuilder: (context, index) {
final contact = filteredContacts[index];
final phones = contact.phones;
return Card(
margin: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 6,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
elevation: 3,
child: Padding(
padding: const EdgeInsets.symmetric(
vertical: 8,
horizontal: 12,
),
child: Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
ListTile(
contentPadding: EdgeInsets.zero,
leading: CircleAvatar(
radius: 24,
backgroundImage:
(contact.thumbnail != null &&
contact.thumbnail!.isNotEmpty)
? MemoryImage(
contact.thumbnail!,
)
: null,
child: (contact.thumbnail == null ||
contact.thumbnail!.isEmpty)
? Text(
contact.initials(),
style: TextStyle(
fontWeight: FontWeight.bold,
color: Theme.of(context)
.colorScheme
.onPrimary,
),
)
: null,
backgroundColor: Theme.of(context)
.colorScheme
.primary,
),
title: Text(
contact.displayName,
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
),
const SizedBox(height: 8),
Column(
children: phones.map((phone) {
final normalized = phone.number.replaceAll(
RegExp(r'[^\d+]'),
'',
);
final isSelected = selectedNumbers.any((num) {
final n = num.replaceAll(
RegExp(r'[^\d+]'),
'',
);
return n == normalized;
});
return CheckboxListTile(
contentPadding: EdgeInsets.zero,
value: isSelected,
title: Text(phone.number),
controlAffinity:
ListTileControlAffinity.leading,
onChanged: (val) {
setModal(() {
if (val == true) {
if (!selectedNumbers.any(
(num) {
final n = num.replaceAll(
RegExp(r'[^\d+]'),
'',
);
return n == normalized;
},
)) {
selectedNumbers.add(phone.number);
}
} else {
selectedNumbers.removeWhere((num) {
final n = num.replaceAll(
RegExp(r'[^\d+]'),
'',
);
return n == normalized;
});
}
});
},
);
}).toList(),
),
],
),
),
);
},
),
),
Padding(
padding: const EdgeInsets.all(16),
child: Row(
children: [
Expanded(
child: OutlinedButton(
onPressed: () => Navigator.pop(context),
child: const Text('إلغاء'),
),
),
const SizedBox(width: 12),
Expanded(
child: ElevatedButton(
onPressed: () async {
_savedNumbers = List.from(selectedNumbers);
await _saveSavedNumbers();
Navigator.pop(context);
_showSnackBar('تم حفظ الأرقام المختارة');
},
child: const Text('حفظ'),
),
),
],
),
),
],
),
);
},
);
},
);
}
// الدالة الجديدة: تعرض شاشة اختيار مبلغ لكل رقم من الأرقام المختارة عند تحديث القائمة دفعة واحدة
Future<void> _showBatchAmountAssignmentDialog(List<String> numbers) async {
if (numbers.isEmpty) {
_showSnackBar('لا توجد أرقام لتحديثها');
return;
}
if (_savedAmounts.isEmpty) {
_showSnackBar('لا توجد مبالغ محفوظة. الرجاء إضافتها في الإعدادات.');
return;
}
// تحميل أسماء جهات الاتصال (ملائمة للعرض)
List<NumberAmount> tempList = [];
for (var number in numbers) {
String? name = await _getContactNameFromNumber(number);
tempList.add(NumberAmount(number: number, displayName: name, amount: 0));
}
final formKey = GlobalKey<FormState>();
await showDialog(
context: context,
barrierDismissible: false,
builder: (context) {
return StatefulBuilder(builder: (context, setDialogState) {
return Dialog(
shape:
RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
child: Container(
constraints: BoxConstraints(
maxHeight: MediaQuery.of(context).size.height * 0.7,
minWidth: 300,
maxWidth: 400,
),
padding: const EdgeInsets.all(16),
child: Column(
children: [
Row(
children: [
Expanded(
child: Text(
'تعيين مبلغ لكل رقم',
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 18,
color: Theme.of(context).colorScheme.onSurface,
),
),
),
IconButton(
icon: Icon(Icons.close),
onPressed: () {
Navigator.pop(context);
},
),
],
),
const Divider(),
Expanded(
child: Form(
key: formKey,
child: ListView.separated(
shrinkWrap: true,
itemCount: tempList.length,
separatorBuilder: (_, __) => const Divider(),
itemBuilder: (context, index) {
final item = tempList[index];
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
item.displayName ?? item.number,
style: const TextStyle(
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 6),
// اختيار مبلغ عن طريق Dropdown من المبالغ المحفوظة
DropdownButtonFormField<int>(
decoration: InputDecoration(
labelText: 'المبلغ',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
),
contentPadding: const EdgeInsets.symmetric(
horizontal: 12, vertical: 14),
),
items: _savedAmounts
.map((e) =>
DropdownMenuItem(value: e, child: Text('$e')))
.toList(),
value: item.amount > 0 ? item.amount : null,
onChanged: (val) {
setDialogState(() {
item.amount = val ?? 0;
});
},
validator: (val) {
if (val == null || val <= 0) {
return 'يرجى اختيار مبلغ صالح';
}
return null;
},
isExpanded: true,
),
],
);
},
),
),
),
const SizedBox(height: 12),
Row(
children: [
Expanded(
child: OutlinedButton(
onPressed: () => Navigator.pop(context),
child: const Text('إلغاء'),
),
),
const SizedBox(width: 12),
Expanded(
child: ElevatedButton(
onPressed: () async {
if (!formKey.currentState!.validate()) {
_showSnackBar('يرجى إدخال جميع المبالغ بشكل صحيح');
return;
}
// إضافة أو تحديث القائمة _transferList دفعة واحدة
for (var item in tempList) {
if (!_transferList.any((e) => e.number == item.number)) {
_transferList.add(TransferItem(
number: item.number,
amount: item.amount,
pin:
(_selectedMethod == 'yemenMobile' ||
_selectedMethod == 'sabafon')
? '1234'
: null,
));
} else {
final existingIndex = _transferList.indexWhere(
(e) => e.number == item.number);
if (existingIndex != -1) {
_transferList[existingIndex].amount = item.amount;
if (_selectedMethod == 'yemenMobile' ||
_selectedMethod == 'sabafon') {
_transferList[existingIndex].pin ??= '1234';
} else {
_transferList[existingIndex].pin = null;
}
}
}
}
await _saveTransferList();
setState(() {});
Navigator.pop(context);
_showSnackBar('تم تحديث قائمة التحويل بالمبالغ المحددة');
},
child: const Text('حفظ'),
),
),
],
),
],
),
),
);
});
},
);
}
// الدالة المعدلة: عند الضغط على زر تحديث قائمة التحويل
Future<void> _onUpdateTransferListPressed() async {
if (_savedNumbers.isEmpty) {
_showSnackBar('لا توجد أرقام محفوظة. الرجاء إضافتها أولاً.');
return;
}
await _showBatchAmountAssignmentDialog(_savedNumbers);
}
void _removeTransferAt(int index) async {
setState(() {
_transferList.removeAt(index);
});
await _saveTransferList();
}
void _editTransfer(int index) {
var item = _transferList[index];
int? selectedAmount = item.amount;
final pinController = TextEditingController(text: item.pin ?? '1234');
showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (context) {
return StatefulBuilder(
builder: (context, setModal) {
return Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
borderRadius: const BorderRadius.vertical(
top: Radius.circular(24),
),
),
padding: EdgeInsets.only(
bottom: MediaQuery.of(context).viewInsets.bottom,
top: 16,
left: 16,
right: 16,
),
child: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"تعديل التحويل",
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 18,
color: Theme.of(context).colorScheme.onSurface,
),
),
IconButton(
icon: Icon(
Icons.close,
color: Theme.of(context).colorScheme.onSurface,
),
onPressed: () => Navigator.pop(context),
),
],
),
const SizedBox(height: 8),
Text(
item.number,
style: TextStyle(
color: Theme.of(
context,
).colorScheme.onSurface.withOpacity(0.7),
fontWeight: FontWeight.w600,
),
),
const Divider(height: 24),
Align(
alignment: Alignment.centerRight,
child: Text(
'اختر المبلغ',
style: TextStyle(
color: Theme.of(context).colorScheme.onSurface,
fontWeight: FontWeight.w600,
),
),
),
const SizedBox(height: 8),
Wrap(
spacing: 8,
children: _savedAmounts.map((amt) {
return ChoiceChip(
label: Text('$amt'),
selected: selectedAmount == amt,
onSelected: (_) {
setModal(() {
selectedAmount = amt;
});
},
);
}).toList(),
),
if (_selectedMethod == 'yemenMobile' || _selectedMethod == 'sabafon')
Padding(
padding: const EdgeInsets.fromLTRB(0, 24, 0, 24),
child: TextField(
controller: pinController,
keyboardType: TextInputType.number,
decoration: InputDecoration(
labelText: "الرقم السري",
prefixIcon: Icon(Icons.lock_outline),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
),
),
obscureText: true,
),
)
else
const SizedBox(height: 24),
Row(
children: [
Expanded(
child: OutlinedButton(
onPressed: () => Navigator.pop(context),
child: const Text("إلغاء"),
),
),
const SizedBox(width: 12),
Expanded(
child: ElevatedButton(
onPressed: () async {
if (selectedAmount == null || selectedAmount! <= 0) {
_showSnackBar(
'يرجى اختيار مبلغ صحيح أكبر من صفر',
);
return;
}
setState(() {
_transferList[index].amount = selectedAmount!;
if (_selectedMethod == 'yemenMobile' ||
_selectedMethod == 'sabafon') {
final pinStr = pinController.text.trim();
_transferList[index].pin =
pinStr.isEmpty ? '1234' : pinStr;
} else {
_transferList[index].pin = null;
}
});
await _saveTransferList();
Navigator.pop(context);
},
child: const Text("حفظ"),
),
),
],
),
],
),
),
);
},
);
},
);
}
Widget _buildTransferMethodMenu() {
final Map<String, String> methodsMap = {
'yemenMobile': 'يمن موبايل',
'y': 'واي',
'sabafon': 'سبأفون',
'you': 'يو',
};
return PopupMenuButton<String>(
icon: const Icon(Icons.compare_arrows),
tooltip: 'طريقة التحويل',
onSelected: (value) async {
setState(() => _selectedMethod = value);
await _saveSelectedMethod();
if (_selectedMethod == 'yemenMobile' || _selectedMethod == 'sabafon') {
for (var item in _transferList) {
item.pin ??= '1234';
}
} else {
for (var item in _transferList) {
item.pin = null;
}
}
setState(() {});
},
itemBuilder: (_) => methodsMap.entries
.map((entry) => PopupMenuItem(
value: entry.key,
child: Text(entry.value),
))
.toList(),
);
}
String _buildUssdCode(TransferItem item) {
final number = item.number;
final amount = item.amount.toString();
final pin = item.pin ?? '1234';
switch (_selectedMethod) {
case 'yemenMobile':
return '*888*$amount*$number*$pin#';
case 'y':
return '*109*$amount*$number#';
case 'sabafon':
return '*123*$pin*$number*$amount#';
case 'you':
return '*130*$number*$amount#';
default:
return '';
}
}
Future<void> _startTransferSequence() async {
if (_transferList.isEmpty) {
_showSnackBar("لا توجد معاملات للتحويل");
return;
}
if (!await Permission.phone.isGranted) {
final status = await Permission.phone.request();
if (!status.isGranted) {
_showSnackBar("يجب منح إذن الاتصال لإجراء تحويل الرصيد");
return;
}
}
setState(() => _isTransferring = true);
_transferLogs.clear();
for (var i = 0; i < _transferList.length; i++) {
var item = _transferList[i];
String ussdCode = _buildUssdCode(item);
final encodedUssd = Uri.encodeComponent(ussdCode);
final Uri uri = Uri.parse('tel:$encodedUssd');
_showSnackBar(
'جاري تحويل ${_getMethodName(_selectedMethod)}: ${item.amount} إلى ${item.number} (${i + 1}/${_transferList.length})',
);
try {
if (await canLaunchUrl(uri)) {
await launchUrl(uri);
_transferLogs.add({
'number': item.number,
'amount': item.amount,
'success': true,
'method': _selectedMethod,
});
await Future.delayed(const Duration(seconds: 10));
} else {
_transferLogs.add({
'number': item.number,
'amount': item.amount,
'success': false,
'method': _selectedMethod,
'error': 'تعذر تشغيل رمز USSD',
});
}
} catch (e) {
_transferLogs.add({
'number': item.number,
'amount': item.amount,
'success': false,
'method': _selectedMethod,
'error': e.toString(),
});
}
}
setState(() => _isTransferring = false);
_showSnackBar("تمت جميع عمليات التحويل");
}
String _getMethodName(String method) {
switch (method) {
case 'yemenMobile':
return 'يمن موبايل';
case 'y':
return 'واي';
case 'sabafon':
return 'سبأفون';
case 'you':
return 'يو';
default:
return method;
}
}
void _showTransferLogsDialog() {
showDialog(
context: context,
builder: (context) {
return Dialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding: const EdgeInsets.all(16),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'سجل عمليات التحويل',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Theme.of(context).colorScheme.onSurface,
),
),
IconButton(
icon: const Icon(Icons.close),
onPressed: () => Navigator.pop(context),
),
],
),
),
const Divider(height: 1),
SizedBox(
height: 300,
width: double.maxFinite,
child: _transferLogs.isEmpty
? Center(
child: Text(
'لا توجد عمليات تحويل حتى الآن',
style: TextStyle(
color:
Theme.of(context).colorScheme.onSurface.withOpacity(0.6),
),
),
)
: ListView.builder(
itemCount: _transferLogs.length,
itemBuilder: (_, i) {
final log = _transferLogs[i];
final success = log['success'] as bool;
return ListTile(
leading: Container(
width: 36,
height: 36,
decoration: BoxDecoration(
color: success ? Colors.green.shade50 : Colors.red.shade50,
shape: BoxShape.circle,
),
child: Icon(
success ? Icons.check : Icons.error,
color: success ? Colors.green : Colors.red,
),
),
title: Text(
'${_getMethodName(log['method'])} - ${log['number']}',
style: TextStyle(
fontWeight: FontWeight.w500,
color: Theme.of(context).colorScheme.onSurface,
),
),
subtitle: Text(
'المبلغ: ${log['amount']}',
style: TextStyle(
color: Theme.of(context)
.colorScheme
.onSurface
.withOpacity(0.6),
),
),
trailing: !success
? Tooltip(
message: log['error'] ?? 'خطأ غير معروف',
child: const Icon(
Icons.info_outline,
color: Colors.red,
),
)
: null,
);
},
),
),
Padding(
padding: const EdgeInsets.all(16),
child: ElevatedButton(
onPressed: () => Navigator.pop(context),
child: const Text('إغلاق'),
),
),
],
),
);
},
);
}
void _showAmountsManagementDialog() async {
final amountsController = TextEditingController();
await showDialog(
context: context,
builder: (context) {
return StatefulBuilder(
builder: (context, setDialogState) {
return AlertDialog(
title: const Text('إدارة المبالغ المحفوظة'),
content: SizedBox(
width: double.maxFinite,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
if (_savedAmounts.isEmpty)
const Padding(
padding: EdgeInsets.all(8.0),
child: Text(
'لا توجد مبالغ محفوظة حالياً',
textAlign: TextAlign.center,
),
),
if (_savedAmounts.isNotEmpty)
Wrap(
spacing: 8,
runSpacing: 4,
children: _savedAmounts.map((amt) {
return Chip(
label: Text('$amt'),
deleteIcon: const Icon(Icons.close),
onDeleted: () {
setDialogState(() {
_savedAmounts.remove(amt);
});
},
);
}).toList(),
),
const SizedBox(height: 16),
TextField(
controller: amountsController,
decoration: InputDecoration(
labelText: 'إضافة مبلغ جديد',
prefixIcon: const Icon(Icons.add),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
),
suffixIcon: IconButton(
icon: const Icon(Icons.add_circle_outline),
onPressed: () {
final input = amountsController.text.trim();
final numAmount = int.tryParse(input);
if (numAmount == null || numAmount <= 0) {
_showSnackBar(
'يرجى إدخال مبلغ صحيح وكبير من صفر',
);
return;
}
if (_savedAmounts.contains(numAmount)) {
_showSnackBar('هذا المبلغ موجود بالفعل');
return;
}
setDialogState(() {
_savedAmounts.add(numAmount);
_savedAmounts.sort();
amountsController.clear();
});
},
),
),
keyboardType: TextInputType.number,
onSubmitted: (value) {
final numAmount = int.tryParse(value.trim());
if (numAmount == null || numAmount <= 0) {
_showSnackBar('يرجى إدخال مبلغ صحيح وكبير من صفر');
return;
}
if (_savedAmounts.contains(numAmount)) {
_showSnackBar('هذا المبلغ موجود بالفعل');
return;
}
setDialogState(() {
_savedAmounts.add(numAmount);
_savedAmounts.sort();
amountsController.clear();
});
},
),
],
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('إغلاق'),
),
ElevatedButton(
onPressed: () async {
await _saveSavedAmounts();
Navigator.pop(context);
_showSnackBar('تم حفظ المبالغ بنجاح');
},
child: const Text('حفظ'),
),
],
);
},
);
},
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('تحويل رصيد متعدد'),
actions: [
IconButton(
icon: const Icon(Icons.contacts),
tooltip: 'إدارة الأرقام المحفوظة',
onPressed: _showSavedNumbersSelection,
),
IconButton(
icon: const Icon(Icons.attach_money),
tooltip: 'إدارة المبالغ المحفوظة',
onPressed: _showAmountsManagementDialog,
),
_buildTransferMethodMenu(),
],
),
floatingActionButton: FloatingActionButton.extended(
icon: const Icon(Icons.send_rounded),
label: _isTransferring
? Row(
mainAxisSize: MainAxisSize.min,
children: const [
SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
color: Colors.white,
strokeWidth: 2,
),
),
SizedBox(width: 8),
Text('جار التحويل...'),
],
)
: const Text('إجراء التحويلات'),
onPressed: _isTransferring ? null : _startTransferSequence,
),
body: Column(
children: [
Card(
margin: const EdgeInsets.all(12),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: [
Row(
children: [
Icon(
Icons.info_outline,
color: Theme.of(context).colorScheme.primary,
),
const SizedBox(width: 8),
Text(
'طريقة التحويل الحالية',
style: TextStyle(
fontWeight: FontWeight.bold,
color: Theme.of(context).colorScheme.onSurface,
),
),
],
),
const SizedBox(height: 8),
Text(
_getMethodName(_selectedMethod),
style: TextStyle(
fontSize: 16,
color:
Theme.of(context).colorScheme.onSurface.withOpacity(0.8),
),
),
const SizedBox(height: 12),
ElevatedButton.icon(
icon: const Icon(Icons.refresh),
label: const Text('تحديث قائمة التحويل'),
onPressed: _onUpdateTransferListPressed,
),
],
),
),
),
// عرض جهات الاتصال المختارة بشكل شبكي (Grid) أكثر جمالية وتنظيم بدلاً من السطر الواحد
if (_savedNumbers.isNotEmpty)
Container(
margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
child: FutureBuilder<List<Map<String, String>>>(
// تجهيز بيانات الأسماء للأرقام المختارة
future: Future.wait(_savedNumbers.map((number) async {
final name = await _getContactNameFromNumber(number);
return {'number': number, 'name': name ?? number};
}).toList()),
builder: (context, snapshot) {
if (!snapshot.hasData) {
return const SizedBox(
height: 40,
child: Center(child: CircularProgressIndicator()),
);
}
final data = snapshot.data!;
// عرض شبكة مختارة من Chips مع حذف
return GridView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: data.length,
gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: 200,
mainAxisExtent: 40,
mainAxisSpacing: 8,
crossAxisSpacing: 8,
),
itemBuilder: (context, index) {
final item = data[index];
return Chip(
label: Text(
item['name']!,
style: TextStyle(
color: Theme.of(context)
.chipTheme
.secondaryLabelStyle
?.color ??
Colors.white,
),
overflow: TextOverflow.ellipsis,
),
deleteIcon: const Icon(Icons.close, color: Colors.white),
onDeleted: () async {
setState(() {
_savedNumbers.remove(item['number']);
});
await _saveSavedNumbers();
_transferList.removeWhere((t) => t.number == item['number']);
await _saveTransferList();
_showSnackBar('تم إزالة ${item['name']}');
},
backgroundColor: Theme.of(context).chipTheme.selectedColor,
elevation: 1,
);
},
);
},
),
),
const Divider(height: 1),
Expanded(
child: _transferList.isEmpty
? Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.phone_iphone_outlined,
size: 64,
color:
Theme.of(context).colorScheme.onSurface.withOpacity(0.3),
),
const SizedBox(height: 16),
Text(
'لا توجد عمليات تحويل',
style: TextStyle(
fontSize: 18,
color:
Theme.of(context).colorScheme.onSurface.withOpacity(0.6),
),
),
const SizedBox(height: 8),
Text(
'أضف الأرقام المحفوظة أولاً',
style: TextStyle(
color:
Theme.of(context).colorScheme.onSurface.withOpacity(0.5),
),
),
],
),
)
: ListView.builder(
itemCount: _transferList.length,
itemBuilder: (context, index) {
final item = _transferList[index];
return Dismissible(
key: Key('${item.number}_$index'),
background: Container(
margin: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 6,
),
decoration: BoxDecoration(
color: Colors.red.shade50,
borderRadius: BorderRadius.circular(16),
),
alignment: Alignment.centerRight,
padding: const EdgeInsets.only(right: 24),
child: Icon(
Icons.delete,
color: Colors.red.shade400,
),
),
onDismissed: (direction) => _removeTransferAt(index),
child: Card(
margin: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 6,
),
child: ListTile(
leading: Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: Theme.of(context)
.colorScheme
.primary
.withOpacity(0.1),
shape: BoxShape.circle,
),
child: Icon(
Icons.phone_android,
color: Theme.of(context).colorScheme.primary,
),
),
title: Text(
item.number,
style: TextStyle(
fontWeight: FontWeight.w500,
color: Theme.of(context).colorScheme.onSurface,
),
),
subtitle: Text(
'المبلغ: ${item.amount}',
style: TextStyle(
color:
Theme.of(context).colorScheme.onSurface.withOpacity(0.6),
),
),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: Icon(
Icons.edit,
color: Theme.of(context).colorScheme.primary,
),
onPressed: () => _editTransfer(index),
tooltip: 'تعديل',
),
IconButton(
icon: Icon(
Icons.delete_forever,
color: Colors.red.shade400,
),
onPressed: () => _removeTransferAt(index),
tooltip: 'حذف',
),
],
),
),
),
);
},
),
),
if (_transferLogs.isNotEmpty)
Padding(
padding: const EdgeInsets.all(12),
child: ElevatedButton.icon(
icon: const Icon(Icons.history),
label: const Text('عرض سجل التحويلات'),
onPressed: _showTransferLogsDialog,
),
),
],
),
);
}
}
```
---
### أهم التعديلات الرئيسية:
1. **تحسين عرض جهات الاتصال المختارة:**
- تم استبدال عرضها كسطر واحد من الشرائح (Chips) إلى شبكة GridView مع ضبط أقصى حجم لكل عنصر، ليظهر بشكل مرتب وأفضل.
- يتم تحميل الأسماء باستخدام FutureBuilder، ويظهر رقم جهة الاتصال أو الاسم.
2. **زر تحديث قائمة التحويل:**
- عندما تضغط على "تحديث قائمة التحويل"، يتم فتح شاشة حوارية توضح كل رقم مختار مع اختيار المبلغ المناسب لكل رقم من خلال Dropdown (اختيار من المبالغ المحفوظة).
- عند التأكيد، يتم إضافة أو تحديث كافة الأرقام مع المبالغ المختارة دفعة واحدة إلى قائمة التحويل.
3. **تحسين تجربة المستخدم:**
- فالتحقق من وجود مبالغ محفوظة قبل السماح بتعيينها.
- رسائل تنبيه ودعم التحقق من صحة المبالغ.
---
يمكنك نسخ الكود ووضعه في ملف `main.dart` لتشغيله مباشرة بعد إضافة التبعية (dependencies) كما في الأصل (مثل flutter_contacts, permission_handler ...الخ).
إذا أردت أي تعديل إضافي على التصميم أو إضافة مميزات أخرى لا تتردد في طلبها.