implementing Twilio in LiveCare
parent
db30fbdfd2
commit
5f1413a2ac
@ -0,0 +1,104 @@
|
||||
class IncomingCallData {
|
||||
String msgID;
|
||||
String notfID;
|
||||
String notificationForeground;
|
||||
String count;
|
||||
String message;
|
||||
String appointmentNo;
|
||||
String title;
|
||||
String projectID;
|
||||
String notificationType;
|
||||
String background;
|
||||
String doctorname;
|
||||
String clinicname;
|
||||
String speciality;
|
||||
String appointmentdate;
|
||||
String appointmenttime;
|
||||
String type;
|
||||
String sessionId;
|
||||
String identity;
|
||||
String name;
|
||||
String videoUrl;
|
||||
String picture;
|
||||
String isCall;
|
||||
String sound;
|
||||
|
||||
IncomingCallData(
|
||||
{this.msgID,
|
||||
this.notfID,
|
||||
this.notificationForeground,
|
||||
this.count,
|
||||
this.message,
|
||||
this.appointmentNo,
|
||||
this.title,
|
||||
this.projectID,
|
||||
this.notificationType,
|
||||
this.background,
|
||||
this.doctorname,
|
||||
this.clinicname,
|
||||
this.speciality,
|
||||
this.appointmentdate,
|
||||
this.appointmenttime,
|
||||
this.type,
|
||||
this.sessionId,
|
||||
this.identity,
|
||||
this.name,
|
||||
this.videoUrl,
|
||||
this.picture,
|
||||
this.isCall,
|
||||
this.sound});
|
||||
|
||||
IncomingCallData.fromJson(Map<String, dynamic> json) {
|
||||
msgID = json['msgID'];
|
||||
notfID = json['notfID'];
|
||||
notificationForeground = json['notification_foreground'];
|
||||
count = json['count'];
|
||||
message = json['message'];
|
||||
appointmentNo = json['AppointmentNo'];
|
||||
title = json['title'];
|
||||
projectID = json['ProjectID'];
|
||||
notificationType = json['NotificationType'];
|
||||
background = json['background'];
|
||||
doctorname = json['doctorname'];
|
||||
clinicname = json['clinicname'];
|
||||
speciality = json['speciality'];
|
||||
appointmentdate = json['appointmentdate'];
|
||||
appointmenttime = json['appointmenttime'];
|
||||
type = json['type'];
|
||||
sessionId = json['session_id'];
|
||||
identity = json['identity'];
|
||||
name = json['name'];
|
||||
videoUrl = json['videoUrl'];
|
||||
picture = json['picture'];
|
||||
isCall = json['is_call'];
|
||||
sound = json['sound'];
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final Map<String, dynamic> data = new Map<String, dynamic>();
|
||||
data['msgID'] = this.msgID;
|
||||
data['notfID'] = this.notfID;
|
||||
data['notification_foreground'] = this.notificationForeground;
|
||||
data['count'] = this.count;
|
||||
data['message'] = this.message;
|
||||
data['AppointmentNo'] = this.appointmentNo;
|
||||
data['title'] = this.title;
|
||||
data['ProjectID'] = this.projectID;
|
||||
data['NotificationType'] = this.notificationType;
|
||||
data['background'] = this.background;
|
||||
data['doctorname'] = this.doctorname;
|
||||
data['clinicname'] = this.clinicname;
|
||||
data['speciality'] = this.speciality;
|
||||
data['appointmentdate'] = this.appointmentdate;
|
||||
data['appointmenttime'] = this.appointmenttime;
|
||||
data['type'] = this.type;
|
||||
data['session_id'] = this.sessionId;
|
||||
data['identity'] = this.identity;
|
||||
data['name'] = this.name;
|
||||
data['videoUrl'] = this.videoUrl;
|
||||
data['picture'] = this.picture;
|
||||
data['is_call'] = this.isCall;
|
||||
data['sound'] = this.sound;
|
||||
return data;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,65 @@
|
||||
import 'package:diplomaticquarterapp/models/LiveCare/room_validators.dart';
|
||||
import 'package:diplomaticquarterapp/models/LiveCare/twilio_enums.dart';
|
||||
|
||||
class RoomModel with RoomValidators {
|
||||
final String name;
|
||||
final bool isLoading;
|
||||
final bool isSubmitted;
|
||||
final String token;
|
||||
final String identity;
|
||||
final TwilioRoomType type;
|
||||
|
||||
RoomModel({
|
||||
this.name,
|
||||
this.isLoading = false,
|
||||
this.isSubmitted = false,
|
||||
this.token,
|
||||
this.identity,
|
||||
this.type = TwilioRoomType.groupSmall,
|
||||
});
|
||||
|
||||
static String getTypeText(TwilioRoomType type) {
|
||||
switch (type) {
|
||||
case TwilioRoomType.peerToPeer:
|
||||
return 'peer 2 peer';
|
||||
break;
|
||||
case TwilioRoomType.group:
|
||||
return 'large (max 50 participants)';
|
||||
break;
|
||||
case TwilioRoomType.groupSmall:
|
||||
return 'small (max 4 participants)';
|
||||
break;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
String get nameErrorText {
|
||||
return isSubmitted && !nameValidator.isValid(name) ? invalidNameErrorText : null;
|
||||
}
|
||||
|
||||
String get typeText {
|
||||
return RoomModel.getTypeText(type);
|
||||
}
|
||||
|
||||
bool get canSubmit {
|
||||
return nameValidator.isValid(name);
|
||||
}
|
||||
|
||||
RoomModel copyWith({
|
||||
String name,
|
||||
bool isLoading,
|
||||
bool isSubmitted,
|
||||
String token,
|
||||
String identity,
|
||||
TwilioRoomType type,
|
||||
}) {
|
||||
return RoomModel(
|
||||
name: name ?? this.name,
|
||||
token: token ?? this.token,
|
||||
identity: identity ?? this.identity,
|
||||
isLoading: isLoading ?? this.isLoading,
|
||||
isSubmitted: isSubmitted ?? this.isSubmitted,
|
||||
type: type ?? this.type,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,6 @@
|
||||
import 'package:diplomaticquarterapp/models/LiveCare/validators.dart';
|
||||
|
||||
mixin RoomValidators {
|
||||
final StringValidator nameValidator = NonEmptyStringValidator();
|
||||
final String invalidNameErrorText = 'Room name can\'t be empty';
|
||||
}
|
||||
@ -0,0 +1,17 @@
|
||||
enum TwilioRoomType {
|
||||
peerToPeer,
|
||||
group,
|
||||
groupSmall,
|
||||
}
|
||||
|
||||
enum TwilioRoomStatus {
|
||||
completed,
|
||||
inProgress,
|
||||
}
|
||||
|
||||
enum TwilioStatusCallbackMethod {
|
||||
GET,
|
||||
POST,
|
||||
}
|
||||
|
||||
enum TwilioVideoCodec { VP8, H264 }
|
||||
@ -0,0 +1,13 @@
|
||||
abstract class StringValidator {
|
||||
bool isValid(String value);
|
||||
}
|
||||
|
||||
class NonEmptyStringValidator implements StringValidator {
|
||||
@override
|
||||
bool isValid(String value) {
|
||||
if (value == null) {
|
||||
return false;
|
||||
}
|
||||
return value.isNotEmpty;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,37 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class ClippedVideo extends StatefulWidget {
|
||||
final double width;
|
||||
final double height;
|
||||
final Widget child;
|
||||
|
||||
const ClippedVideo({
|
||||
Key key,
|
||||
@required this.width,
|
||||
@required this.height,
|
||||
@required this.child,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
_ClippedVideoState createState() => _ClippedVideoState();
|
||||
}
|
||||
|
||||
class _ClippedVideoState extends State<ClippedVideo> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
width: widget.width,
|
||||
height: widget.height,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(20)),
|
||||
border: Border.all(
|
||||
color: Colors.white24,
|
||||
),
|
||||
),
|
||||
child: ClipRRect(
|
||||
child: widget.child,
|
||||
borderRadius: const BorderRadius.all(Radius.circular(20)),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,244 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:after_layout/after_layout.dart';
|
||||
import 'package:diplomaticquarterapp/pages/conference/widgets/circle_button.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class ConferenceButtonBar extends StatefulWidget {
|
||||
final VoidCallback onVideoEnabled;
|
||||
final VoidCallback onAudioEnabled;
|
||||
final VoidCallback onHangup;
|
||||
final VoidCallback onSwitchCamera;
|
||||
final VoidCallback onPersonAdd;
|
||||
final VoidCallback onPersonRemove;
|
||||
final void Function(double) onHeight;
|
||||
final VoidCallback onHide;
|
||||
final VoidCallback onShow;
|
||||
final Stream<bool> videoEnabled;
|
||||
final Stream<bool> audioEnabled;
|
||||
|
||||
const ConferenceButtonBar({
|
||||
Key key,
|
||||
this.onVideoEnabled,
|
||||
this.onAudioEnabled,
|
||||
this.onHangup,
|
||||
this.onSwitchCamera,
|
||||
this.onPersonAdd,
|
||||
this.onPersonRemove,
|
||||
@required this.videoEnabled,
|
||||
@required this.audioEnabled,
|
||||
this.onHeight,
|
||||
this.onHide,
|
||||
this.onShow,
|
||||
}) : assert(videoEnabled != null),
|
||||
assert(audioEnabled != null),
|
||||
super(key: key);
|
||||
|
||||
@override
|
||||
_ConferenceButtonBarState createState() => _ConferenceButtonBarState();
|
||||
}
|
||||
|
||||
class _ConferenceButtonBarState extends State<ConferenceButtonBar> with AfterLayoutMixin<ConferenceButtonBar> {
|
||||
var _bottom = -100.0;
|
||||
Timer _timer;
|
||||
int _remaining;
|
||||
var _videoEnabled = true;
|
||||
var _audioEnabled = true;
|
||||
double _hidden;
|
||||
double _visible;
|
||||
final _keyButtonBarHeight = GlobalKey();
|
||||
|
||||
final Duration timeout = const Duration(seconds: 5);
|
||||
final Duration ms = const Duration(milliseconds: 1);
|
||||
final Duration periodicDuration = const Duration(milliseconds: 100);
|
||||
|
||||
Timer startTimeout([int milliseconds]) {
|
||||
final duration = milliseconds == null ? timeout : ms * milliseconds;
|
||||
_remaining = duration.inMilliseconds;
|
||||
return Timer.periodic(periodicDuration, (Timer timer) {
|
||||
_remaining -= periodicDuration.inMilliseconds;
|
||||
if (_remaining <= 0) {
|
||||
timer.cancel();
|
||||
_toggleBar();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void _pauseTimer() {
|
||||
if (_timer == null) {
|
||||
return;
|
||||
}
|
||||
_timer.cancel();
|
||||
_timer = null;
|
||||
}
|
||||
|
||||
void _resumeTimer() {
|
||||
// resume the timer only when there is no timer active or when
|
||||
// the bar is not already hidden.
|
||||
if ((_timer != null && _timer.isActive) || _bottom == _hidden) {
|
||||
return;
|
||||
}
|
||||
_timer = startTimeout(_remaining);
|
||||
}
|
||||
|
||||
void _toggleBar() {
|
||||
setState(() {
|
||||
_bottom = _bottom == _visible ? _hidden : _visible;
|
||||
if (_bottom == _visible && widget.onShow != null) {
|
||||
widget.onShow();
|
||||
}
|
||||
if (_bottom == _hidden && widget.onHide != null) {
|
||||
widget.onHide();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void _toggleBarOnEnd() {
|
||||
if (_timer != null) {
|
||||
if (_timer.isActive) {
|
||||
_timer.cancel();
|
||||
}
|
||||
_timer = null;
|
||||
}
|
||||
if (_bottom == 0) {
|
||||
_timer = startTimeout();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_timer = startTimeout();
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
_visible = MediaQuery.of(context).viewPadding.bottom;
|
||||
super.didChangeDependencies();
|
||||
}
|
||||
|
||||
@override
|
||||
void afterFirstLayout(BuildContext context) {
|
||||
final RenderBox renderBoxButtonBar = _keyButtonBarHeight.currentContext.findRenderObject();
|
||||
final heightButtonBar = renderBoxButtonBar.size.height;
|
||||
// Because the `didChangeDependencies` fires before the `afterFirstLayout`, we can use the `_visible` property here.
|
||||
_hidden = -(heightButtonBar + _visible);
|
||||
widget.onHeight(heightButtonBar);
|
||||
_toggleBar();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
if (_timer != null && _timer.isActive) {
|
||||
_timer.cancel();
|
||||
_timer = null;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Positioned(
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
child: GestureDetector(
|
||||
key: Key('show-hide-button-bar-gesture'),
|
||||
behavior: HitTestBehavior.translucent,
|
||||
onTapDown: (_) => _pauseTimer(),
|
||||
onTapUp: (_) => _toggleBar(),
|
||||
onTapCancel: () => _resumeTimer(),
|
||||
child: Stack(
|
||||
children: <Widget>[
|
||||
AnimatedPositioned(
|
||||
key: Key('button-bar'),
|
||||
bottom: _bottom,
|
||||
left: 0,
|
||||
right: 0,
|
||||
duration: const Duration(milliseconds: 300),
|
||||
curve: Curves.linear,
|
||||
child: _buildRow(context),
|
||||
onEnd: _toggleBarOnEnd,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _onPressed(VoidCallback callback) {
|
||||
if (callback != null) {
|
||||
callback();
|
||||
}
|
||||
if (_timer != null && _timer.isActive) {
|
||||
_timer.cancel();
|
||||
}
|
||||
_timer = startTimeout();
|
||||
}
|
||||
|
||||
Widget _buildRow(BuildContext context) {
|
||||
return Padding(
|
||||
key: _keyButtonBarHeight,
|
||||
padding: const EdgeInsets.only(bottom: 8.0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: <Widget>[
|
||||
CircleButton(
|
||||
child: StreamBuilder<bool>(
|
||||
stream: widget.videoEnabled,
|
||||
initialData: _videoEnabled,
|
||||
builder: (context, snapshot) {
|
||||
_videoEnabled = snapshot.data;
|
||||
return Icon(
|
||||
_videoEnabled ? Icons.videocam : Icons.videocam_off,
|
||||
color: Colors.white,
|
||||
);
|
||||
}),
|
||||
key: Key('camera-button'),
|
||||
onPressed: () => _onPressed(widget.onVideoEnabled),
|
||||
),
|
||||
CircleButton(
|
||||
child: StreamBuilder<bool>(
|
||||
stream: widget.audioEnabled,
|
||||
initialData: _audioEnabled,
|
||||
builder: (context, snapshot) {
|
||||
_audioEnabled = snapshot.data;
|
||||
return Icon(
|
||||
_audioEnabled ? Icons.mic : Icons.mic_off,
|
||||
color: Colors.white,
|
||||
);
|
||||
}),
|
||||
key: Key('microphone-button'),
|
||||
onPressed: () => _onPressed(widget.onAudioEnabled),
|
||||
),
|
||||
CircleButton(
|
||||
radius: 35,
|
||||
child: const RotationTransition(
|
||||
turns: AlwaysStoppedAnimation<double>(135 / 360),
|
||||
child: Icon(
|
||||
Icons.phone,
|
||||
color: Colors.white,
|
||||
size: 40,
|
||||
),
|
||||
),
|
||||
color: Colors.red.withAlpha(200),
|
||||
key: Key('hangup-button'),
|
||||
onPressed: () => _onPressed(widget.onHangup),
|
||||
),
|
||||
CircleButton(
|
||||
child: const Icon(Icons.switch_camera, color: Colors.white),
|
||||
key: Key('switch-camera-button'),
|
||||
onPressed: () => _onPressed(widget.onSwitchCamera),
|
||||
),
|
||||
CircleButton(
|
||||
child: const Icon(Icons.person_add, color: Colors.white),
|
||||
key: Key('add-person-button'),
|
||||
onPressed: () => _onPressed(widget.onPersonAdd),
|
||||
onLongPress: () => _onPressed(widget.onPersonRemove),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,388 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:diplomaticquarterapp/models/LiveCare/room_model.dart';
|
||||
import 'package:diplomaticquarterapp/pages/conference/conference_button_bar.dart';
|
||||
import 'package:diplomaticquarterapp/pages/conference/conference_room.dart';
|
||||
import 'package:diplomaticquarterapp/pages/conference/draggable_publisher.dart';
|
||||
import 'package:diplomaticquarterapp/pages/conference/participant_widget.dart';
|
||||
import 'package:diplomaticquarterapp/pages/conference/widgets/noise_box.dart';
|
||||
import 'package:diplomaticquarterapp/pages/conference/widgets/platform_alert_dialog.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:wakelock/wakelock.dart';
|
||||
|
||||
class ConferencePage extends StatefulWidget {
|
||||
final RoomModel roomModel;
|
||||
|
||||
const ConferencePage({Key key, this.roomModel}) : super(key: key);
|
||||
|
||||
@override
|
||||
_ConferencePageState createState() => _ConferencePageState();
|
||||
}
|
||||
|
||||
class _ConferencePageState extends State<ConferencePage> {
|
||||
final StreamController<bool> _onButtonBarVisibleStreamController = StreamController<bool>.broadcast();
|
||||
final StreamController<double> _onButtonBarHeightStreamController = StreamController<double>.broadcast();
|
||||
ConferenceRoom _conferenceRoom;
|
||||
StreamSubscription _onConferenceRoomException;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_lockInPortrait();
|
||||
_connectToRoom();
|
||||
_wakeLock(true);
|
||||
}
|
||||
|
||||
void _connectToRoom() async {
|
||||
try {
|
||||
final conferenceRoom = ConferenceRoom(
|
||||
name: widget.roomModel.name,
|
||||
token: widget.roomModel.token,
|
||||
identity: widget.roomModel.identity,
|
||||
);
|
||||
await conferenceRoom.connect();
|
||||
setState(() {
|
||||
_conferenceRoom = conferenceRoom;
|
||||
_onConferenceRoomException = _conferenceRoom.onException.listen((err) async {
|
||||
await PlatformAlertDialog(
|
||||
title: err is PlatformException ? err.message : 'An error occured',
|
||||
content: err is PlatformException ? err.details : err.toString(),
|
||||
defaultActionText: 'OK',
|
||||
).show(context);
|
||||
});
|
||||
_conferenceRoom.addListener(_conferenceRoomUpdated);
|
||||
});
|
||||
} catch (err) {
|
||||
print(err);
|
||||
await PlatformAlertDialog(
|
||||
title: err is PlatformException ? err.message : 'An error occured',
|
||||
content: err is PlatformException ? err.details : err.toString(),
|
||||
defaultActionText: 'OK',
|
||||
).show(context);
|
||||
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _lockInPortrait() async {
|
||||
await SystemChrome.setPreferredOrientations(<DeviceOrientation>[
|
||||
DeviceOrientation.portraitUp,
|
||||
DeviceOrientation.portraitDown,
|
||||
]);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_freePortraitLock();
|
||||
_wakeLock(false);
|
||||
_disposeStreamsAndSubscriptions();
|
||||
if (_conferenceRoom != null) _conferenceRoom.removeListener(_conferenceRoomUpdated);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _freePortraitLock() async {
|
||||
await SystemChrome.setPreferredOrientations(<DeviceOrientation>[
|
||||
DeviceOrientation.landscapeRight,
|
||||
DeviceOrientation.landscapeLeft,
|
||||
DeviceOrientation.portraitUp,
|
||||
DeviceOrientation.portraitDown,
|
||||
]);
|
||||
}
|
||||
|
||||
Future<void> _disposeStreamsAndSubscriptions() async {
|
||||
if (_onButtonBarVisibleStreamController != null) await _onButtonBarVisibleStreamController.close();
|
||||
if (_onButtonBarHeightStreamController != null) await _onButtonBarHeightStreamController.close();
|
||||
if (_onConferenceRoomException != null) await _onConferenceRoomException.cancel();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return WillPopScope(
|
||||
onWillPop: () async => false,
|
||||
child: Scaffold(
|
||||
backgroundColor: Colors.black,
|
||||
body: _conferenceRoom == null ? showProgress() : buildLayout(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
LayoutBuilder buildLayout() {
|
||||
return LayoutBuilder(
|
||||
builder: (BuildContext context, BoxConstraints constraints) {
|
||||
return Stack(
|
||||
children: <Widget>[
|
||||
_buildParticipants(context, constraints.biggest, _conferenceRoom),
|
||||
ConferenceButtonBar(
|
||||
audioEnabled: _conferenceRoom.onAudioEnabled,
|
||||
videoEnabled: _conferenceRoom.onVideoEnabled,
|
||||
onAudioEnabled: _conferenceRoom.toggleAudioEnabled,
|
||||
onVideoEnabled: _conferenceRoom.toggleVideoEnabled,
|
||||
onHangup: _onHangup,
|
||||
onSwitchCamera: _conferenceRoom.switchCamera,
|
||||
onPersonAdd: _onPersonAdd,
|
||||
onPersonRemove: _onPersonRemove,
|
||||
onHeight: _onHeightBar,
|
||||
onShow: _onShowBar,
|
||||
onHide: _onHideBar,
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget showProgress() {
|
||||
return Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
Center(child: CircularProgressIndicator()),
|
||||
SizedBox(
|
||||
height: 10,
|
||||
),
|
||||
Text(
|
||||
'Connecting to the room...',
|
||||
style: TextStyle(color: Colors.white),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _onHangup() async {
|
||||
print('onHangup');
|
||||
await _conferenceRoom.disconnect();
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
|
||||
void _onPersonAdd() {
|
||||
print('onPersonAdd');
|
||||
try {
|
||||
_conferenceRoom.addDummy(
|
||||
child: Stack(
|
||||
children: <Widget>[
|
||||
const Placeholder(),
|
||||
Center(
|
||||
child: Text(
|
||||
(_conferenceRoom.participants.length + 1).toString(),
|
||||
style: const TextStyle(
|
||||
shadows: <Shadow>[
|
||||
Shadow(
|
||||
blurRadius: 3.0,
|
||||
color: Color.fromARGB(255, 0, 0, 0),
|
||||
),
|
||||
Shadow(
|
||||
blurRadius: 8.0,
|
||||
color: Color.fromARGB(255, 255, 255, 255),
|
||||
),
|
||||
],
|
||||
fontSize: 80,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
} on PlatformException catch (err) {
|
||||
PlatformAlertDialog(
|
||||
title: err.message,
|
||||
content: err.details,
|
||||
defaultActionText: 'OK',
|
||||
).show(context);
|
||||
}
|
||||
}
|
||||
|
||||
void _onPersonRemove() {
|
||||
print('onPersonRemove');
|
||||
_conferenceRoom.removeDummy();
|
||||
}
|
||||
|
||||
Widget _buildParticipants(BuildContext context, Size size, ConferenceRoom conferenceRoom) {
|
||||
final children = <Widget>[];
|
||||
final length = conferenceRoom.participants.length;
|
||||
|
||||
if (length <= 2) {
|
||||
_buildOverlayLayout(context, size, children);
|
||||
return Stack(children: children);
|
||||
}
|
||||
|
||||
void buildInCols(bool removeLocalBeforeChunking, bool moveLastOfEachRowToNextRow, int columns) {
|
||||
_buildLayoutInGrid(
|
||||
context,
|
||||
size,
|
||||
children,
|
||||
removeLocalBeforeChunking: removeLocalBeforeChunking,
|
||||
moveLastOfEachRowToNextRow: moveLastOfEachRowToNextRow,
|
||||
columns: columns,
|
||||
);
|
||||
}
|
||||
|
||||
if (length <= 3) {
|
||||
buildInCols(true, false, 1);
|
||||
} else if (length == 5) {
|
||||
buildInCols(false, true, 2);
|
||||
} else if (length <= 6 || length == 8) {
|
||||
buildInCols(false, false, 2);
|
||||
} else if (length == 7 || length == 9) {
|
||||
buildInCols(true, false, 2);
|
||||
} else if (length == 10) {
|
||||
buildInCols(false, true, 3);
|
||||
} else if (length == 13 || length == 16) {
|
||||
buildInCols(true, false, 3);
|
||||
} else if (length <= 18) {
|
||||
buildInCols(false, false, 3);
|
||||
}
|
||||
|
||||
return Column(
|
||||
children: children,
|
||||
);
|
||||
}
|
||||
|
||||
void _buildOverlayLayout(BuildContext context, Size size, List<Widget> children) {
|
||||
final participants = _conferenceRoom.participants;
|
||||
if (participants.length == 1) {
|
||||
children.add(_buildNoiseBox());
|
||||
} else {
|
||||
final remoteParticipant = participants.firstWhere((ParticipantWidget participant) => participant.isRemote, orElse: () => null);
|
||||
if (remoteParticipant != null) {
|
||||
children.add(remoteParticipant);
|
||||
}
|
||||
}
|
||||
|
||||
final localParticipant = participants.firstWhere((ParticipantWidget participant) => !participant.isRemote, orElse: () => null);
|
||||
if (localParticipant != null) {
|
||||
children.add(DraggablePublisher(
|
||||
key: Key('publisher'),
|
||||
child: localParticipant,
|
||||
availableScreenSize: size,
|
||||
onButtonBarVisible: _onButtonBarVisibleStreamController.stream,
|
||||
onButtonBarHeight: _onButtonBarHeightStreamController.stream,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
void _buildLayoutInGrid(
|
||||
BuildContext context,
|
||||
Size size,
|
||||
List<Widget> children, {
|
||||
bool removeLocalBeforeChunking = false,
|
||||
bool moveLastOfEachRowToNextRow = false,
|
||||
int columns = 2,
|
||||
}) {
|
||||
final participants = _conferenceRoom.participants;
|
||||
ParticipantWidget localParticipant;
|
||||
if (removeLocalBeforeChunking) {
|
||||
localParticipant = participants.firstWhere((ParticipantWidget participant) => !participant.isRemote, orElse: () => null);
|
||||
if (localParticipant != null) {
|
||||
participants.remove(localParticipant);
|
||||
}
|
||||
}
|
||||
final chunkedParticipants = chunk(array: participants, size: columns);
|
||||
if (localParticipant != null) {
|
||||
chunkedParticipants.last.add(localParticipant);
|
||||
participants.add(localParticipant);
|
||||
}
|
||||
|
||||
if (moveLastOfEachRowToNextRow) {
|
||||
for (var i = 0; i < chunkedParticipants.length - 1; i++) {
|
||||
var participant = chunkedParticipants[i].removeLast();
|
||||
chunkedParticipants[i + 1].insert(0, participant);
|
||||
}
|
||||
}
|
||||
|
||||
for (final participantChunk in chunkedParticipants) {
|
||||
final rowChildren = <Widget>[];
|
||||
for (final participant in participantChunk) {
|
||||
rowChildren.add(
|
||||
Container(
|
||||
width: size.width / participantChunk.length,
|
||||
height: size.height / chunkedParticipants.length,
|
||||
child: participant,
|
||||
),
|
||||
);
|
||||
}
|
||||
children.add(
|
||||
Container(
|
||||
height: size.height / chunkedParticipants.length,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: rowChildren,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
NoiseBox _buildNoiseBox() {
|
||||
return NoiseBox(
|
||||
density: NoiseBoxDensity.xLow,
|
||||
backgroundColor: Colors.grey.shade900,
|
||||
child: Center(
|
||||
child: Container(
|
||||
color: Colors.black54,
|
||||
width: double.infinity,
|
||||
height: 40,
|
||||
child: Center(
|
||||
child: Text(
|
||||
'Waiting for another participant to connect to the room...',
|
||||
key: Key('text-wait'),
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(color: Colors.white),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
List<List<T>> chunk<T>({@required List<T> array, @required int size}) {
|
||||
final result = <List<T>>[];
|
||||
if (array.isEmpty || size <= 0) {
|
||||
return result;
|
||||
}
|
||||
var first = 0;
|
||||
var last = size;
|
||||
final totalLoop = array.length % size == 0 ? array.length ~/ size : array.length ~/ size + 1;
|
||||
for (var i = 0; i < totalLoop; i++) {
|
||||
if (last > array.length) {
|
||||
result.add(array.sublist(first, array.length));
|
||||
} else {
|
||||
result.add(array.sublist(first, last));
|
||||
}
|
||||
first = last;
|
||||
last = last + size;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void _onHeightBar(double height) {
|
||||
_onButtonBarHeightStreamController.add(height);
|
||||
}
|
||||
|
||||
void _onShowBar() {
|
||||
setState(() {
|
||||
SystemChrome.setEnabledSystemUIOverlays([SystemUiOverlay.bottom, SystemUiOverlay.top]);
|
||||
});
|
||||
_onButtonBarVisibleStreamController.add(true);
|
||||
}
|
||||
|
||||
void _onHideBar() {
|
||||
setState(() {
|
||||
SystemChrome.setEnabledSystemUIOverlays([SystemUiOverlay.bottom]);
|
||||
});
|
||||
_onButtonBarVisibleStreamController.add(false);
|
||||
}
|
||||
|
||||
Future<void> _wakeLock(bool enable) async {
|
||||
try {
|
||||
return await (enable ? Wakelock.enable() : Wakelock.disable());
|
||||
} catch (err) {
|
||||
print('Unable to change the Wakelock and set it to $enable');
|
||||
print(err);
|
||||
}
|
||||
}
|
||||
|
||||
void _conferenceRoomUpdated() {
|
||||
setState(() {});
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,530 @@
|
||||
import 'dart:async';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:diplomaticquarterapp/pages/conference/participant_widget.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:twilio_programmable_video/twilio_programmable_video.dart';
|
||||
|
||||
class ConferenceRoom with ChangeNotifier {
|
||||
final String name;
|
||||
final String token;
|
||||
final String identity;
|
||||
|
||||
final StreamController<bool> _onAudioEnabledStreamController = StreamController<bool>.broadcast();
|
||||
Stream<bool> onAudioEnabled;
|
||||
final StreamController<bool> _onVideoEnabledStreamController = StreamController<bool>.broadcast();
|
||||
Stream<bool> onVideoEnabled;
|
||||
final StreamController<Exception> _onExceptionStreamController = StreamController<Exception>.broadcast();
|
||||
Stream<Exception> onException;
|
||||
|
||||
final Completer<Room> _completer = Completer<Room>();
|
||||
|
||||
final List<ParticipantWidget> _participants = [];
|
||||
final List<ParticipantBuffer> _participantBuffer = [];
|
||||
final List<StreamSubscription> _streamSubscriptions = [];
|
||||
final List<RemoteDataTrack> _dataTracks = [];
|
||||
final List<String> _messages = [];
|
||||
|
||||
CameraCapturer _cameraCapturer;
|
||||
Room _room;
|
||||
Timer _timer;
|
||||
|
||||
ConferenceRoom({
|
||||
@required this.name,
|
||||
@required this.token,
|
||||
@required this.identity,
|
||||
}) {
|
||||
onAudioEnabled = _onAudioEnabledStreamController.stream;
|
||||
onVideoEnabled = _onVideoEnabledStreamController.stream;
|
||||
onException = _onExceptionStreamController.stream;
|
||||
}
|
||||
|
||||
List<ParticipantWidget> get participants {
|
||||
return [..._participants];
|
||||
}
|
||||
|
||||
Future<Room> connect() async {
|
||||
print('ConferenceRoom.connect()');
|
||||
try {
|
||||
await TwilioProgrammableVideo.debug(dart: true, native: true);
|
||||
await TwilioProgrammableVideo.setSpeakerphoneOn(true);
|
||||
|
||||
_cameraCapturer = CameraCapturer(CameraSource.FRONT_CAMERA);
|
||||
var connectOptions = ConnectOptions(
|
||||
token,
|
||||
roomName: name,
|
||||
preferredAudioCodecs: [OpusCodec()],
|
||||
audioTracks: [LocalAudioTrack(true)],
|
||||
dataTracks: [LocalDataTrack()],
|
||||
videoTracks: [LocalVideoTrack(true, _cameraCapturer)],
|
||||
enableDominantSpeaker: true,
|
||||
);
|
||||
|
||||
_room = await TwilioProgrammableVideo.connect(connectOptions);
|
||||
|
||||
_streamSubscriptions.add(_room.onConnected.listen(_onConnected));
|
||||
_streamSubscriptions.add(_room.onConnectFailure.listen(_onConnectFailure));
|
||||
|
||||
return _completer.future;
|
||||
} catch (err) {
|
||||
print(err);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> disconnect() async {
|
||||
print('ConferenceRoom.disconnect()');
|
||||
if (_timer != null) {
|
||||
_timer.cancel();
|
||||
}
|
||||
await _room.disconnect();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
print('ConferenceRoom.dispose()');
|
||||
_disposeStreamsAndSubscriptions();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _disposeStreamsAndSubscriptions() async {
|
||||
await _onAudioEnabledStreamController.close();
|
||||
await _onVideoEnabledStreamController.close();
|
||||
await _onExceptionStreamController.close();
|
||||
for (var streamSubscription in _streamSubscriptions) {
|
||||
await streamSubscription.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> sendMessage(String message) async {
|
||||
final tracks = _room.localParticipant.localDataTracks;
|
||||
final localDataTrack = tracks.isEmpty ? null : tracks[0].localDataTrack;
|
||||
if (localDataTrack == null || _messages.isNotEmpty) {
|
||||
print('ConferenceRoom.sendMessage => Track is not available yet, buffering message.');
|
||||
_messages.add(message);
|
||||
return;
|
||||
}
|
||||
await localDataTrack.send(message);
|
||||
}
|
||||
|
||||
Future<void> sendBufferMessage(ByteBuffer message) async {
|
||||
final tracks = _room.localParticipant.localDataTracks;
|
||||
final localDataTrack = tracks.isEmpty ? null : tracks[0].localDataTrack;
|
||||
if (localDataTrack == null) {
|
||||
return;
|
||||
}
|
||||
await localDataTrack.sendBuffer(message);
|
||||
}
|
||||
|
||||
Future<void> toggleVideoEnabled() async {
|
||||
final tracks = _room.localParticipant.localVideoTracks;
|
||||
final localVideoTrack = tracks.isEmpty ? null : tracks[0].localVideoTrack;
|
||||
if (localVideoTrack == null) {
|
||||
print('ConferenceRoom.toggleVideoEnabled() => Track is not available yet!');
|
||||
return;
|
||||
}
|
||||
await localVideoTrack.enable(!localVideoTrack.isEnabled);
|
||||
|
||||
var index = _participants.indexWhere((ParticipantWidget participant) => !participant.isRemote);
|
||||
if (index < 0) {
|
||||
return;
|
||||
}
|
||||
var participant = _participants[index];
|
||||
_participants.replaceRange(
|
||||
index,
|
||||
index + 1,
|
||||
[
|
||||
participant.copyWith(videoEnabled: localVideoTrack.isEnabled),
|
||||
],
|
||||
);
|
||||
print('ConferenceRoom.toggleVideoEnabled() => ${localVideoTrack.isEnabled}');
|
||||
_onVideoEnabledStreamController.add(localVideoTrack.isEnabled);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> toggleAudioEnabled() async {
|
||||
final tracks = _room.localParticipant.localAudioTracks;
|
||||
final localAudioTrack = tracks.isEmpty ? null : tracks[0].localAudioTrack;
|
||||
if (localAudioTrack == null) {
|
||||
print('ConferenceRoom.toggleAudioEnabled() => Track is not available yet!');
|
||||
return;
|
||||
}
|
||||
await localAudioTrack.enable(!localAudioTrack.isEnabled);
|
||||
|
||||
var index = _participants.indexWhere((ParticipantWidget participant) => !participant.isRemote);
|
||||
if (index < 0) {
|
||||
return;
|
||||
}
|
||||
var participant = _participants[index];
|
||||
_participants.replaceRange(
|
||||
index,
|
||||
index + 1,
|
||||
[
|
||||
participant.copyWith(audioEnabled: localAudioTrack.isEnabled),
|
||||
],
|
||||
);
|
||||
print('ConferenceRoom.toggleAudioEnabled() => ${localAudioTrack.isEnabled}');
|
||||
_onAudioEnabledStreamController.add(localAudioTrack.isEnabled);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> switchCamera() async {
|
||||
print('ConferenceRoom.switchCamera()');
|
||||
try {
|
||||
await _cameraCapturer.switchCamera();
|
||||
} on FormatException catch (e) {
|
||||
print(
|
||||
'ConferenceRoom.switchCamera() failed because of FormatException with message: ${e.message}',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void addDummy({Widget child}) {
|
||||
print('ConferenceRoom.addDummy()');
|
||||
if (_participants.length >= 18) {
|
||||
throw PlatformException(
|
||||
code: 'ConferenceRoom.maximumReached',
|
||||
message: 'Maximum reached',
|
||||
details: 'Currently the lay-out can only render a maximum of 18 participants',
|
||||
);
|
||||
}
|
||||
_participants.insert(
|
||||
0,
|
||||
ParticipantWidget(
|
||||
id: (_participants.length + 1).toString(),
|
||||
child: child,
|
||||
isRemote: true,
|
||||
audioEnabled: true,
|
||||
videoEnabled: true,
|
||||
isDummy: true,
|
||||
),
|
||||
);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void removeDummy() {
|
||||
print('ConferenceRoom.removeDummy()');
|
||||
var dummy = _participants.firstWhere((participant) => participant.isDummy, orElse: () => null);
|
||||
if (dummy != null) {
|
||||
_participants.remove(dummy);
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
void _onConnected(Room room) {
|
||||
print('ConferenceRoom._onConnected => state: ${room.state}');
|
||||
|
||||
// When connected for the first time, add remote participant listeners
|
||||
_streamSubscriptions.add(_room.onParticipantConnected.listen(_onParticipantConnected));
|
||||
_streamSubscriptions.add(_room.onParticipantDisconnected.listen(_onParticipantDisconnected));
|
||||
_streamSubscriptions.add(_room.onDominantSpeakerChange.listen(_onDominantSpeakerChanged));
|
||||
// Only add ourselves when connected for the first time too.
|
||||
_participants.add(
|
||||
_buildParticipant(
|
||||
child: room.localParticipant.localVideoTracks[0].localVideoTrack.widget(),
|
||||
id: identity,
|
||||
audioEnabled: true,
|
||||
videoEnabled: true,
|
||||
),
|
||||
);
|
||||
for (final remoteParticipant in room.remoteParticipants) {
|
||||
var participant = _participants.firstWhere((participant) => participant.id == remoteParticipant.sid, orElse: () => null);
|
||||
if (participant == null) {
|
||||
print('Adding participant that was already present in the room ${remoteParticipant.sid}, before I connected');
|
||||
_addRemoteParticipantListeners(remoteParticipant);
|
||||
}
|
||||
}
|
||||
// We have to listen for the [onDataTrackPublished] event on the [LocalParticipant] in
|
||||
// order to be able to use the [send] method.
|
||||
_streamSubscriptions.add(room.localParticipant.onDataTrackPublished.listen(_onLocalDataTrackPublished));
|
||||
notifyListeners();
|
||||
_completer.complete(room);
|
||||
|
||||
_timer = Timer.periodic(const Duration(minutes: 1), (_) {
|
||||
// Let's see if we can send some data over the DataTrack API
|
||||
sendMessage('And another minute has passed since I connected...');
|
||||
// Also try the ByteBuffer way of sending data
|
||||
final list = 'This data has been sent over the ByteBuffer channel of the DataTrack API'.codeUnits;
|
||||
var bytes = Uint8List.fromList(list);
|
||||
sendBufferMessage(bytes.buffer);
|
||||
});
|
||||
}
|
||||
|
||||
void _onLocalDataTrackPublished(LocalDataTrackPublishedEvent event) {
|
||||
// Send buffered messages, if any...
|
||||
while (_messages.isNotEmpty) {
|
||||
var message = _messages.removeAt(0);
|
||||
print('Sending buffered message: $message');
|
||||
event.localDataTrackPublication.localDataTrack.send(message);
|
||||
}
|
||||
}
|
||||
|
||||
void _onConnectFailure(RoomConnectFailureEvent event) {
|
||||
print('ConferenceRoom._onConnectFailure: ${event.exception}');
|
||||
_completer.completeError(event.exception);
|
||||
}
|
||||
|
||||
void _onDominantSpeakerChanged(DominantSpeakerChangedEvent event) {
|
||||
print('ConferenceRoom._onDominantSpeakerChanged: ${event.remoteParticipant.identity}');
|
||||
var oldDominantParticipant = _participants.firstWhere((p) => p.isDominant, orElse: () => null);
|
||||
if (oldDominantParticipant != null) {
|
||||
var oldDominantParticipantIndex = _participants.indexOf(oldDominantParticipant);
|
||||
_participants.replaceRange(oldDominantParticipantIndex, oldDominantParticipantIndex + 1, [oldDominantParticipant.copyWith(isDominant: false)]);
|
||||
}
|
||||
|
||||
var newDominantParticipant = _participants.firstWhere((p) => p.id == event.remoteParticipant.sid);
|
||||
var newDominantParticipantIndex = _participants.indexOf(newDominantParticipant);
|
||||
_participants.replaceRange(newDominantParticipantIndex, newDominantParticipantIndex + 1, [newDominantParticipant.copyWith(isDominant: true)]);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void _onParticipantConnected(RoomParticipantConnectedEvent event) {
|
||||
print('ConferenceRoom._onParticipantConnected, ${event.remoteParticipant.sid}');
|
||||
_addRemoteParticipantListeners(event.remoteParticipant);
|
||||
}
|
||||
|
||||
void _onParticipantDisconnected(RoomParticipantDisconnectedEvent event) {
|
||||
print('ConferenceRoom._onParticipantDisconnected: ${event.remoteParticipant.sid}');
|
||||
_participants.removeWhere((ParticipantWidget p) => p.id == event.remoteParticipant.sid);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
ParticipantWidget _buildParticipant({
|
||||
@required Widget child,
|
||||
@required String id,
|
||||
@required bool audioEnabled,
|
||||
@required bool videoEnabled,
|
||||
RemoteParticipant remoteParticipant,
|
||||
}) {
|
||||
return ParticipantWidget(
|
||||
id: remoteParticipant?.sid,
|
||||
isRemote: remoteParticipant != null,
|
||||
child: child,
|
||||
audioEnabled: audioEnabled,
|
||||
videoEnabled: videoEnabled,
|
||||
);
|
||||
}
|
||||
|
||||
void _addRemoteParticipantListeners(RemoteParticipant remoteParticipant) {
|
||||
print('ConferenceRoom._addRemoteParticipantListeners() => Adding listeners to remoteParticipant ${remoteParticipant.sid}');
|
||||
_streamSubscriptions.add(remoteParticipant.onAudioTrackDisabled.listen(_onAudioTrackDisabled));
|
||||
_streamSubscriptions.add(remoteParticipant.onAudioTrackEnabled.listen(_onAudioTrackEnabled));
|
||||
_streamSubscriptions.add(remoteParticipant.onAudioTrackPublished.listen(_onAudioTrackPublished));
|
||||
_streamSubscriptions.add(remoteParticipant.onAudioTrackSubscribed.listen(_onAudioTrackSubscribed));
|
||||
_streamSubscriptions.add(remoteParticipant.onAudioTrackSubscriptionFailed.listen(_onAudioTrackSubscriptionFailed));
|
||||
_streamSubscriptions.add(remoteParticipant.onAudioTrackUnpublished.listen(_onAudioTrackUnpublished));
|
||||
_streamSubscriptions.add(remoteParticipant.onAudioTrackUnsubscribed.listen(_onAudioTrackUnsubscribed));
|
||||
|
||||
_streamSubscriptions.add(remoteParticipant.onDataTrackPublished.listen(_onDataTrackPublished));
|
||||
_streamSubscriptions.add(remoteParticipant.onDataTrackSubscribed.listen(_onDataTrackSubscribed));
|
||||
_streamSubscriptions.add(remoteParticipant.onDataTrackSubscriptionFailed.listen(_onDataTrackSubscriptionFailed));
|
||||
_streamSubscriptions.add(remoteParticipant.onDataTrackUnpublished.listen(_onDataTrackUnpublished));
|
||||
_streamSubscriptions.add(remoteParticipant.onDataTrackUnsubscribed.listen(_onDataTrackUnsubscribed));
|
||||
|
||||
_streamSubscriptions.add(remoteParticipant.onVideoTrackDisabled.listen(_onVideoTrackDisabled));
|
||||
_streamSubscriptions.add(remoteParticipant.onVideoTrackEnabled.listen(_onVideoTrackEnabled));
|
||||
_streamSubscriptions.add(remoteParticipant.onVideoTrackPublished.listen(_onVideoTrackPublished));
|
||||
_streamSubscriptions.add(remoteParticipant.onVideoTrackSubscribed.listen(_onVideoTrackSubscribed));
|
||||
_streamSubscriptions.add(remoteParticipant.onVideoTrackSubscriptionFailed.listen(_onVideoTrackSubscriptionFailed));
|
||||
_streamSubscriptions.add(remoteParticipant.onVideoTrackUnpublished.listen(_onVideoTrackUnpublished));
|
||||
_streamSubscriptions.add(remoteParticipant.onVideoTrackUnsubscribed.listen(_onVideoTrackUnsubscribed));
|
||||
}
|
||||
|
||||
void _onAudioTrackDisabled(RemoteAudioTrackEvent event) {
|
||||
print('ConferenceRoom._onAudioTrackDisabled(), ${event.remoteParticipant.sid}, ${event.remoteAudioTrackPublication.trackSid}, isEnabled: ${event.remoteAudioTrackPublication.isTrackEnabled}');
|
||||
_setRemoteAudioEnabled(event);
|
||||
}
|
||||
|
||||
void _onAudioTrackEnabled(RemoteAudioTrackEvent event) {
|
||||
print('ConferenceRoom._onAudioTrackEnabled(), ${event.remoteParticipant.sid}, ${event.remoteAudioTrackPublication.trackSid}, isEnabled: ${event.remoteAudioTrackPublication.isTrackEnabled}');
|
||||
_setRemoteAudioEnabled(event);
|
||||
}
|
||||
|
||||
void _onAudioTrackPublished(RemoteAudioTrackEvent event) {
|
||||
print('ConferenceRoom._onAudioTrackPublished(), ${event.remoteParticipant.sid}}');
|
||||
}
|
||||
|
||||
void _onAudioTrackSubscribed(RemoteAudioTrackSubscriptionEvent event) {
|
||||
print('ConferenceRoom._onAudioTrackSubscribed(), ${event.remoteParticipant.sid}, ${event.remoteAudioTrackPublication.trackSid}');
|
||||
_addOrUpdateParticipant(event);
|
||||
}
|
||||
|
||||
void _onAudioTrackSubscriptionFailed(RemoteAudioTrackSubscriptionFailedEvent event) {
|
||||
print('ConferenceRoom._onAudioTrackSubscriptionFailed(), ${event.remoteParticipant.sid}, ${event.remoteAudioTrackPublication.trackSid}');
|
||||
_onExceptionStreamController.add(
|
||||
PlatformException(
|
||||
code: 'ConferenceRoom.audioTrackSubscriptionFailed',
|
||||
message: 'AudioTrack Subscription Failed',
|
||||
details: event.exception.toString(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _onAudioTrackUnpublished(RemoteAudioTrackEvent event) {
|
||||
print('ConferenceRoom._onAudioTrackUnpublished(), ${event.remoteParticipant.sid}, ${event.remoteAudioTrackPublication.trackSid}');
|
||||
}
|
||||
|
||||
void _onAudioTrackUnsubscribed(RemoteAudioTrackSubscriptionEvent event) {
|
||||
print('ConferenceRoom._onAudioTrackUnsubscribed(), ${event.remoteParticipant.sid}, ${event.remoteAudioTrack.sid}');
|
||||
}
|
||||
|
||||
void _onDataTrackPublished(RemoteDataTrackEvent event) {
|
||||
print('ConferenceRoom._onDataTrackPublished(), ${event.remoteParticipant.sid}}');
|
||||
}
|
||||
|
||||
void _onDataTrackSubscribed(RemoteDataTrackSubscriptionEvent event) {
|
||||
print('ConferenceRoom._onDataTrackSubscribed(), ${event.remoteParticipant.sid}, ${event.remoteDataTrackPublication.trackSid}');
|
||||
final dataTrack = event.remoteDataTrackPublication.remoteDataTrack;
|
||||
_dataTracks.add(dataTrack);
|
||||
_streamSubscriptions.add(dataTrack.onMessage.listen(_onMessage));
|
||||
_streamSubscriptions.add(dataTrack.onBufferMessage.listen(_onBufferMessage));
|
||||
}
|
||||
|
||||
void _onDataTrackSubscriptionFailed(RemoteDataTrackSubscriptionFailedEvent event) {
|
||||
print('ConferenceRoom._onDataTrackSubscriptionFailed(), ${event.remoteParticipant.sid}, ${event.remoteDataTrackPublication.trackSid}');
|
||||
_onExceptionStreamController.add(
|
||||
PlatformException(
|
||||
code: 'ConferenceRoom.dataTrackSubscriptionFailed',
|
||||
message: 'DataTrack Subscription Failed',
|
||||
details: event.exception.toString(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _onDataTrackUnpublished(RemoteDataTrackEvent event) {
|
||||
print('ConferenceRoom._onDataTrackUnpublished(), ${event.remoteParticipant.sid}, ${event.remoteDataTrackPublication.trackSid}');
|
||||
}
|
||||
|
||||
void _onDataTrackUnsubscribed(RemoteDataTrackSubscriptionEvent event) {
|
||||
print('ConferenceRoom._onDataTrackUnsubscribed(), ${event.remoteParticipant.sid}, ${event.remoteDataTrack.sid}');
|
||||
}
|
||||
|
||||
void _onVideoTrackDisabled(RemoteVideoTrackEvent event) {
|
||||
print('ConferenceRoom._onVideoTrackDisabled(), ${event.remoteParticipant.sid}, ${event.remoteVideoTrackPublication.trackSid}, isEnabled: ${event.remoteVideoTrackPublication.isTrackEnabled}');
|
||||
_setRemoteVideoEnabled(event);
|
||||
}
|
||||
|
||||
void _onVideoTrackEnabled(RemoteVideoTrackEvent event) {
|
||||
print('ConferenceRoom._onVideoTrackEnabled(), ${event.remoteParticipant.sid}, ${event.remoteVideoTrackPublication.trackSid}, isEnabled: ${event.remoteVideoTrackPublication.isTrackEnabled}');
|
||||
_setRemoteVideoEnabled(event);
|
||||
}
|
||||
|
||||
void _onVideoTrackPublished(RemoteVideoTrackEvent event) {
|
||||
print('ConferenceRoom._onVideoTrackPublished(), ${event.remoteParticipant.sid}, ${event.remoteVideoTrackPublication.trackSid}');
|
||||
}
|
||||
|
||||
void _onVideoTrackSubscribed(RemoteVideoTrackSubscriptionEvent event) {
|
||||
print('ConferenceRoom._onVideoTrackSubscribed(), ${event.remoteParticipant.sid}, ${event.remoteVideoTrack.sid}');
|
||||
_addOrUpdateParticipant(event);
|
||||
}
|
||||
|
||||
void _onVideoTrackSubscriptionFailed(RemoteVideoTrackSubscriptionFailedEvent event) {
|
||||
print('ConferenceRoom._onVideoTrackSubscriptionFailed(), ${event.remoteParticipant.sid}, ${event.remoteVideoTrackPublication.trackSid}');
|
||||
_onExceptionStreamController.add(
|
||||
PlatformException(
|
||||
code: 'ConferenceRoom.videoTrackSubscriptionFailed',
|
||||
message: 'VideoTrack Subscription Failed',
|
||||
details: event.exception.toString(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _onVideoTrackUnpublished(RemoteVideoTrackEvent event) {
|
||||
print('ConferenceRoom._onVideoTrackUnpublished(), ${event.remoteParticipant.sid}, ${event.remoteVideoTrackPublication.trackSid}');
|
||||
}
|
||||
|
||||
void _onVideoTrackUnsubscribed(RemoteVideoTrackSubscriptionEvent event) {
|
||||
print('ConferenceRoom._onVideoTrackUnsubscribed(), ${event.remoteParticipant.sid}, ${event.remoteVideoTrack.sid}');
|
||||
}
|
||||
|
||||
void _onMessage(RemoteDataTrackStringMessageEvent event) {
|
||||
print('onMessage => ${event.remoteDataTrack.sid}, ${event.message}');
|
||||
}
|
||||
|
||||
void _onBufferMessage(RemoteDataTrackBufferMessageEvent event) {
|
||||
print('onBufferMessage => ${event.remoteDataTrack.sid}, ${String.fromCharCodes(event.message.asUint8List())}');
|
||||
}
|
||||
|
||||
void _setRemoteAudioEnabled(RemoteAudioTrackEvent event) {
|
||||
if (event.remoteAudioTrackPublication == null) {
|
||||
return;
|
||||
}
|
||||
var index = _participants.indexWhere((ParticipantWidget participant) => participant.id == event.remoteParticipant.sid);
|
||||
if (index < 0) {
|
||||
return;
|
||||
}
|
||||
var participant = _participants[index];
|
||||
_participants.replaceRange(
|
||||
index,
|
||||
index + 1,
|
||||
[
|
||||
participant.copyWith(audioEnabled: event.remoteAudioTrackPublication.isTrackEnabled),
|
||||
],
|
||||
);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void _setRemoteVideoEnabled(RemoteVideoTrackEvent event) {
|
||||
if (event.remoteVideoTrackPublication == null) {
|
||||
return;
|
||||
}
|
||||
var index = _participants.indexWhere((ParticipantWidget participant) => participant.id == event.remoteParticipant.sid);
|
||||
if (index < 0) {
|
||||
return;
|
||||
}
|
||||
var participant = _participants[index];
|
||||
_participants.replaceRange(
|
||||
index,
|
||||
index + 1,
|
||||
[
|
||||
participant.copyWith(videoEnabled: event.remoteVideoTrackPublication.isTrackEnabled),
|
||||
],
|
||||
);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void _addOrUpdateParticipant(RemoteParticipantEvent event) {
|
||||
print('ConferenceRoom._addOrUpdateParticipant(), ${event.remoteParticipant.sid}');
|
||||
final participant = _participants.firstWhere(
|
||||
(ParticipantWidget participant) => participant.id == event.remoteParticipant.sid,
|
||||
orElse: () => null,
|
||||
);
|
||||
if (participant != null) {
|
||||
print('Participant found: ${participant.id}, updating A/V enabled values');
|
||||
_setRemoteVideoEnabled(event);
|
||||
_setRemoteAudioEnabled(event);
|
||||
} else {
|
||||
final bufferedParticipant = _participantBuffer.firstWhere(
|
||||
(ParticipantBuffer participant) => participant.id == event.remoteParticipant.sid,
|
||||
orElse: () => null,
|
||||
);
|
||||
if (bufferedParticipant != null) {
|
||||
_participantBuffer.remove(bufferedParticipant);
|
||||
} else if (event is RemoteAudioTrackEvent) {
|
||||
print('Audio subscription came first, waiting for the video subscription...');
|
||||
_participantBuffer.add(
|
||||
ParticipantBuffer(
|
||||
id: event.remoteParticipant.sid,
|
||||
audioEnabled: event.remoteAudioTrackPublication?.remoteAudioTrack?.isEnabled ?? true,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (event is RemoteVideoTrackSubscriptionEvent) {
|
||||
print('New participant, adding: ${event.remoteParticipant.sid}');
|
||||
_participants.insert(
|
||||
0,
|
||||
_buildParticipant(
|
||||
child: event.remoteVideoTrack.widget(),
|
||||
id: event.remoteParticipant.sid,
|
||||
remoteParticipant: event.remoteParticipant,
|
||||
audioEnabled: bufferedParticipant?.audioEnabled ?? true,
|
||||
videoEnabled: event.remoteVideoTrackPublication?.remoteVideoTrack?.isEnabled ?? true,
|
||||
),
|
||||
);
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,173 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:diplomaticquarterapp/pages/conference/clipped_video.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class DraggablePublisher extends StatefulWidget {
|
||||
final Size availableScreenSize;
|
||||
final Widget child;
|
||||
final double scaleFactor;
|
||||
final Stream<bool> onButtonBarVisible;
|
||||
final Stream<double> onButtonBarHeight;
|
||||
|
||||
const DraggablePublisher({
|
||||
Key key,
|
||||
@required this.availableScreenSize,
|
||||
this.child,
|
||||
@required this.onButtonBarVisible,
|
||||
@required this.onButtonBarHeight,
|
||||
|
||||
/// The portion of the screen the DraggableWidget should use.
|
||||
this.scaleFactor = .25,
|
||||
}) : assert(scaleFactor != null && scaleFactor > 0 && scaleFactor <= .4),
|
||||
assert(availableScreenSize != null),
|
||||
assert(onButtonBarVisible != null),
|
||||
assert(onButtonBarHeight != null),
|
||||
super(key: key);
|
||||
|
||||
@override
|
||||
_DraggablePublisherState createState() => _DraggablePublisherState();
|
||||
}
|
||||
|
||||
class _DraggablePublisherState extends State<DraggablePublisher> {
|
||||
bool _isButtonBarVisible = true;
|
||||
double _buttonBarHeight = 0;
|
||||
double _width;
|
||||
double _height;
|
||||
double _top;
|
||||
double _left;
|
||||
double _viewPaddingTop;
|
||||
double _viewPaddingBottom;
|
||||
final double _padding = 8.0;
|
||||
final Duration _duration300ms = const Duration(milliseconds: 300);
|
||||
final Duration _duration0ms = const Duration(milliseconds: 0);
|
||||
Duration _duration;
|
||||
StreamSubscription _streamSubscription;
|
||||
StreamSubscription _streamHeightSubscription;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_duration = _duration300ms;
|
||||
_width = widget.availableScreenSize.width * widget.scaleFactor;
|
||||
_height = _width * (widget.availableScreenSize.height / widget.availableScreenSize.width);
|
||||
_top = widget.availableScreenSize.height - (_buttonBarHeight + _padding) - _height;
|
||||
_left = widget.availableScreenSize.width - _padding - _width;
|
||||
|
||||
_streamSubscription = widget.onButtonBarVisible.listen(_buttonBarVisible);
|
||||
_streamHeightSubscription = widget.onButtonBarHeight.listen(_getButtonBarHeight);
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
final mediaQuery = MediaQuery.of(context);
|
||||
_viewPaddingTop = mediaQuery.viewPadding.top;
|
||||
_viewPaddingBottom = mediaQuery.viewPadding.bottom;
|
||||
super.didChangeDependencies();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_streamSubscription.cancel();
|
||||
_streamHeightSubscription.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _getButtonBarHeight(double height) {
|
||||
setState(() {
|
||||
_buttonBarHeight = height;
|
||||
_positionWidget();
|
||||
});
|
||||
}
|
||||
|
||||
void _buttonBarVisible(bool visible) {
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
setState(() {
|
||||
_isButtonBarVisible = visible;
|
||||
if (_duration == _duration300ms) {
|
||||
// only position the widget when we are not currently dragging it around
|
||||
_positionWidget();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AnimatedPositioned(
|
||||
top: _top,
|
||||
left: _left,
|
||||
width: _width,
|
||||
height: _height,
|
||||
duration: _duration,
|
||||
child: Listener(
|
||||
onPointerDown: (_) => _duration = _duration0ms,
|
||||
onPointerMove: (PointerMoveEvent event) {
|
||||
setState(() {
|
||||
_left = (_left + event.delta.dx).roundToDouble();
|
||||
_top = (_top + event.delta.dy).roundToDouble();
|
||||
});
|
||||
},
|
||||
onPointerUp: (_) => _positionWidget(),
|
||||
onPointerCancel: (_) => _positionWidget(),
|
||||
child: ClippedVideo(
|
||||
height: _height,
|
||||
width: _width,
|
||||
child: widget.child,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
double _getCurrentStatusBarHeight() {
|
||||
if (_isButtonBarVisible) {
|
||||
return _viewPaddingTop;
|
||||
}
|
||||
final _defaultViewPaddingTop = Platform.isIOS ? 20.0 : Platform.isAndroid ? 24.0 : 0.0;
|
||||
if (_viewPaddingTop > _defaultViewPaddingTop) {
|
||||
// There must be a hardware notch in the display.
|
||||
return _viewPaddingTop;
|
||||
}
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
double _getCurrentButtonBarHeight() {
|
||||
if (_isButtonBarVisible) {
|
||||
return _buttonBarHeight + _viewPaddingBottom;
|
||||
}
|
||||
return _viewPaddingBottom;
|
||||
}
|
||||
|
||||
void _positionWidget() {
|
||||
// Determine the center of the object being dragged so we can decide
|
||||
// in which corner the object should be placed.
|
||||
var dx = (_width / 2) + _left;
|
||||
dx = dx < 0 ? 0 : dx >= widget.availableScreenSize.width ? widget.availableScreenSize.width - 1 : dx;
|
||||
var dy = (_height / 2) + _top;
|
||||
dy = dy < 0 ? 0 : dy >= widget.availableScreenSize.height ? widget.availableScreenSize.height - 1 : dy;
|
||||
final draggableCenter = Offset(dx, dy);
|
||||
|
||||
setState(() {
|
||||
_duration = _duration300ms;
|
||||
if (Rect.fromLTRB(0, 0, widget.availableScreenSize.width / 2, widget.availableScreenSize.height / 2).contains(draggableCenter)) {
|
||||
// Top-left
|
||||
_top = _getCurrentStatusBarHeight() + _padding;
|
||||
_left = _padding;
|
||||
} else if (Rect.fromLTRB(widget.availableScreenSize.width / 2, 0, widget.availableScreenSize.width, widget.availableScreenSize.height / 2).contains(draggableCenter)) {
|
||||
// Top-right
|
||||
_top = _getCurrentStatusBarHeight() + _padding;
|
||||
_left = widget.availableScreenSize.width - _padding - _width;
|
||||
} else if (Rect.fromLTRB(0, widget.availableScreenSize.height / 2, widget.availableScreenSize.width / 2, widget.availableScreenSize.height).contains(draggableCenter)) {
|
||||
// Bottom-left
|
||||
_top = widget.availableScreenSize.height - (_getCurrentButtonBarHeight() + _padding) - _height;
|
||||
_left = _padding;
|
||||
} else if (Rect.fromLTRB(widget.availableScreenSize.width / 2, widget.availableScreenSize.height / 2, widget.availableScreenSize.width, widget.availableScreenSize.height).contains(draggableCenter)) {
|
||||
// Bottom-right
|
||||
_top = widget.availableScreenSize.height - (_getCurrentButtonBarHeight() + _padding) - _height;
|
||||
_left = widget.availableScreenSize.width - _padding - _width;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,197 @@
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class ParticipantBuffer {
|
||||
final bool audioEnabled;
|
||||
final String id;
|
||||
|
||||
ParticipantBuffer({
|
||||
@required this.audioEnabled,
|
||||
@required this.id,
|
||||
}) : assert(audioEnabled != null),
|
||||
assert(id != null);
|
||||
}
|
||||
|
||||
class ParticipantWidget extends StatelessWidget {
|
||||
final Widget child;
|
||||
final String id;
|
||||
final bool audioEnabled;
|
||||
final bool videoEnabled;
|
||||
final bool isRemote;
|
||||
final bool isDummy;
|
||||
final bool isDominant;
|
||||
|
||||
const ParticipantWidget({
|
||||
Key key,
|
||||
@required this.child,
|
||||
@required this.audioEnabled,
|
||||
@required this.videoEnabled,
|
||||
@required this.id,
|
||||
@required this.isRemote,
|
||||
this.isDominant = false,
|
||||
this.isDummy = false,
|
||||
}) : assert(child != null),
|
||||
assert(audioEnabled != null),
|
||||
assert(videoEnabled != null),
|
||||
assert(isRemote != null),
|
||||
assert(isDominant != null),
|
||||
assert(isDummy != null),
|
||||
super(key: key);
|
||||
|
||||
ParticipantWidget copyWith({
|
||||
Widget child,
|
||||
bool audioEnabled,
|
||||
bool videoEnabled,
|
||||
bool isDominant,
|
||||
}) {
|
||||
return ParticipantWidget(
|
||||
id: id,
|
||||
child: child ?? this.child,
|
||||
audioEnabled: audioEnabled ?? this.audioEnabled,
|
||||
videoEnabled: videoEnabled ?? this.videoEnabled,
|
||||
isDominant: isDominant ?? this.isDominant,
|
||||
isRemote: isRemote,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final children = <Widget>[];
|
||||
final icons = <Widget>[];
|
||||
if (!videoEnabled) {
|
||||
icons.add(_buildVideoEnabledIcon());
|
||||
children.add(
|
||||
ClipRect(
|
||||
// Need to clip this BackdropFilter, otherwise it will blur the entire screen
|
||||
child: BackdropFilter(
|
||||
filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(color: Colors.black.withOpacity(.1)),
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
children.add(child);
|
||||
}
|
||||
children.add(Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: AnimatedOpacity(
|
||||
duration: Duration(milliseconds: 500),
|
||||
opacity: isDominant ? 1 : 0,
|
||||
child: Icon(
|
||||
Icons.volume_up,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
));
|
||||
if (!audioEnabled) {
|
||||
icons.add(_buildAudioEnabledIcon());
|
||||
}
|
||||
if (icons.isNotEmpty) {
|
||||
if (isRemote) {
|
||||
final rows = <Widget>[];
|
||||
rows.add(_buildRow(icons));
|
||||
if (!audioEnabled && !videoEnabled) {
|
||||
rows.add(_buildRow(_fitText('The camera and microphone are off', Colors.white24)));
|
||||
} else if (!audioEnabled) {
|
||||
rows.add(_buildRow(_fitText('The microphone is off', Colors.black26)));
|
||||
} else if (!videoEnabled) {
|
||||
rows.add(_buildRow(_fitText('The camera is off', Colors.white24)));
|
||||
}
|
||||
children.add(
|
||||
Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: rows,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
children.add(Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: icons,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
return Stack(
|
||||
children: children,
|
||||
);
|
||||
}
|
||||
|
||||
List<Widget> _fitText(String text, Color color) {
|
||||
return [
|
||||
Flexible(
|
||||
child: FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(left: 8.0, right: 8.0),
|
||||
child: Text(text, maxLines: 1, style: _buildTextStyle(color)),
|
||||
),
|
||||
),
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
TextStyle _buildTextStyle(Color color) {
|
||||
return TextStyle(
|
||||
color: color,
|
||||
shadows: <Shadow>[
|
||||
Shadow(
|
||||
blurRadius: 1.0,
|
||||
color: Color.fromARGB(255, 0, 0, 0),
|
||||
),
|
||||
Shadow(
|
||||
blurRadius: 1.0,
|
||||
color: Color.fromARGB(24, 255, 255, 255),
|
||||
),
|
||||
],
|
||||
fontSize: 15,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildRow(List<Widget> children) {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: children,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildAudioEnabledIcon() {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: CircleAvatar(
|
||||
maxRadius: 15,
|
||||
child: FittedBox(
|
||||
child: Icon(
|
||||
Icons.mic_off,
|
||||
color: Colors.black,
|
||||
key: Key('microphone-off-icon'),
|
||||
),
|
||||
),
|
||||
backgroundColor: Colors.white24,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildVideoEnabledIcon() {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: CircleAvatar(
|
||||
maxRadius: 15,
|
||||
child: FittedBox(
|
||||
child: Icon(
|
||||
Icons.videocam_off,
|
||||
color: Colors.black,
|
||||
key: Key('videocam-off-icon'),
|
||||
),
|
||||
),
|
||||
backgroundColor: Colors.white24,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,106 @@
|
||||
import 'dart:async';
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class ButtonToProgress extends StatefulWidget {
|
||||
final double height;
|
||||
final double progressHeight;
|
||||
final String loadingText;
|
||||
final Duration duration;
|
||||
final TextStyle loadingTextStyle;
|
||||
final VoidCallback onPressed;
|
||||
final Stream<bool> onLoading;
|
||||
final Widget child;
|
||||
|
||||
const ButtonToProgress({
|
||||
Key key,
|
||||
this.height = 40.0,
|
||||
this.progressHeight = 5.0,
|
||||
this.loadingText,
|
||||
this.duration = const Duration(milliseconds: 300),
|
||||
this.loadingTextStyle,
|
||||
this.onPressed,
|
||||
this.onLoading,
|
||||
@required this.child,
|
||||
}) : assert(child != null),
|
||||
assert(height != null && height > 0),
|
||||
assert(progressHeight != null && progressHeight > 0 && progressHeight <= height),
|
||||
super(key: key);
|
||||
|
||||
@override
|
||||
_ButtonToProgressState createState() => _ButtonToProgressState();
|
||||
}
|
||||
|
||||
class _ButtonToProgressState extends State<ButtonToProgress> {
|
||||
double _height;
|
||||
double _opacity = 0;
|
||||
bool _isLoading = false;
|
||||
|
||||
StreamSubscription<bool> _subscription;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_height = widget.height;
|
||||
if (widget.onLoading != null) {
|
||||
_subscription = widget.onLoading.listen((bool isLoading) {
|
||||
setState(() {
|
||||
_isLoading = isLoading;
|
||||
_height = isLoading ? widget.progressHeight : widget.height;
|
||||
_opacity = isLoading ? 1 : 0;
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
if (_subscription != null) {
|
||||
_subscription.cancel();
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
height: widget.height,
|
||||
child: Stack(
|
||||
children: [
|
||||
if (widget.loadingText == null)
|
||||
Container()
|
||||
else
|
||||
Padding(
|
||||
padding: EdgeInsets.only(bottom: widget.progressHeight),
|
||||
child: AnimatedOpacity(
|
||||
child: Center(
|
||||
child: FittedBox(
|
||||
child: Text(
|
||||
widget.loadingText,
|
||||
style: widget.loadingTextStyle,
|
||||
),
|
||||
),
|
||||
),
|
||||
opacity: _opacity,
|
||||
duration: Duration(milliseconds: widget.duration.inMilliseconds + 200),
|
||||
curve: Curves.easeInCubic,
|
||||
),
|
||||
),
|
||||
AnimatedPadding(
|
||||
duration: widget.duration,
|
||||
padding: EdgeInsets.only(
|
||||
top: math.max(widget.height - _height, 0),
|
||||
),
|
||||
child: AnimatedContainer(
|
||||
duration: widget.duration,
|
||||
height: _height,
|
||||
width: double.infinity,
|
||||
child: _isLoading ? const LinearProgressIndicator() : widget.child,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,103 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:native_device_orientation/native_device_orientation.dart';
|
||||
|
||||
class CircleButton extends StatefulWidget {
|
||||
final VoidCallback onLongPress;
|
||||
final VoidCallback onPressed;
|
||||
final GestureTapDownCallback onTapDown;
|
||||
final VoidCallback onTapCancel;
|
||||
final Widget child;
|
||||
final Color color;
|
||||
final double radius;
|
||||
|
||||
const CircleButton({
|
||||
Key key,
|
||||
this.onLongPress,
|
||||
this.onPressed,
|
||||
this.child,
|
||||
this.color,
|
||||
this.radius = 25.0,
|
||||
this.onTapCancel,
|
||||
this.onTapDown,
|
||||
}) : assert(radius != null),
|
||||
super(key: key);
|
||||
|
||||
@override
|
||||
_CircleButtonState createState() => _CircleButtonState();
|
||||
}
|
||||
|
||||
class _CircleButtonState extends State<CircleButton> {
|
||||
double _rotationAngle = 0.0;
|
||||
|
||||
final Stream<NativeDeviceOrientation> _orientationStream = NativeDeviceOrientationCommunicator().onOrientationChanged(useSensor: true);
|
||||
StreamSubscription<NativeDeviceOrientation> _orientationSubscription;
|
||||
|
||||
void _handleOrientationChange(NativeDeviceOrientation orientation) {
|
||||
var targetAngle = 0.0;
|
||||
switch (orientation) {
|
||||
case NativeDeviceOrientation.unknown:
|
||||
case NativeDeviceOrientation.portraitUp:
|
||||
targetAngle = 0.0;
|
||||
break;
|
||||
case NativeDeviceOrientation.portraitDown:
|
||||
targetAngle = 180.0;
|
||||
break;
|
||||
case NativeDeviceOrientation.landscapeLeft:
|
||||
targetAngle = 90.0;
|
||||
break;
|
||||
case NativeDeviceOrientation.landscapeRight:
|
||||
targetAngle = 270.0;
|
||||
break;
|
||||
}
|
||||
setState(() {
|
||||
_rotationAngle = targetAngle;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_orientationSubscription = _orientationStream.listen(
|
||||
_handleOrientationChange,
|
||||
onError: (dynamic err) => print(err),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
_orientationSubscription.cancel();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final size = 2 * widget.radius;
|
||||
|
||||
return Container(
|
||||
width: size,
|
||||
height: size,
|
||||
decoration: BoxDecoration(
|
||||
color: (widget.color ?? Theme.of(context).primaryColor).withAlpha(200),
|
||||
borderRadius: BorderRadius.all(
|
||||
Radius.circular(widget.radius),
|
||||
),
|
||||
),
|
||||
child: GestureDetector(
|
||||
onLongPress: widget.onLongPress,
|
||||
onTapDown: widget.onTapDown,
|
||||
onTapCancel: widget.onTapCancel,
|
||||
child: RawMaterialButton(
|
||||
onPressed: widget.onPressed,
|
||||
child: RotationTransition(
|
||||
child: widget.child,
|
||||
turns: AlwaysStoppedAnimation<double>(_rotationAngle / 360),
|
||||
),
|
||||
elevation: 0,
|
||||
shape: const CircleBorder(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,144 @@
|
||||
import 'dart:math' as math;
|
||||
import 'dart:ui';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
enum NoiseBoxDensity {
|
||||
high,
|
||||
medium,
|
||||
low,
|
||||
xHigh,
|
||||
xLow,
|
||||
}
|
||||
|
||||
class NoiseBox extends StatefulWidget {
|
||||
final NoiseBoxDensity density;
|
||||
final Color backgroundColor;
|
||||
final Widget child;
|
||||
|
||||
const NoiseBox({
|
||||
Key key,
|
||||
this.backgroundColor,
|
||||
this.child,
|
||||
this.density = NoiseBoxDensity.medium,
|
||||
}) : assert(density != null),
|
||||
super(key: key);
|
||||
|
||||
@override
|
||||
_NoiseBoxState createState() => _NoiseBoxState();
|
||||
}
|
||||
|
||||
class _NoiseBoxState extends State<NoiseBox> with SingleTickerProviderStateMixin {
|
||||
AnimationController _animationController;
|
||||
int _density;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_animationController = AnimationController(
|
||||
vsync: this,
|
||||
duration: const Duration(seconds: 60),
|
||||
);
|
||||
_animationController.repeat();
|
||||
switch (widget.density) {
|
||||
case NoiseBoxDensity.high:
|
||||
_density = 5;
|
||||
break;
|
||||
case NoiseBoxDensity.medium:
|
||||
_density = 7;
|
||||
break;
|
||||
case NoiseBoxDensity.low:
|
||||
_density = 10;
|
||||
break;
|
||||
case NoiseBoxDensity.xHigh:
|
||||
_density = 3;
|
||||
break;
|
||||
case NoiseBoxDensity.xLow:
|
||||
_density = 12;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_animationController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return LayoutBuilder(
|
||||
builder: (BuildContext context, BoxConstraints constraints) => Container(
|
||||
color: widget.backgroundColor,
|
||||
width: constraints.biggest.width,
|
||||
height: constraints.biggest.height,
|
||||
child: AnimatedBuilder(
|
||||
animation: _animationController,
|
||||
builder: (BuildContext context, Widget w) {
|
||||
final children = <Widget>[
|
||||
CustomPaint(
|
||||
painter: NoisePainter(
|
||||
width: constraints.biggest.width,
|
||||
height: constraints.biggest.height,
|
||||
density: _density,
|
||||
),
|
||||
),
|
||||
];
|
||||
if (widget.child != null) {
|
||||
children.add(widget.child);
|
||||
}
|
||||
return Stack(
|
||||
children: children,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class NoisePainter extends CustomPainter {
|
||||
final double width;
|
||||
final double height;
|
||||
final int density;
|
||||
|
||||
NoisePainter({
|
||||
@required this.width,
|
||||
@required this.height,
|
||||
@required this.density,
|
||||
}) : assert(width != null),
|
||||
assert(height != null),
|
||||
assert(density != null && density >= 3 && density < math.min(width, height));
|
||||
|
||||
List<Color> colors = <Color>[
|
||||
Colors.black,
|
||||
Colors.grey,
|
||||
Colors.blueGrey,
|
||||
Colors.red,
|
||||
Colors.green,
|
||||
Colors.blue,
|
||||
Colors.white,
|
||||
];
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
final random = math.Random();
|
||||
for (var w = 0; w < width; w += density) {
|
||||
for (var h = 0; h < height; h += density) {
|
||||
final offset = Offset(
|
||||
random.nextDouble() * width,
|
||||
random.nextDouble() * height,
|
||||
);
|
||||
final paint = Paint();
|
||||
paint.color = colors[random.nextInt(colors.length)];
|
||||
paint.strokeWidth = random.nextDouble() * 2;
|
||||
|
||||
canvas.drawPoints(PointMode.points, <Offset>[offset], paint);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(CustomPainter oldDelegate) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,98 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
|
||||
import './platform_widget.dart';
|
||||
|
||||
class PlatformAlertDialog extends PlatformWidget {
|
||||
PlatformAlertDialog({@required this.title, @required this.content, @required this.defaultActionText, this.cancelActionText})
|
||||
: assert(title != null),
|
||||
assert(content != null),
|
||||
assert(defaultActionText != null);
|
||||
|
||||
final String title;
|
||||
final String content;
|
||||
final String defaultActionText;
|
||||
final String cancelActionText;
|
||||
|
||||
Future<bool> show(BuildContext context) async {
|
||||
return Platform.isIOS
|
||||
? await showCupertinoDialog<bool>(
|
||||
context: context,
|
||||
builder: (BuildContext context) => this,
|
||||
)
|
||||
: await showDialog<bool>(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (BuildContext context) => this,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget buildCupertinoWidget(BuildContext context) {
|
||||
return CupertinoAlertDialog(
|
||||
title: Text(title),
|
||||
content: Text(content),
|
||||
actions: _buildActions(context),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget buildMaterialWidget(BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: Text(title),
|
||||
content: Text(content),
|
||||
actions: _buildActions(context),
|
||||
);
|
||||
}
|
||||
|
||||
List<Widget> _buildActions(BuildContext context) {
|
||||
final actions = <Widget>[];
|
||||
if (cancelActionText != null) {
|
||||
actions.add(
|
||||
PlatformAlertDialogAction(
|
||||
child: Text(cancelActionText),
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop(false);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
actions.add(
|
||||
PlatformAlertDialogAction(
|
||||
child: Text(defaultActionText),
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop(true);
|
||||
},
|
||||
),
|
||||
);
|
||||
return actions;
|
||||
}
|
||||
}
|
||||
|
||||
class PlatformAlertDialogAction extends PlatformWidget {
|
||||
PlatformAlertDialogAction({
|
||||
this.child,
|
||||
this.onPressed,
|
||||
});
|
||||
|
||||
final Widget child;
|
||||
final VoidCallback onPressed;
|
||||
|
||||
@override
|
||||
Widget buildCupertinoWidget(BuildContext context) {
|
||||
return CupertinoDialogAction(
|
||||
child: child,
|
||||
onPressed: onPressed,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget buildMaterialWidget(BuildContext context) {
|
||||
return FlatButton(
|
||||
child: child,
|
||||
onPressed: onPressed,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,22 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import './platform_alert_dialog.dart';
|
||||
|
||||
class PlatformExceptionAlertDialog extends PlatformAlertDialog {
|
||||
PlatformExceptionAlertDialog({
|
||||
String title = 'An error occurred',
|
||||
@required Exception exception,
|
||||
}) : super(
|
||||
title: title,
|
||||
content: exception is PlatformException ? _message(exception) : exception.toString(),
|
||||
defaultActionText: 'OK',
|
||||
);
|
||||
|
||||
static String _message(PlatformException exception) {
|
||||
return _errors[exception.code] ?? (exception.details != null ? (exception.details['message'] ?? exception.message) : exception.message);
|
||||
}
|
||||
|
||||
static final Map<String, String> _errors = <String, String>{
|
||||
'ERROR_CODE': 'Error description...',
|
||||
};
|
||||
}
|
||||
@ -0,0 +1,16 @@
|
||||
import 'dart:io';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
abstract class PlatformWidget extends StatelessWidget {
|
||||
Widget buildCupertinoWidget(BuildContext context);
|
||||
|
||||
Widget buildMaterialWidget(BuildContext context);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (Platform.isIOS) {
|
||||
return buildCupertinoWidget(context);
|
||||
}
|
||||
return buildMaterialWidget(context);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,31 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
typedef ResponsiveBuilder = Widget Function(
|
||||
BuildContext context,
|
||||
Size size,
|
||||
);
|
||||
|
||||
class ResponsiveSafeArea extends StatelessWidget {
|
||||
const ResponsiveSafeArea({
|
||||
@required ResponsiveBuilder builder,
|
||||
Key key,
|
||||
}) : responsiveBuilder = builder,
|
||||
assert(builder != null),
|
||||
super(key: key);
|
||||
|
||||
final ResponsiveBuilder responsiveBuilder;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SafeArea(
|
||||
child: LayoutBuilder(
|
||||
builder: (BuildContext context, BoxConstraints constraints) {
|
||||
return responsiveBuilder(
|
||||
context,
|
||||
constraints.biggest,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,255 @@
|
||||
import 'package:diplomaticquarterapp/models/LiveCare/IncomingCallData.dart';
|
||||
import 'package:diplomaticquarterapp/models/LiveCare/room_model.dart';
|
||||
import 'package:diplomaticquarterapp/pages/conference/conference_page.dart';
|
||||
import 'package:diplomaticquarterapp/pages/conference/widgets/platform_exception_alert_dialog.dart';
|
||||
import 'package:diplomaticquarterapp/widgets/others/app_scaffold_widget.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:just_audio/just_audio.dart';
|
||||
|
||||
class IncomingCall extends StatefulWidget {
|
||||
IncomingCallData incomingCallData;
|
||||
|
||||
IncomingCall({@required this.incomingCallData});
|
||||
|
||||
@override
|
||||
_IncomingCallState createState() => _IncomingCallState();
|
||||
}
|
||||
|
||||
class _IncomingCallState extends State<IncomingCall>
|
||||
with SingleTickerProviderStateMixin {
|
||||
AnimationController _animationController;
|
||||
|
||||
final player = AudioPlayer();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_animationController =
|
||||
AnimationController(vsync: this, duration: Duration(milliseconds: 500));
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) => _runAnimation());
|
||||
|
||||
print(widget.incomingCallData.doctorname);
|
||||
print(widget.incomingCallData.clinicname);
|
||||
print(widget.incomingCallData.speciality);
|
||||
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_animationController.dispose();
|
||||
player.stop();
|
||||
disposeAudioResources();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AppScaffold(
|
||||
isShowAppBar: false,
|
||||
body: SafeArea(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(color: Colors.grey[700]),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: <Widget>[
|
||||
Container(
|
||||
margin: EdgeInsets.only(top: 30.0),
|
||||
alignment: Alignment.center,
|
||||
child: Text("Incoming Video Call",
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 26.0,
|
||||
color: Colors.white,
|
||||
letterSpacing: 1.0)),
|
||||
),
|
||||
Container(
|
||||
alignment: Alignment.center,
|
||||
margin: EdgeInsets.fromLTRB(50.0, 30.0, 50.0, 20.0),
|
||||
child: Image.asset(
|
||||
'assets/images/new-design/hmg_full_logo_hd_white.png'),
|
||||
),
|
||||
Container(
|
||||
margin: EdgeInsets.fromLTRB(30.0, 10.0, 30.0, 0.0),
|
||||
child: Divider(
|
||||
color: Colors.white,
|
||||
thickness: 1.0,
|
||||
),
|
||||
),
|
||||
Container(
|
||||
margin: EdgeInsets.only(top: 20.0),
|
||||
alignment: Alignment.center,
|
||||
child: Text("Dr Eyad Ismail Abu Jayab",
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 22.0,
|
||||
fontWeight: FontWeight.bold,
|
||||
letterSpacing: 0.8,
|
||||
color: Colors.white)),
|
||||
),
|
||||
Container(
|
||||
margin: EdgeInsets.only(top: 10.0),
|
||||
alignment: Alignment.center,
|
||||
child: Text("ENT Clinic",
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 22.0,
|
||||
letterSpacing: 0.8,
|
||||
color: Colors.white)),
|
||||
),
|
||||
Container(
|
||||
margin: EdgeInsets.only(top: 10.0),
|
||||
alignment: Alignment.center,
|
||||
child: Text("Speciality",
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 22.0,
|
||||
letterSpacing: 0.8,
|
||||
color: Colors.white)),
|
||||
),
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey[900].withOpacity(0.8),
|
||||
borderRadius: BorderRadius.all(Radius.circular(10.0)),
|
||||
),
|
||||
padding: EdgeInsets.all(20.0),
|
||||
margin: EdgeInsets.only(top: 20.0),
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
Text("Appointment Information",
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 20.0,
|
||||
fontWeight: FontWeight.bold,
|
||||
letterSpacing: 1.0,
|
||||
color: Colors.white)),
|
||||
Container(
|
||||
margin: EdgeInsets.only(top: 20.0),
|
||||
child: Text("Sun, 15th Dec, 2019, 09:00",
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 20.0,
|
||||
letterSpacing: 1.0,
|
||||
color: Colors.white)),
|
||||
),
|
||||
Container(
|
||||
margin: EdgeInsets.only(top: 20.0),
|
||||
child: Text("ENT Clinic",
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 20.0,
|
||||
letterSpacing: 1.0,
|
||||
color: Colors.white)),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Container(
|
||||
margin: EdgeInsets.only(top: 100.0),
|
||||
alignment: Alignment.center,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: <Widget>[
|
||||
RotationTransition(
|
||||
turns: Tween(begin: 0.0, end: -.1)
|
||||
.chain(CurveTween(curve: Curves.elasticIn))
|
||||
.animate(_animationController),
|
||||
child: Container(
|
||||
child: RawMaterialButton(
|
||||
onPressed: () {
|
||||
_submit();
|
||||
},
|
||||
elevation: 2.0,
|
||||
fillColor: Colors.green,
|
||||
child: Icon(
|
||||
Icons.call,
|
||||
color: Colors.white,
|
||||
size: 35.0,
|
||||
),
|
||||
padding: EdgeInsets.all(15.0),
|
||||
shape: CircleBorder(),
|
||||
),
|
||||
)),
|
||||
Container(
|
||||
child: RawMaterialButton(
|
||||
onPressed: () {
|
||||
backToHome();
|
||||
},
|
||||
elevation: 2.0,
|
||||
fillColor: Colors.red,
|
||||
child: Icon(
|
||||
Icons.call_end,
|
||||
color: Colors.white,
|
||||
size: 35.0,
|
||||
),
|
||||
padding: EdgeInsets.all(15.0),
|
||||
shape: CircleBorder(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
)),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _runAnimation() async {
|
||||
setAudioFile();
|
||||
for (int i = 0; i < 100; i++) {
|
||||
await _animationController.forward();
|
||||
await _animationController.reverse();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _submit() async {
|
||||
backToHome();
|
||||
try {
|
||||
final roomModel = RoomModel(
|
||||
name: widget.incomingCallData.name,
|
||||
token: widget.incomingCallData.sessionId,
|
||||
identity: widget.incomingCallData.identity);
|
||||
|
||||
await Navigator.of(context).push(
|
||||
MaterialPageRoute<ConferencePage>(
|
||||
fullscreenDialog: true,
|
||||
builder: (BuildContext context) =>
|
||||
ConferencePage(roomModel: roomModel),
|
||||
),
|
||||
);
|
||||
} catch (err) {
|
||||
print(err);
|
||||
await PlatformExceptionAlertDialog(
|
||||
exception: err,
|
||||
).show(context);
|
||||
}
|
||||
}
|
||||
|
||||
void backToHome() {
|
||||
player.stop();
|
||||
disposeAudioResources();
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
|
||||
disposeAudioResources() async {
|
||||
await player.dispose();
|
||||
}
|
||||
|
||||
void setAudioFile() async {
|
||||
player.stop();
|
||||
await player.setVolume(1.0); // full volume
|
||||
try {
|
||||
await player.setAsset('assets/sounds/ring_60Sec.mp3').then((value) {
|
||||
player.setLoopMode(LoopMode.one); // loop ring sound
|
||||
player.play();
|
||||
}).catchError((err) {
|
||||
print("Error: $err");
|
||||
});
|
||||
} catch (e) {
|
||||
print("Error: $e");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,10 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class NavigationService {
|
||||
final GlobalKey<NavigatorState> navigatorKey =
|
||||
new GlobalKey<NavigatorState>();
|
||||
|
||||
Future<dynamic> navigateTo(String routeName) {
|
||||
return navigatorKey.currentState.pushNamed(routeName);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue