diff --git a/lib/firebase/firebase_auth_extensions.dart b/lib/firebase/firebase_auth_extensions.dart index 988f7d8..649c9f3 100644 --- a/lib/firebase/firebase_auth_extensions.dart +++ b/lib/firebase/firebase_auth_extensions.dart @@ -13,6 +13,7 @@ String generateNonce([int length = 32]) { final charset = '0123456789ABCDEFGHIJKLMNOPQRSTUVXYZabcdefghijklmnopqrstuvwxyz-._'; final random = Random.secure(); + return List.generate(length, (_) => charset[random.nextInt(charset.length)]) .join(); } @@ -21,6 +22,7 @@ String generateNonce([int length = 32]) { String sha256ofString(String input) { final bytes = utf8.encode(input); final digest = sha256.convert(bytes); + return digest.toString(); } diff --git a/lib/screens/edit_call_screen.dart b/lib/screens/edit_call_screen.dart index 0023797..051fb18 100644 --- a/lib/screens/edit_call_screen.dart +++ b/lib/screens/edit_call_screen.dart @@ -1,3 +1,4 @@ +import 'package:bluejay/bluejay.dart'; import 'package:call_manager/data_models/call.dart'; import 'package:call_manager/firebase/firebase.dart'; import 'package:call_manager/provided.dart'; @@ -25,11 +26,7 @@ class EditCallScreen extends StatefulWidget { class _EditCallScreenState extends State with FirebaseMixin, Provided { Contact? selectedContact; - //TextField controllers - final _descriptionFieldController = TextEditingController(); - final _nameFieldController = TextEditingController(); - final _phoneFieldController = TextEditingController(); - + final _formKey = GlobalKey(); @override // ignore: long-method, code-metrics @@ -45,97 +42,130 @@ class _EditCallScreenState extends State body: SingleChildScrollView( child: Padding( padding: const EdgeInsets.all(16.0), - child: Column( - children: [ - TypeAheadFormField( - suggestionsCallback: contactsUtility.searchContactsWithQuery, - itemBuilder: (context, dynamic contact) { - return ListTile( - leading: ContactAvatar(contact: contact), - title: Text(contact.displayName), - ); - }, - transitionBuilder: (context, suggestionsBox, controller) { - return suggestionsBox; - }, - onSuggestionSelected: (dynamic contact) { - selectedContact = contact; - _nameFieldController.text = selectedContact!.displayName!; - if (selectedContact!.phones!.length > 1) { - showModalBottomSheet( - context: context, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), - builder: (_) => MultiplePhoneNumbersSheet( - selectedContact: selectedContact, + child: Form( + key: _formKey, + child: Column( + children: [ + TextEditingControllerBuilder( + text: widget.call.name!, + builder: (_, controller) { + return TypeAheadFormField( + suggestionsCallback: + contactsUtility.searchContactsWithQuery, + itemBuilder: (context, dynamic contact) { + return ListTile( + leading: ContactAvatar(contact: contact), + title: Text(contact.displayName), + ); + }, + transitionBuilder: (context, suggestionsBox, controller) { + return suggestionsBox; + }, + onSuggestionSelected: (dynamic contact) { + selectedContact = contact; + controller.text = selectedContact!.displayName!; + if (selectedContact!.phones!.length > 1) { + showModalBottomSheet( + context: context, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + builder: (_) => MultiplePhoneNumbersSheet( + selectedContact: selectedContact, + ), + ).then((value) => widget.call.phoneNumber = value); + } else { + widget.call.phoneNumber = + selectedContact!.phones!.first.value!; + } + }, + validator: (value) => value == null || value.isEmpty + ? 'This field is required' + : null, + onSaved: (contactName) { + controller.text = contactName!; + widget.call.name = contactName; + }, + textFieldConfiguration: TextFieldConfiguration( + textCapitalization: TextCapitalization.words, + controller: controller, + keyboardType: TextInputType.text, + maxLines: 1, + decoration: InputDecoration( + prefixIcon: Icon( + Icons.person_outline, + color: theme.iconTheme.color, + ), + suffixIcon: ClearButton( + onPressed: () { + controller.clear(); + widget.call.name = controller.text; + }, + ), + labelText: 'Name', + ), ), - ).then((value) => _phoneFieldController.text = value); - } else { - _phoneFieldController.text = - selectedContact!.phones!.first.value!; - } - }, - validator: (input) => input == null || input == '' - ? 'This field is required' - : null, - onSaved: (contactName) => - _nameFieldController.text = contactName!, - textFieldConfiguration: TextFieldConfiguration( - textCapitalization: TextCapitalization.words, - controller: _nameFieldController, - keyboardType: TextInputType.text, - maxLines: 1, - decoration: InputDecoration( - prefixIcon: Icon( - Icons.person_outline, - color: theme.iconTheme.color, - ), - suffixIcon: ClearButton( - onPressed: () => _nameFieldController.text = '', - ), - labelText: widget.call.name, - ), + ); + }, ), - ), - const SizedBox(height: 16.0), - TextField( - keyboardType: TextInputType.phone, - maxLines: 1, - autofocus: false, - controller: _phoneFieldController, - decoration: InputDecoration( - prefixIcon: Icon( - Icons.phone_outlined, - color: theme.iconTheme.color, - ), - suffixIcon: ClearButton( - onPressed: () => _phoneFieldController.text = '', - ), - labelText: widget.call.phoneNumber, + const SizedBox(height: 16.0), + TextEditingControllerBuilder( + text: widget.call.phoneNumber!, + builder: (_, controller) { + return TextFormField( + keyboardType: TextInputType.phone, + maxLines: 1, + autofocus: false, + controller: controller, + onChanged: (value) => widget.call.phoneNumber = value, + validator: (value) => value == null || value.isEmpty + ? 'This field is required' + : null, + decoration: InputDecoration( + prefixIcon: Icon( + Icons.phone_outlined, + color: theme.iconTheme.color, + ), + suffixIcon: ClearButton( + onPressed: () { + controller.clear(); + widget.call.phoneNumber = controller.text; + }, + ), + labelText: 'Phone number', + ), + ); + }, ), - ), - const SizedBox(height: 16.0), - TextFormField( - keyboardType: TextInputType.multiline, - textCapitalization: TextCapitalization.sentences, - maxLines: 2, - autofocus: false, - controller: _descriptionFieldController, - decoration: InputDecoration( - labelText: widget.call.hasDescription - ? widget.call.description - : 'Description', - prefixIcon: Icon( - Icons.comment_outlined, - color: theme.iconTheme.color, - ), - suffixIcon: ClearButton( - onPressed: () => _descriptionFieldController.text = '', - ), + const SizedBox(height: 16.0), + TextEditingControllerBuilder( + text: widget.call.description ?? '', + builder: (_, controller) { + return TextFormField( + keyboardType: TextInputType.multiline, + textCapitalization: TextCapitalization.sentences, + maxLines: 2, + autofocus: false, + controller: controller, + onChanged: (value) => widget.call.description = value, + decoration: InputDecoration( + labelText: 'Description', + prefixIcon: Icon( + Icons.comment_outlined, + color: theme.iconTheme.color, + ), + suffixIcon: ClearButton( + onPressed: () { + controller.clear(); + widget.call.description = controller.text; + }, + ), + ), + ); + }, ), - ), - ], + ], + ), ), ), ), @@ -143,30 +173,21 @@ class _EditCallScreenState extends State ? FloatingActionButton.extended( highlightElevation: 2.0, onPressed: () { - if (_nameFieldController.text.isNotEmpty) { - widget.call.name = _nameFieldController.text; - } - - if (_phoneFieldController.text.isNotEmpty) { - widget.call.phoneNumber = _phoneFieldController.text; - } + _formKey.currentState!.save(); + if (_formKey.currentState!.validate()) { + if (selectedContact != null) { + widget.call.avatar = selectedContact?.avatar != null + ? String.fromCharCodes(selectedContact!.avatar!) + : ''; + } - if (_descriptionFieldController.text.isNotEmpty) { - widget.call.description = _descriptionFieldController.text; - } + firestore + .calls(currentUser!.uid) + .doc(widget.call.id) + .update(widget.call.toJson()); - if (selectedContact != null) { - widget.call.avatar = selectedContact?.avatar != null - ? String.fromCharCodes(selectedContact!.avatar!) - : ''; + Navigator.of(context).pop(); } - - firestore - .calls(currentUser!.uid) - .doc(widget.call.id) - .update(widget.call.toJson()); - - Navigator.of(context).pop(); }, tooltip: 'Save', elevation: 2.0, diff --git a/lib/screens/new_call_screen.dart b/lib/screens/new_call_screen.dart index 1ddb085..9f824a5 100644 --- a/lib/screens/new_call_screen.dart +++ b/lib/screens/new_call_screen.dart @@ -1,3 +1,4 @@ +import 'package:bluejay/bluejay.dart'; import 'package:call_manager/data_models/call.dart'; import 'package:call_manager/firebase/firebase.dart'; import 'package:call_manager/provided.dart'; @@ -21,6 +22,10 @@ class NewCallScreen extends StatefulWidget { class _NewCallScreenState extends State with FirebaseMixin, Provided { // Contact Picker stuff + late Call call = Call( + name: '', + phoneNumber: '', + ); Iterable? contacts; final dateFormat = DateFormat('EEEE, MMMM d, yyyy'); final formKey = GlobalKey(); @@ -30,22 +35,12 @@ class _NewCallScreenState extends State final timeFormat = DateFormat('h:mm a'); - final _dateFieldController = TextEditingController(); - final _descriptionFieldController = TextEditingController(); final _nameFieldController = TextEditingController(); - final _phoneFieldController = TextEditingController(); - final _timeFieldController = TextEditingController(); Future saveCall() async { + formKey.currentState!.save(); if (formKey.currentState!.validate()) { formKey.currentState!.save(); - final call = Call( - avatar: selectedContact?.avatar != null - ? String.fromCharCodes(selectedContact!.avatar!) - : '', - name: _nameFieldController.text, - phoneNumber: _phoneFieldController.text, - ); if (reminderDate != null && reminderTime != null) { call.reminderDate = reminderDate.toString(); @@ -71,7 +66,7 @@ class _NewCallScreenState extends State } @override - // ignore: long-method + // ignore: long-method, code-metrics Widget build(BuildContext context) { final theme = Theme.of(context); @@ -88,142 +83,176 @@ class _NewCallScreenState extends State padding: const EdgeInsets.all(16.0), child: Column( children: [ - TypeAheadFormField( - suggestionsCallback: contactsUtility.searchContactsWithQuery, - itemBuilder: (context, dynamic contact) { - return ListTile( - leading: ContactAvatar(contact: contact), - title: Text(contact.displayName), + TextEditingControllerBuilder( + text: '', + builder: (_, controller) { + return TypeAheadFormField( + suggestionsCallback: + contactsUtility.searchContactsWithQuery, + itemBuilder: (context, dynamic contact) { + return ListTile( + leading: ContactAvatar(contact: contact), + title: Text(contact.displayName), + ); + }, + transitionBuilder: (context, suggestionsBox, controller) { + return suggestionsBox; + }, + onSuggestionSelected: (dynamic contact) { + selectedContact = contact; + _nameFieldController.text = + selectedContact!.displayName!; + if (selectedContact!.phones!.length > 1) { + showModalBottomSheet( + context: context, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + builder: (_) => MultiplePhoneNumbersSheet( + selectedContact: selectedContact, + ), + ).then((value) { + call.avatar = selectedContact?.avatar != null + ? String.fromCharCodes(selectedContact!.avatar!) + : ''; + call.phoneNumber = value; + }); + } else { + call.avatar = selectedContact?.avatar != null + ? String.fromCharCodes(selectedContact!.avatar!) + : ''; + call.phoneNumber = + selectedContact!.phones!.first.value!; + } + }, + validator: (input) => input == null || input == '' + ? 'This field is required' + : null, + onSaved: (contactName) => call.name = contactName!, + textFieldConfiguration: TextFieldConfiguration( + textCapitalization: TextCapitalization.words, + controller: _nameFieldController, + keyboardType: TextInputType.text, + maxLines: 1, + decoration: InputDecoration( + prefixIcon: Icon( + Icons.person_outline, + color: theme.iconTheme.color, + ), + labelText: 'Name*', + suffixIcon: ClearButton( + onPressed: () => controller.clear(), + ), + ), + ), ); }, - transitionBuilder: (context, suggestionsBox, controller) { - return suggestionsBox; - }, - onSuggestionSelected: (dynamic contact) { - selectedContact = contact; - _nameFieldController.text = selectedContact!.displayName!; - if (selectedContact!.phones!.length > 1) { - showModalBottomSheet( - context: context, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), + ), + const SizedBox(height: 16.0), + TextEditingControllerBuilder( + text: '', + builder: (_, controller) { + return TextFormField( + validator: (input) => input == null || input == '' + ? 'This field is required' + : null, + onSaved: (input) => controller.text = input!, + keyboardType: TextInputType.phone, + maxLines: 1, + autofocus: false, + controller: controller, + onChanged: (value) => call.phoneNumber = value, + decoration: InputDecoration( + prefixIcon: Icon( + Icons.phone_outlined, + color: theme.iconTheme.color, ), - builder: (_) => MultiplePhoneNumbersSheet( - selectedContact: selectedContact, + labelText: 'Phone Number*', + suffixIcon: ClearButton( + onPressed: () => controller.clear(), ), - ).then((value) { - _phoneFieldController.text = value; - }); - } else { - _phoneFieldController.text = - selectedContact!.phones!.first.value!; - } - }, - validator: (input) => input == null || input == '' - ? 'This field is required' - : null, - onSaved: (contactName) => - _nameFieldController.text = contactName!, - textFieldConfiguration: TextFieldConfiguration( - textCapitalization: TextCapitalization.words, - controller: _nameFieldController, - keyboardType: TextInputType.text, - maxLines: 1, - decoration: InputDecoration( - prefixIcon: Icon( - Icons.person_outline, - color: theme.iconTheme.color, ), - labelText: 'Name (Required)', - suffixIcon: ClearButton( - onPressed: () => _nameFieldController.text = '', - ), - ), - ), - ), - const SizedBox(height: 16.0), - TextFormField( - validator: (input) => input == null || input == '' - ? 'This field is required' - : null, - onSaved: (input) => _phoneFieldController.text = input!, - keyboardType: TextInputType.phone, - maxLines: 1, - autofocus: false, - controller: _phoneFieldController, - decoration: InputDecoration( - prefixIcon: Icon( - Icons.phone_outlined, - color: theme.iconTheme.color, - ), - labelText: 'Phone Number (Required)', - suffixIcon: ClearButton( - onPressed: () => _phoneFieldController.text = '', - ), - ), + ); + }, ), const SizedBox(height: 16.0), - TextFormField( - keyboardType: TextInputType.multiline, - textCapitalization: TextCapitalization.sentences, - maxLines: 2, - autofocus: false, - controller: _descriptionFieldController, - decoration: InputDecoration( - labelText: 'Description', - prefixIcon: Icon( - Icons.comment_outlined, - color: theme.iconTheme.color, - ), - suffixIcon: ClearButton( - onPressed: () => _descriptionFieldController.text = '', - ), - ), + TextEditingControllerBuilder( + text: '', + builder: (_, controller) { + return TextFormField( + keyboardType: TextInputType.multiline, + textCapitalization: TextCapitalization.sentences, + maxLines: 2, + autofocus: false, + controller: controller, + onChanged: (value) => call.description = value, + decoration: InputDecoration( + labelText: 'Description', + prefixIcon: Icon( + Icons.comment_outlined, + color: theme.iconTheme.color, + ), + suffixIcon: ClearButton( + onPressed: () => controller.clear(), + ), + ), + ); + }, ), const SizedBox(height: 16.0), - DateTimeField( - format: dateFormat, - onShowPicker: (context, currentValue) { - return showDatePicker( - context: context, - initialDate: DateTime.now(), - firstDate: DateTime.now(), - lastDate: DateTime(DateTime.now().year + 1), + TextEditingControllerBuilder( + text: '', + builder: (_, controller) { + return DateTimeField( + format: dateFormat, + onShowPicker: (context, currentValue) { + return showDatePicker( + context: context, + initialDate: DateTime.now(), + firstDate: DateTime.now(), + lastDate: DateTime(DateTime.now().year + 1), + ); + }, + onChanged: (date) => reminderDate = date, + controller: controller, + decoration: InputDecoration( + prefixIcon: Icon( + Icons.today, + color: theme.iconTheme.color, + ), + labelText: 'Reminder Date', + ), ); }, - onChanged: (date) => reminderDate = date, - controller: _dateFieldController, - decoration: InputDecoration( - prefixIcon: Icon( - Icons.today, - color: theme.iconTheme.color, - ), - labelText: 'Reminder Date', - ), ), const SizedBox(height: 16.0), - DateTimeField( - format: timeFormat, - onChanged: (timeOfDay) => - reminderTime = TimeOfDay.fromDateTime(timeOfDay!), - onShowPicker: (context, currentValue) async { - final time = await showTimePicker( - context: context, - initialTime: TimeOfDay.fromDateTime( - currentValue ?? DateTime.now(), + TextEditingControllerBuilder( + text: '', + builder: (_, controller) { + return DateTimeField( + format: timeFormat, + onChanged: (timeOfDay) => + reminderTime = TimeOfDay.fromDateTime(timeOfDay!), + onShowPicker: (context, currentValue) async { + final time = await showTimePicker( + context: context, + initialTime: TimeOfDay.fromDateTime( + currentValue ?? DateTime.now(), + ), + ); + + return DateTimeField.convert(time); + }, + controller: controller, + decoration: InputDecoration( + labelText: 'Reminder Time', + prefixIcon: Icon( + Icons.access_time, + color: theme.iconTheme.color, + ), ), ); - - return DateTimeField.convert(time); }, - controller: _timeFieldController, - decoration: InputDecoration( - labelText: 'Reminder Time', - prefixIcon: Icon( - Icons.access_time, - color: theme.iconTheme.color, - ), - ), ), ], ), diff --git a/lib/services/notifications_service.dart b/lib/services/notifications_service.dart index 525aa60..dda6efd 100644 --- a/lib/services/notifications_service.dart +++ b/lib/services/notifications_service.dart @@ -62,7 +62,6 @@ class NotificationService { ); Future scheduleNotification(Call call, DateTime scheduledDate) async { - print(tz.local); await notificationsPlugin!.zonedSchedule( 0, 'Reminder: call ${call.name}', diff --git a/lib/widgets/calls_list.dart b/lib/widgets/calls_list.dart index 67cca29..9edbe7a 100644 --- a/lib/widgets/calls_list.dart +++ b/lib/widgets/calls_list.dart @@ -15,8 +15,10 @@ class _CallsListState extends State with FirebaseMixin { Widget build(BuildContext context) { return StreamBuilder>>( stream: firestore.calls(currentUser!.uid).snapshots(), - builder: (context, - AsyncSnapshot>> snapshot) { + builder: ( + context, + AsyncSnapshot>> snapshot, + ) { if (!snapshot.hasData) { return Center( child: const CircularProgressIndicator(), diff --git a/pubspec.lock b/pubspec.lock index f72f68b..421283f 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -50,6 +50,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.5.0" + bluejay: + dependency: "direct main" + description: + name: bluejay + url: "https://pub.dartlang.org" + source: hosted + version: "1.3.0" boolean_selector: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index adcc9b6..0d1335d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: call_manager description: Call Manager in Flutter -version: 2.6.0+46 +version: 2.6.1+47 publish_to: none environment: @@ -10,6 +10,7 @@ dependencies: flutter: sdk: flutter + bluejay: ^1.3.0 cupertino_icons: ^1.0.3 datetime_picker_formfield: ^2.0.0 direct_dialer: