Compare commits

..

3 Commits

Author SHA1 Message Date
9bfb55821d fix: search bar upgrade (#42)
Some checks failed
/ mirror (push) Successful in 4s
/ build (push) Has been cancelled
/ build-stealth (push) Has been cancelled
Reviewed-on: #42
Co-authored-by: Florian Griffon <florian.griffon@epitech.eu>
Co-committed-by: Florian Griffon <florian.griffon@epitech.eu>
2025-03-04 14:22:06 +01:00
b7ebacec85 cicd-stealth (#40)
Reviewed-on: #40
Co-authored-by: ange <ange@yw5n.com>
Co-committed-by: ange <ange@yw5n.com>
2025-03-04 14:22:06 +01:00
ef78e4c17d fix: call correctly in history page (#41)
Reviewed-on: #41
Co-authored-by: Florian Griffon <florian.griffon@epitech.eu>
Co-committed-by: Florian Griffon <florian.griffon@epitech.eu>
2025-03-04 14:22:06 +01:00
6 changed files with 212 additions and 92 deletions

View File

@ -10,8 +10,22 @@ jobs:
- uses: actions/checkout@v1 - uses: actions/checkout@v1
with: with:
subpath: dialer/ subpath: dialer/
- uses: icing/flutter@main - uses: docker://git.gmoker.com/icing/flutter:main
- uses: actions/upload-artifact@v1 - uses: actions/upload-artifact@v1
with: with:
name: icing-dialer-${{ gitea.ref_name }}-${{ gitea.run_id }}.apk name: icing-dialer-${{ gitea.ref_name }}-${{ gitea.run_id }}.apk
path: build/app/outputs/flutter-apk/app-release.apk path: build/app/outputs/flutter-apk/app-release.apk
build-stealth:
runs-on: debian
steps:
- uses: actions/checkout@v1
with:
subpath: dialer/
- uses: docker://git.gmoker.com/icing/flutter:main
with:
args: "build apk --dart-define=STEALTH=true"
- uses: actions/upload-artifact@v1
with:
name: icing-dialer-stealth-${{ gitea.ref_name }}-${{ gitea.run_id }}.apk
path: build/app/outputs/flutter-apk/app-release.apk

View File

@ -2,4 +2,9 @@
IMG=git.gmoker.com/icing/flutter:main IMG=git.gmoker.com/icing/flutter:main
docker run --rm -v "$PWD:/app/" "$IMG" build apk if [ "$1" == '-s' ]; then
OPT+=(--dart-define=STEALTH=true)
fi
set -x
docker run --rm -v "$PWD:/app/" "$IMG" build apk "${OPT[@]}"

View File

@ -11,6 +11,7 @@ import 'package:dialer/features/contacts/contact_state.dart';
import 'package:dialer/widgets/username_color_generator.dart'; import 'package:dialer/widgets/username_color_generator.dart';
import '../../services/block_service.dart'; import '../../services/block_service.dart';
import '../contacts/widgets/contact_modal.dart'; import '../contacts/widgets/contact_modal.dart';
import '../../services/call_service.dart';
class History { class History {
final Contact contact; final Contact contact;
@ -20,12 +21,12 @@ class History {
final int attempts; final int attempts;
History( History(
this.contact, this.contact,
this.date, this.date,
this.callType, this.callType,
this.callStatus, this.callStatus,
this.attempts, this.attempts,
); );
} }
class HistoryPage extends StatefulWidget { class HistoryPage extends StatefulWidget {
@ -41,6 +42,7 @@ class _HistoryPageState extends State<HistoryPage>
bool loading = true; bool loading = true;
int? _expandedIndex; int? _expandedIndex;
final ObfuscateService _obfuscateService = ObfuscateService(); final ObfuscateService _obfuscateService = ObfuscateService();
final CallService _callService = CallService();
// Create a MethodChannel instance. // Create a MethodChannel instance.
static const MethodChannel _channel = MethodChannel('com.example.calllog'); static const MethodChannel _channel = MethodChannel('com.example.calllog');
@ -83,8 +85,8 @@ class _HistoryPageState extends State<HistoryPage>
} }
} catch (e) { } catch (e) {
print("Error updating favorite status: $e"); print("Error updating favorite status: $e");
ScaffoldMessenger.of(context) ScaffoldMessenger.of(context).showSnackBar(
.showSnackBar(SnackBar(content: Text('Failed to update favorite status'))); SnackBar(content: Text('Failed to update favorite status')));
} }
} }
@ -155,7 +157,7 @@ class _HistoryPageState extends State<HistoryPage>
// Convert timestamp to DateTime. // Convert timestamp to DateTime.
DateTime callDate = DateTime callDate =
DateTime.fromMillisecondsSinceEpoch(entry['date'] ?? 0); DateTime.fromMillisecondsSinceEpoch(entry['date'] ?? 0);
int typeInt = entry['type'] ?? 0; int typeInt = entry['type'] ?? 0;
int duration = entry['duration'] ?? 0; int duration = entry['duration'] ?? 0;
@ -193,7 +195,8 @@ class _HistoryPageState extends State<HistoryPage>
); );
} }
callHistories.add(History(matchedContact, callDate, callType, callStatus, 1)); callHistories
.add(History(matchedContact, callDate, callType, callStatus, 1));
} }
// Sort histories by most recent. // Sort histories by most recent.
@ -218,7 +221,7 @@ class _HistoryPageState extends State<HistoryPage>
for (var history in historyList) { for (var history in historyList) {
final callDate = final callDate =
DateTime(history.date.year, history.date.month, history.date.day); DateTime(history.date.year, history.date.month, history.date.day);
if (callDate == today) { if (callDate == today) {
todayHistories.add(history); todayHistories.add(history);
} else if (callDate == yesterday) { } else if (callDate == yesterday) {
@ -291,7 +294,7 @@ class _HistoryPageState extends State<HistoryPage>
} }
List<History> missedCalls = List<History> missedCalls =
histories.where((h) => h.callStatus == 'missed').toList(); histories.where((h) => h.callStatus == 'missed').toList();
final allItems = _buildGroupedList(histories); final allItems = _buildGroupedList(histories);
final missedItems = _buildGroupedList(missedCalls); final missedItems = _buildGroupedList(missedCalls);
@ -360,7 +363,8 @@ class _HistoryPageState extends State<HistoryPage>
onEdit: () async { onEdit: () async {
if (await FlutterContacts.requestPermission()) { if (await FlutterContacts.requestPermission()) {
final updatedContact = final updatedContact =
await FlutterContacts.openExternalEdit(contact.id); await FlutterContacts.openExternalEdit(
contact.id);
if (updatedContact != null) { if (updatedContact != null) {
await _refreshContacts(); await _refreshContacts();
Navigator.of(context).pop(); Navigator.of(context).pop();
@ -415,18 +419,11 @@ class _HistoryPageState extends State<HistoryPage>
icon: const Icon(Icons.phone, color: Colors.green), icon: const Icon(Icons.phone, color: Colors.green),
onPressed: () async { onPressed: () async {
if (contact.phones.isNotEmpty) { if (contact.phones.isNotEmpty) {
final Uri callUri = _callService.makeGsmCall(contact.phones.first.number);
Uri(scheme: 'tel', path: contact.phones.first.number);
if (await canLaunchUrl(callUri)) {
await launchUrl(callUri);
} else {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Could not launch call')),
);
}
} else { } else {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Contact has no phone number')), const SnackBar(
content: Text('Contact has no phone number')),
); );
} }
}, },
@ -444,7 +441,9 @@ class _HistoryPageState extends State<HistoryPage>
color: Colors.grey[850], color: Colors.grey[850],
child: FutureBuilder<bool>( child: FutureBuilder<bool>(
future: BlockService().isNumberBlocked( future: BlockService().isNumberBlocked(
contact.phones.isNotEmpty ? contact.phones.first.number : ''), contact.phones.isNotEmpty
? contact.phones.first.number
: ''),
builder: (context, snapshot) { builder: (context, snapshot) {
final isBlocked = snapshot.data ?? false; final isBlocked = snapshot.data ?? false;
return Row( return Row(
@ -460,29 +459,37 @@ class _HistoryPageState extends State<HistoryPage>
await launchUrl(smsUri); await launchUrl(smsUri);
} else { } else {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Could not send message')), const SnackBar(
content:
Text('Could not send message')),
); );
} }
} else { } else {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Contact has no phone number')), const SnackBar(
content:
Text('Contact has no phone number')),
); );
} }
}, },
icon: const Icon(Icons.message, color: Colors.white), icon:
label: const Text('Message', style: TextStyle(color: Colors.white)), const Icon(Icons.message, color: Colors.white),
label: const Text('Message',
style: TextStyle(color: Colors.white)),
), ),
TextButton.icon( TextButton.icon(
onPressed: () { onPressed: () {
Navigator.push( Navigator.push(
context, context,
MaterialPageRoute( MaterialPageRoute(
builder: (_) => CallDetailsPage(history: history), builder: (_) =>
CallDetailsPage(history: history),
), ),
); );
}, },
icon: const Icon(Icons.info, color: Colors.white), icon: const Icon(Icons.info, color: Colors.white),
label: const Text('Details', style: TextStyle(color: Colors.white)), label: const Text('Details',
style: TextStyle(color: Colors.white)),
), ),
TextButton.icon( TextButton.icon(
onPressed: () async { onPressed: () async {
@ -491,24 +498,29 @@ class _HistoryPageState extends State<HistoryPage>
: null; : null;
if (phoneNumber == null) { if (phoneNumber == null) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Contact has no phone number')), const SnackBar(
content:
Text('Contact has no phone number')),
); );
return; return;
} }
if (isBlocked) { if (isBlocked) {
await BlockService().unblockNumber(phoneNumber); await BlockService().unblockNumber(phoneNumber);
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('$phoneNumber unblocked')), SnackBar(
content: Text('$phoneNumber unblocked')),
); );
} else { } else {
await BlockService().blockNumber(phoneNumber); await BlockService().blockNumber(phoneNumber);
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('$phoneNumber blocked')), SnackBar(
content: Text('$phoneNumber blocked')),
); );
} }
setState(() {}); setState(() {});
}, },
icon: Icon(isBlocked ? Icons.lock_open : Icons.block, icon: Icon(
isBlocked ? Icons.lock_open : Icons.block,
color: Colors.white), color: Colors.white),
label: Text(isBlocked ? 'Unblock' : 'Block', label: Text(isBlocked ? 'Unblock' : 'Block',
style: const TextStyle(color: Colors.white)), style: const TextStyle(color: Colors.white)),
@ -554,21 +566,22 @@ class CallDetailsPage extends StatelessWidget {
children: [ children: [
(contact.thumbnail != null && contact.thumbnail!.isNotEmpty) (contact.thumbnail != null && contact.thumbnail!.isNotEmpty)
? ObfuscatedAvatar( ? ObfuscatedAvatar(
imageBytes: contact.thumbnail, imageBytes: contact.thumbnail,
radius: 30, radius: 30,
backgroundColor: contactBg, backgroundColor: contactBg,
fallbackInitial: contact.displayName, fallbackInitial: contact.displayName,
) )
: CircleAvatar( : CircleAvatar(
backgroundColor: generateColorFromName(contact.displayName), backgroundColor:
radius: 30, generateColorFromName(contact.displayName),
child: Text( radius: 30,
contact.displayName.isNotEmpty child: Text(
? contact.displayName[0].toUpperCase() contact.displayName.isNotEmpty
: '?', ? contact.displayName[0].toUpperCase()
style: TextStyle(color: contactLetter), : '?',
), style: TextStyle(color: contactLetter),
), ),
),
const SizedBox(width: 16), const SizedBox(width: 16),
Expanded( Expanded(
child: Text( child: Text(
@ -600,7 +613,8 @@ class CallDetailsPage extends StatelessWidget {
if (contact.phones.isNotEmpty) if (contact.phones.isNotEmpty)
DetailRow( DetailRow(
label: 'Number:', label: 'Number:',
value: _obfuscateService.obfuscateData(contact.phones.first.number), value: _obfuscateService
.obfuscateData(contact.phones.first.number),
), ),
], ],
), ),

View File

@ -8,6 +8,7 @@ import 'package:flutter_contacts/flutter_contacts.dart';
import 'package:dialer/features/settings/settings.dart'; import 'package:dialer/features/settings/settings.dart';
import '../../services/contact_service.dart'; import '../../services/contact_service.dart';
import 'package:dialer/features/voicemail/voicemail_page.dart'; import 'package:dialer/features/voicemail/voicemail_page.dart';
import '../contacts/widgets/contact_modal.dart';
class _MyHomePageState extends State<MyHomePage> class _MyHomePageState extends State<MyHomePage>
@ -17,6 +18,8 @@ class _MyHomePageState extends State<MyHomePage>
List<Contact> _contactSuggestions = []; List<Contact> _contactSuggestions = [];
final ContactService _contactService = ContactService(); final ContactService _contactService = ContactService();
final ObfuscateService _obfuscateService = ObfuscateService(); final ObfuscateService _obfuscateService = ObfuscateService();
final TextEditingController _searchController = TextEditingController();
@override @override
void initState() { void initState() {
@ -32,12 +35,15 @@ class _MyHomePageState extends State<MyHomePage>
setState(() {}); setState(() {});
} }
void _onSearchChanged(String query) { void _clearSearch() {
print("Search query: $query"); _searchController.clear();
_onSearchChanged('');
}
void _onSearchChanged(String query) {
setState(() { setState(() {
if (query.isEmpty) { if (query.isEmpty) {
_contactSuggestions = List.from(_allContacts); _contactSuggestions = List.from(_allContacts); // Reset suggestions
} else { } else {
_contactSuggestions = _allContacts.where((contact) { _contactSuggestions = _allContacts.where((contact) {
return contact.displayName return contact.displayName
@ -50,6 +56,7 @@ class _MyHomePageState extends State<MyHomePage>
@override @override
void dispose() { void dispose() {
_searchController.dispose();
_tabController.removeListener(_handleTabIndex); _tabController.removeListener(_handleTabIndex);
_tabController.dispose(); _tabController.dispose();
super.dispose(); super.dispose();
@ -59,6 +66,34 @@ class _MyHomePageState extends State<MyHomePage>
setState(() {}); setState(() {});
} }
void _toggleFavorite(Contact contact) async {
try {
if (await FlutterContacts.requestPermission()) {
Contact? fullContact = await FlutterContacts.getContact(contact.id,
withProperties: true,
withAccounts: true,
withPhoto: true,
withThumbnail: true);
if (fullContact != null) {
fullContact.isStarred = !fullContact.isStarred;
await FlutterContacts.updateContact(fullContact);
setState(() {
// Updating the contact list after toggling the favorite
_fetchContacts();
});
}
} else {
print("Could not fetch contact details");
}
} catch (e) {
print("Error updating favorite status: $e");
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Failed to update contact favorite status')),
);
}
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
@ -80,63 +115,109 @@ class _MyHomePageState extends State<MyHomePage>
decoration: BoxDecoration( decoration: BoxDecoration(
color: const Color.fromARGB(255, 30, 30, 30), color: const Color.fromARGB(255, 30, 30, 30),
borderRadius: BorderRadius.circular(12.0), borderRadius: BorderRadius.circular(12.0),
border: Border( border: Border.all(color: Colors.grey.shade800, width: 1),
top: BorderSide(color: Colors.grey.shade800, width: 1),
left: BorderSide(color: Colors.grey.shade800, width: 1),
right: BorderSide(color: Colors.grey.shade800, width: 1),
bottom:
BorderSide(color: Colors.grey.shade800, width: 2),
),
), ),
child: SearchAnchor( child: SearchAnchor(
builder: builder:
(BuildContext context, SearchController controller) { (BuildContext context, SearchController controller) {
return SearchBar( return GestureDetector(
controller: controller,
padding:
WidgetStateProperty.all<EdgeInsetsGeometry>(
const EdgeInsets.only(
top: 6.0,
bottom: 6.0,
left: 16.0,
right: 16.0,
),
),
onTap: () { onTap: () {
controller.openView(); controller.openView(); // Open the search view
_onSearchChanged('');
}, },
backgroundColor: WidgetStateProperty.all( child: Container(
const Color.fromARGB(255, 30, 30, 30)), decoration: BoxDecoration(
hintText: 'Search contacts', color: const Color.fromARGB(255, 30, 30, 30),
hintStyle: WidgetStateProperty.all(
const TextStyle(color: Colors.grey, fontSize: 16.0),
),
leading: const Icon(
Icons.search,
color: Colors.grey,
size: 24.0,
),
shape:
WidgetStateProperty.all<RoundedRectangleBorder>(
RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12.0), borderRadius: BorderRadius.circular(12.0),
border: Border.all(
color: Colors.grey.shade800, width: 1),
),
padding: const EdgeInsets.symmetric(
vertical: 12.0, horizontal: 16.0),
child: Row(
children: [
const Icon(Icons.search,
color: Colors.grey, size: 24.0),
const SizedBox(width: 8.0),
Text(
_searchController.text.isEmpty
? 'Search contacts'
: _searchController.text,
style: const TextStyle(
color: Colors.grey, fontSize: 16.0),
),
const Spacer(),
if (_searchController.text.isNotEmpty)
GestureDetector(
onTap: _clearSearch,
child: const Icon(
Icons.clear,
color: Colors.grey,
size: 24.0,
),
),
],
), ),
), ),
); );
}, },
viewOnChanged: (query) { viewOnChanged: (query) {
_onSearchChanged(query); _onSearchChanged(query); // Update immediately
}, },
suggestionsBuilder: suggestionsBuilder:
(BuildContext context, SearchController controller) { (BuildContext context, SearchController controller) {
return _contactSuggestions.map((contact) { return _contactSuggestions.map((contact) {
return ListTile( return ListTile(
key: ValueKey(contact.id), key: ValueKey(contact.id),
title: Text(_obfuscateService.obfuscateData(contact.displayName), title: Text(_obfuscateService.obfuscateData(contact.displayName),
style: const TextStyle(color: Colors.white)), style: const TextStyle(color: Colors.white)),
onTap: () { onTap: () {
// Clear the search text input
controller.text = '';
// Close the search view
controller.closeView(contact.displayName); controller.closeView(contact.displayName);
// Show the ContactModal when a contact is tapped
showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (context) {
return ContactModal(
contact: contact,
onEdit: () async {
if (await FlutterContacts
.requestPermission()) {
final updatedContact =
await FlutterContacts
.openExternalEdit(contact.id);
if (updatedContact != null) {
_fetchContacts();
Navigator.of(context).pop();
ScaffoldMessenger.of(context)
.showSnackBar(
SnackBar(
content: Text(
'${contact.displayName} updated successfully!'),
),
);
} else {
ScaffoldMessenger.of(context)
.showSnackBar(
SnackBar(
content: Text(
'Edit canceled or failed.'),
),
);
}
}
},
onToggleFavorite: () =>
_toggleFavorite(contact),
isFavorite: contact.isStarred,
);
},
);
}, },
); );
}).toList(); }).toList();

View File

@ -2,4 +2,9 @@
IMG=git.gmoker.com/icing/flutter:main IMG=git.gmoker.com/icing/flutter:main
docker run --rm -p 5037:5037 -v "$PWD:/app/" "$IMG" run if [ "$1" == '-s' ]; then
OPT+=(--dart-define=STEALTH=true)
fi
set -x
docker run --rm -p 5037:5037 -v "$PWD:/app/" "$IMG" run "${OPTS[@]}"

View File

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