From 65db24b1b6744cfb4d2090a6821ef9df2a3287a6 Mon Sep 17 00:00:00 2001 From: KGCybeX Date: Tue, 28 Nov 2023 21:38:36 +0200 Subject: [PATCH] refactor: example app --- example/lib/screens/ui_call_screen.dart | 57 ++++-- example/lib/screens/widgets/call_actions.dart | 38 ++++ .../lib/screens/widgets/call_features.dart | 188 ++++++++++++++++++ .../{on_call_widget.dart => call_status.dart} | 8 +- .../screens/widgets/permissions_block.dart | 162 +-------------- example/lib/screens/widgets/twilio_log.dart | 48 +++++ 6 files changed, 320 insertions(+), 181 deletions(-) create mode 100644 example/lib/screens/widgets/call_actions.dart create mode 100644 example/lib/screens/widgets/call_features.dart rename example/lib/screens/widgets/{on_call_widget.dart => call_status.dart} (94%) create mode 100644 example/lib/screens/widgets/twilio_log.dart diff --git a/example/lib/screens/ui_call_screen.dart b/example/lib/screens/ui_call_screen.dart index fdbb739c..5d9a3796 100644 --- a/example/lib/screens/ui_call_screen.dart +++ b/example/lib/screens/ui_call_screen.dart @@ -1,16 +1,21 @@ +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:twilio_voice_example/screens/widgets/on_call_widget.dart'; +import 'widgets/call_actions.dart'; +import 'widgets/call_features.dart'; +import 'widgets/call_status.dart'; import 'widgets/permissions_block.dart'; +import 'widgets/twilio_log.dart'; typedef PerformCall = Future Function(String clientIdentifier); class UICallScreen extends StatefulWidget { final String userId; final PerformCall onPerformCall; + final PerformCall? onCallToQueue; - const UICallScreen({Key? key, required this.userId, required this.onPerformCall}) : super(key: key); + const UICallScreen({Key? key, required this.userId, required this.onPerformCall, this.onCallToQueue}) : super(key: key); @override State createState() => _UICallScreenState(); @@ -70,26 +75,44 @@ class _UICallScreenState extends State { ], ), const SizedBox(height: 10), - ElevatedButton( - child: const Text("Make Call"), - onPressed: () { - if (!_identifierKey.currentState!.validate()) { - return; + CallActions( + canCall: true, + onPerformCall: () { + final identifier = _identifierKey.currentState?.value; + if (identifier != null && identifier.isNotEmpty) { + widget.onPerformCall(identifier); } - final identity = _controller.text; - widget.onPerformCall(identity); }, ), - const Divider(), - const Padding( - padding: EdgeInsets.all(8.0), - child: OnCallWidget(), + const SizedBox(height: 10), + const Card( + child: Padding( + padding: EdgeInsets.all(8.0), + child: CallStatus(), + ), + ), + const Card( + child: Padding( + padding: EdgeInsets.all(8.0), + child: CallControls(), + ), + ), + const Card( + child: Padding( + padding: EdgeInsets.all(8.0), + child: PermissionsBlock(), + ), ), const Divider(), - const Expanded( - child: PermissionsBlock(), - ) + Expanded( + child: Column( + children: [ + Text("Events (latest at top)", style: Theme.of(context).textTheme.titleLarge), + const TwilioLog(), + ], + ), + ), ], ); } -} \ No newline at end of file +} diff --git a/example/lib/screens/widgets/call_actions.dart b/example/lib/screens/widgets/call_actions.dart new file mode 100644 index 00000000..cad26bc6 --- /dev/null +++ b/example/lib/screens/widgets/call_actions.dart @@ -0,0 +1,38 @@ +import 'package:flutter/material.dart'; + +class CallActions extends StatelessWidget { + final bool canCall; + final VoidCallback? onPerformCall; + + const CallActions({ + super.key, + this.canCall = true, + this.onPerformCall, + }); + + @override + Widget build(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if(canCall) + _CallAction(onPressed: onPerformCall, text: "Make Call"), + ], + ); + } +} + +class _CallAction extends StatelessWidget { + final VoidCallback? onPressed; + final String text; + + const _CallAction({super.key, required this.onPressed, required this.text}); + + @override + Widget build(BuildContext context) { + return ElevatedButton( + onPressed: onPressed, + child: Text(text), + ); + } +} diff --git a/example/lib/screens/widgets/call_features.dart b/example/lib/screens/widgets/call_features.dart new file mode 100644 index 00000000..a2b9cf67 --- /dev/null +++ b/example/lib/screens/widgets/call_features.dart @@ -0,0 +1,188 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; +import 'package:twilio_voice/twilio_voice.dart'; + +import 'state_toggle.dart'; + +class CallControls extends StatefulWidget { + const CallControls({super.key}); + + @override + State createState() => _CallControlsState(); +} + +class _CallControlsState extends State { + + late final StreamSubscription _subscription; + final _events = []; + + //#region #region State Getters + bool _stateHold = false; + + set stateHold(bool value) { + setState(() { + _stateHold = value; + }); + } + + bool _stateMute = false; + + set stateMute(bool value) { + setState(() { + _stateMute = value; + }); + } + + bool _stateSpeaker = false; + + set stateSpeaker(bool value) { + setState(() { + _stateSpeaker = value; + }); + } + + bool _stateBluetooth = false; + + set stateBluetooth(bool value) { + setState(() { + _stateBluetooth = value; + }); + } + + //#endregion + + final _tv = TwilioVoice.instance; + bool activeCall = false; + + @override + void initState() { + super.initState(); + _subscription = _tv.callEventsListener.listen((event) { + _events.add(event); + switch (event) { + case CallEvent.unhold: + case CallEvent.hold: + case CallEvent.unmute: + case CallEvent.mute: + case CallEvent.speakerOn: + case CallEvent.speakerOff: + case CallEvent.bluetoothOn: + case CallEvent.bluetoothOff: + _updateStates(); + break; + + case CallEvent.connected: + activeCall = true; + _updateStates(); + break; + + case CallEvent.callEnded: + activeCall = false; + _updateStates(); + break; + + case CallEvent.incoming: + case CallEvent.ringing: + case CallEvent.declined: + case CallEvent.answer: + case CallEvent.missedCall: + case CallEvent.returningCall: + case CallEvent.reconnecting: + case CallEvent.reconnected: + _updateStates(); + break; + + case CallEvent.log: + break; + + case CallEvent.permission: + // Using app lifecycle states, we don't have to update permissions here - convenience only. + break; + } + }); + _updateStates(); + } + + void _updateStates() { + // get all states from call + _tv.call.isMuted().then((value) => stateMute = value ?? false); + _tv.call.isHolding().then((value) => stateHold = value ?? false); + _tv.call.isOnSpeaker().then((value) => stateSpeaker = value ?? false); + _tv.call.isBluetoothOn().then((value) => stateBluetooth = value ?? false); + } + + @override + Widget build(BuildContext context) { + return Column( + children: [ + // state + Text("State", style: Theme.of(context).textTheme.titleLarge), + + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + Expanded( + child: StateToggle( + state: _stateMute, + icon: _stateMute ? Icons.mic : Icons.mic_off, + title: "Mute", + onTap: () => _tv.call.toggleMute(!_stateMute), + ), + ), + Expanded( + child: StateToggle( + state: _stateHold, + icon: Icons.pause, + title: "Hold", + iconColor: _stateHold ? Colors.orange : null, + onTap: () => _tv.call.holdCall(holdCall: !_stateHold), + ), + ), + Expanded( + child: StateToggle( + state: _stateSpeaker, + icon: Icons.volume_up, + title: "Speaker", + iconColor: _stateSpeaker ? Colors.green : null, + onTap: () => _tv.call.toggleSpeaker(!_stateSpeaker), + ), + ), + Expanded( + child: StateToggle( + state: _stateBluetooth, + icon: Icons.bluetooth, + title: "Bluetooth", + iconColor: _stateBluetooth ? Colors.blue : null, + onTap: () => _tv.call.toggleBluetooth(bluetoothOn: !_stateBluetooth), + ), + ), + ], + ), + + Row( + children: [ + Expanded( + child: StateToggle( + state: activeCall, + icon: Icons.call_end, + title: "Hangup", + iconColor: Colors.red, + onTap: activeCall ? () => _tv.call.hangUp() : null, + ), + ), + ], + ), + + const SizedBox(height: 12), + ] + ); + } + + @override + void dispose() { + _subscription.cancel(); + super.dispose(); + } +} diff --git a/example/lib/screens/widgets/on_call_widget.dart b/example/lib/screens/widgets/call_status.dart similarity index 94% rename from example/lib/screens/widgets/on_call_widget.dart rename to example/lib/screens/widgets/call_status.dart index 658c7555..43c95dc1 100644 --- a/example/lib/screens/widgets/on_call_widget.dart +++ b/example/lib/screens/widgets/call_status.dart @@ -1,14 +1,14 @@ import 'package:flutter/material.dart'; import 'package:twilio_voice/twilio_voice.dart'; -class OnCallWidget extends StatefulWidget { - const OnCallWidget({super.key}); +class CallStatus extends StatefulWidget { + const CallStatus({super.key}); @override - State createState() => _OnCallWidgetState(); + State createState() => _CallStatusState(); } -class _OnCallWidgetState extends State { +class _CallStatusState extends State { final List _events = []; /// Store only call-related events diff --git a/example/lib/screens/widgets/permissions_block.dart b/example/lib/screens/widgets/permissions_block.dart index fccc430f..f41bbff6 100644 --- a/example/lib/screens/widgets/permissions_block.dart +++ b/example/lib/screens/widgets/permissions_block.dart @@ -6,10 +6,9 @@ import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:twilio_voice/twilio_voice.dart'; -import 'package:twilio_voice_example/screens/widgets/permission_tile.dart'; -import 'package:twilio_voice_example/screens/widgets/state_toggle.dart'; import '../../utils.dart'; +import 'permission_tile.dart'; class PermissionsBlock extends StatefulWidget { const PermissionsBlock({super.key}); @@ -20,7 +19,6 @@ class PermissionsBlock extends StatefulWidget { class _PermissionsBlockState extends State with WidgetsBindingObserver { late final StreamSubscription _subscription; - final _events = []; AppLifecycleState? _lastLifecycleState; @@ -94,41 +92,6 @@ class _PermissionsBlockState extends State with WidgetsBinding //#endregion - //#region #region State Getters - bool _stateHold = false; - - set stateHold(bool value) { - setState(() { - _stateHold = value; - }); - } - - bool _stateMute = false; - - set stateMute(bool value) { - setState(() { - _stateMute = value; - }); - } - - bool _stateSpeaker = false; - - set stateSpeaker(bool value) { - setState(() { - _stateSpeaker = value; - }); - } - - bool _stateBluetooth = false; - - set stateBluetooth(bool value) { - setState(() { - _stateBluetooth = value; - }); - } - - //#endregion - @override void didChangeAppLifecycleState(AppLifecycleState state) { printDebug("AppLifecycleState: $state"); @@ -141,60 +104,7 @@ class _PermissionsBlockState extends State with WidgetsBinding @override void initState() { super.initState(); - WidgetsBinding.instance.addObserver(this); - _subscription = _tv.callEventsListener.listen((event) { - _events.add(event); - switch (event) { - case CallEvent.unhold: - case CallEvent.hold: - case CallEvent.unmute: - case CallEvent.mute: - case CallEvent.speakerOn: - case CallEvent.speakerOff: - case CallEvent.bluetoothOn: - case CallEvent.bluetoothOff: - _updateStates(); - break; - - case CallEvent.connected: - activeCall = true; - _updateStates(); - break; - - case CallEvent.callEnded: - activeCall = false; - _updateStates(); - break; - - case CallEvent.incoming: - case CallEvent.ringing: - case CallEvent.declined: - case CallEvent.answer: - case CallEvent.missedCall: - case CallEvent.returningCall: - case CallEvent.reconnecting: - case CallEvent.reconnected: - _updateStates(); - break; - - case CallEvent.log: - break; - - case CallEvent.permission: - // Using app lifecycle states, we don't have to update permissions here - convenience only. - break; - } - }); _updatePermissions(); - _updateStates(); - } - - void _updateStates() { - // get all states from call - _tv.call.isMuted().then((value) => stateMute = value ?? false); - _tv.call.isHolding().then((value) => stateHold = value ?? false); - _tv.call.isOnSpeaker().then((value) => stateSpeaker = value ?? false); - _tv.call.isBluetoothOn().then((value) => stateBluetooth = value ?? false); } void _updatePermissions() { @@ -216,66 +126,9 @@ class _PermissionsBlockState extends State with WidgetsBinding return SingleChildScrollView( child: Column( children: [ - // state - Text("State", style: Theme.of(context).textTheme.titleLarge), - Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - Expanded( - child: StateToggle( - state: _stateMute, - icon: _stateMute ? Icons.mic : Icons.mic_off, - title: "Mute", - onTap: () => _tv.call.toggleMute(!_stateMute), - ), - ), - Expanded( - child: StateToggle( - state: _stateHold, - icon: Icons.pause, - title: "Hold", - iconColor: _stateHold ? Colors.orange : null, - onTap: () => _tv.call.holdCall(holdCall: !_stateHold), - ), - ), - Expanded( - child: StateToggle( - state: _stateSpeaker, - icon: Icons.volume_up, - title: "Speaker", - iconColor: _stateSpeaker ? Colors.green : null, - onTap: () => _tv.call.toggleSpeaker(!_stateSpeaker), - ), - ), - Expanded( - child: StateToggle( - state: _stateBluetooth, - icon: Icons.bluetooth, - title: "Bluetooth", - iconColor: _stateBluetooth ? Colors.blue : null, - onTap: () => _tv.call.toggleBluetooth(bluetoothOn: !_stateBluetooth), - ), - ), - ], - ), - Row( - children: [ - Expanded( - child: StateToggle( - state: activeCall, - icon: Icons.call_end, - title: "Hangup", - iconColor: Colors.red, - onTap: activeCall ? () => _tv.call.hangUp() : null, - ), - ), - ], - ), - - const SizedBox(height: 12), - // permissions Text("Permissions", style: Theme.of(context).textTheme.titleLarge), + Column( children: [ PermissionTile( @@ -375,17 +228,6 @@ class _PermissionsBlockState extends State with WidgetsBinding ), ], ), - - Text("Events (latest at top)", style: Theme.of(context).textTheme.titleLarge), - - ListView.separated( - shrinkWrap: true, - reverse: true, - itemBuilder: (context, index) => Text(_events[index].toString()), - separatorBuilder: (context, index) => const Divider(height: 2, thickness: 0.5), - itemCount: _events.length, - physics: const NeverScrollableScrollPhysics(), - ), ], ), ); diff --git a/example/lib/screens/widgets/twilio_log.dart b/example/lib/screens/widgets/twilio_log.dart new file mode 100644 index 00000000..f5aa007d --- /dev/null +++ b/example/lib/screens/widgets/twilio_log.dart @@ -0,0 +1,48 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:twilio_voice/twilio_voice.dart'; + +class TwilioLog extends StatefulWidget { + const TwilioLog({super.key}); + + @override + State createState() => _TwilioLogState(); +} + +class _TwilioLogState extends State { + late final StreamSubscription _subscription; + final _tv = TwilioVoice.instance; + final _events = []; + + @override + void initState() { + super.initState(); + _subscription = _tv.callEventsListener.listen((event) { + setState(() { + _events.insert(0, event); + }); + }); + } + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16), + child: ListView.separated( + shrinkWrap: true, + reverse: true, + itemBuilder: (context, index) => Text(_events[index].toString()), + separatorBuilder: (context, index) => const Divider(height: 2, thickness: 0.5), + itemCount: _events.length, + physics: const NeverScrollableScrollPhysics(), + ), + ); + } + + @override + void dispose() { + _subscription.cancel(); + super.dispose(); + } +}