Merge pull request #2611 from krille-chan/krille/avoid-returning-widgets
refactor: Enable avoid-returning-widgets lint
This commit is contained in:
commit
f797bce8d0
6 changed files with 497 additions and 473 deletions
|
|
@ -45,8 +45,6 @@ dart_code_linter:
|
||||||
- avoid-unnecessary-conditionals
|
- avoid-unnecessary-conditionals
|
||||||
# TODO:
|
# TODO:
|
||||||
# - member-ordering
|
# - member-ordering
|
||||||
# - avoid-late-keyword
|
|
||||||
# - avoid-non-null-assertion
|
|
||||||
# - avoid-global-state
|
# - avoid-global-state
|
||||||
# - prefer-match-file-name
|
# - prefer-match-file-name
|
||||||
# - avoid-banned-imports:
|
# - avoid-banned-imports:
|
||||||
|
|
@ -61,6 +59,7 @@ dart_code_linter:
|
||||||
- prefer-media-query-direct-access
|
- prefer-media-query-direct-access
|
||||||
- avoid-wrapping-in-padding
|
- avoid-wrapping-in-padding
|
||||||
- prefer-correct-edge-insets-constructor
|
- prefer-correct-edge-insets-constructor
|
||||||
|
- avoid-returning-widgets
|
||||||
# TODO:
|
# TODO:
|
||||||
# - avoid-returning-widgets
|
# - avoid-returning-widgets
|
||||||
# - prefer-single-widget-per-file:
|
# - prefer-single-widget-per-file:
|
||||||
|
|
|
||||||
|
|
@ -34,120 +34,6 @@ class ChatView extends StatelessWidget {
|
||||||
|
|
||||||
const ChatView(this.controller, {super.key});
|
const ChatView(this.controller, {super.key});
|
||||||
|
|
||||||
List<Widget> _appBarActions(BuildContext context) {
|
|
||||||
if (controller.selectMode) {
|
|
||||||
return [
|
|
||||||
if (controller.canEditSelectedEvents)
|
|
||||||
IconButton(
|
|
||||||
icon: const Icon(Icons.edit_outlined),
|
|
||||||
tooltip: L10n.of(context).edit,
|
|
||||||
onPressed: controller.editSelectedEventAction,
|
|
||||||
),
|
|
||||||
if (controller.selectedEvents.length == 1 &&
|
|
||||||
controller.activeThreadId == null &&
|
|
||||||
controller.room.canSendDefaultMessages)
|
|
||||||
IconButton(
|
|
||||||
icon: const Icon(Icons.message_outlined),
|
|
||||||
tooltip: L10n.of(context).replyInThread,
|
|
||||||
onPressed: () => controller.enterThread(
|
|
||||||
controller.selectedEvents.single.eventId,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
IconButton(
|
|
||||||
icon: const Icon(Icons.copy_outlined),
|
|
||||||
tooltip: L10n.of(context).copyToClipboard,
|
|
||||||
onPressed: controller.copyEventsAction,
|
|
||||||
),
|
|
||||||
if (controller.canRedactSelectedEvents)
|
|
||||||
IconButton(
|
|
||||||
icon: const Icon(Icons.delete_outlined),
|
|
||||||
tooltip: L10n.of(context).redactMessage,
|
|
||||||
onPressed: controller.redactEventsAction,
|
|
||||||
),
|
|
||||||
if (controller.selectedEvents.length == 1)
|
|
||||||
PopupMenuButton<_EventContextAction>(
|
|
||||||
useRootNavigator: true,
|
|
||||||
onSelected: (action) {
|
|
||||||
switch (action) {
|
|
||||||
case _EventContextAction.info:
|
|
||||||
controller.showEventInfo();
|
|
||||||
controller.clearSelectedEvents();
|
|
||||||
break;
|
|
||||||
case _EventContextAction.report:
|
|
||||||
controller.reportEventAction();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
itemBuilder: (context) => [
|
|
||||||
if (controller.canPinSelectedEvents)
|
|
||||||
PopupMenuItem(
|
|
||||||
onTap: controller.pinEvent,
|
|
||||||
value: null,
|
|
||||||
child: Row(
|
|
||||||
mainAxisSize: .min,
|
|
||||||
children: [
|
|
||||||
const Icon(Icons.push_pin_outlined),
|
|
||||||
const SizedBox(width: 12),
|
|
||||||
Text(L10n.of(context).pinMessage),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (controller.canSaveSelectedEvent)
|
|
||||||
PopupMenuItem(
|
|
||||||
onTap: () => controller.saveSelectedEvent(context),
|
|
||||||
value: null,
|
|
||||||
child: Row(
|
|
||||||
mainAxisSize: .min,
|
|
||||||
children: [
|
|
||||||
const Icon(Icons.download_outlined),
|
|
||||||
const SizedBox(width: 12),
|
|
||||||
Text(L10n.of(context).downloadFile),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
PopupMenuItem(
|
|
||||||
value: _EventContextAction.info,
|
|
||||||
child: Row(
|
|
||||||
mainAxisSize: .min,
|
|
||||||
children: [
|
|
||||||
const Icon(Icons.info_outlined),
|
|
||||||
const SizedBox(width: 12),
|
|
||||||
Text(L10n.of(context).messageInfo),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (controller.selectedEvents.single.status.isSent)
|
|
||||||
PopupMenuItem(
|
|
||||||
value: _EventContextAction.report,
|
|
||||||
child: Row(
|
|
||||||
mainAxisSize: .min,
|
|
||||||
children: [
|
|
||||||
const Icon(Icons.shield_outlined, color: Colors.red),
|
|
||||||
const SizedBox(width: 12),
|
|
||||||
Text(L10n.of(context).reportMessage),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
];
|
|
||||||
} else if (!controller.room.isArchived) {
|
|
||||||
return [
|
|
||||||
if (AppSettings.experimentalVoip.value &&
|
|
||||||
Matrix.of(context).voipPlugin != null &&
|
|
||||||
controller.room.isDirectChat)
|
|
||||||
IconButton(
|
|
||||||
onPressed: controller.onPhoneButtonTap,
|
|
||||||
icon: const Icon(Icons.call_outlined),
|
|
||||||
tooltip: L10n.of(context).placeCall,
|
|
||||||
),
|
|
||||||
EncryptionButton(controller.room),
|
|
||||||
ChatSettingsPopupMenu(controller.room, true),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
|
|
@ -238,7 +124,118 @@ class ChatView extends StatelessWidget {
|
||||||
),
|
),
|
||||||
titleSpacing: FluffyThemes.isColumnMode(context) ? 24 : 0,
|
titleSpacing: FluffyThemes.isColumnMode(context) ? 24 : 0,
|
||||||
title: ChatAppBarTitle(controller),
|
title: ChatAppBarTitle(controller),
|
||||||
actions: _appBarActions(context),
|
actions: [
|
||||||
|
if (controller.selectMode) ...[
|
||||||
|
if (controller.canEditSelectedEvents)
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.edit_outlined),
|
||||||
|
tooltip: L10n.of(context).edit,
|
||||||
|
onPressed: controller.editSelectedEventAction,
|
||||||
|
),
|
||||||
|
if (controller.selectedEvents.length == 1 &&
|
||||||
|
controller.activeThreadId == null &&
|
||||||
|
controller.room.canSendDefaultMessages)
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.message_outlined),
|
||||||
|
tooltip: L10n.of(context).replyInThread,
|
||||||
|
onPressed: () => controller.enterThread(
|
||||||
|
controller.selectedEvents.single.eventId,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.copy_outlined),
|
||||||
|
tooltip: L10n.of(context).copyToClipboard,
|
||||||
|
onPressed: controller.copyEventsAction,
|
||||||
|
),
|
||||||
|
if (controller.canRedactSelectedEvents)
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.delete_outlined),
|
||||||
|
tooltip: L10n.of(context).redactMessage,
|
||||||
|
onPressed: controller.redactEventsAction,
|
||||||
|
),
|
||||||
|
if (controller.selectedEvents.length == 1)
|
||||||
|
PopupMenuButton<_EventContextAction>(
|
||||||
|
useRootNavigator: true,
|
||||||
|
onSelected: (action) {
|
||||||
|
switch (action) {
|
||||||
|
case _EventContextAction.info:
|
||||||
|
controller.showEventInfo();
|
||||||
|
controller.clearSelectedEvents();
|
||||||
|
break;
|
||||||
|
case _EventContextAction.report:
|
||||||
|
controller.reportEventAction();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
itemBuilder: (context) => [
|
||||||
|
if (controller.canPinSelectedEvents)
|
||||||
|
PopupMenuItem(
|
||||||
|
onTap: controller.pinEvent,
|
||||||
|
value: null,
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: .min,
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.push_pin_outlined),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Text(L10n.of(context).pinMessage),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (controller.canSaveSelectedEvent)
|
||||||
|
PopupMenuItem(
|
||||||
|
onTap: () =>
|
||||||
|
controller.saveSelectedEvent(context),
|
||||||
|
value: null,
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: .min,
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.download_outlined),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Text(L10n.of(context).downloadFile),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
PopupMenuItem(
|
||||||
|
value: _EventContextAction.info,
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: .min,
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.info_outlined),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Text(L10n.of(context).messageInfo),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (controller.selectedEvents.single.status.isSent)
|
||||||
|
PopupMenuItem(
|
||||||
|
value: _EventContextAction.report,
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: .min,
|
||||||
|
children: [
|
||||||
|
const Icon(
|
||||||
|
Icons.shield_outlined,
|
||||||
|
color: Colors.red,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Text(L10n.of(context).reportMessage),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
] else if (!controller.room.isArchived) ...[
|
||||||
|
if (AppSettings.experimentalVoip.value &&
|
||||||
|
Matrix.of(context).voipPlugin != null &&
|
||||||
|
controller.room.isDirectChat)
|
||||||
|
IconButton(
|
||||||
|
onPressed: controller.onPhoneButtonTap,
|
||||||
|
icon: const Icon(Icons.call_outlined),
|
||||||
|
tooltip: L10n.of(context).placeCall,
|
||||||
|
),
|
||||||
|
EncryptionButton(controller.room),
|
||||||
|
ChatSettingsPopupMenu(controller.room, true),
|
||||||
|
],
|
||||||
|
],
|
||||||
bottom: PreferredSize(
|
bottom: PreferredSize(
|
||||||
preferredSize: Size.fromHeight(appbarBottomHeight),
|
preferredSize: Size.fromHeight(appbarBottomHeight),
|
||||||
child: Column(
|
child: Column(
|
||||||
|
|
|
||||||
|
|
@ -392,7 +392,10 @@ class InputBar extends StatelessWidget {
|
||||||
controller: controller,
|
controller: controller,
|
||||||
focusNode: focusNode,
|
focusNode: focusNode,
|
||||||
readOnly: readOnly,
|
readOnly: readOnly,
|
||||||
contextMenuBuilder: (c, e) => markdownContextBuilder(c, e, controller),
|
contextMenuBuilder: (c, e) => MarkdownContextBuilder(
|
||||||
|
editableTextState: e,
|
||||||
|
controller: controller,
|
||||||
|
),
|
||||||
contentInsertionConfiguration: ContentInsertionConfiguration(
|
contentInsertionConfiguration: ContentInsertionConfiguration(
|
||||||
onContentInserted: (KeyboardInsertedContent content) {
|
onContentInserted: (KeyboardInsertedContent content) {
|
||||||
final data = content.data;
|
final data = content.data;
|
||||||
|
|
|
||||||
|
|
@ -339,235 +339,103 @@ class MyCallingPage extends State<Calling> {
|
||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
List<Widget> _buildActionButtons(bool isFloating) {
|
|
||||||
if (isFloating) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
final switchCameraButton = FloatingActionButton(
|
|
||||||
heroTag: 'switchCamera',
|
|
||||||
onPressed: _switchCamera,
|
|
||||||
backgroundColor: Colors.black45,
|
|
||||||
child: const Icon(Icons.switch_camera),
|
|
||||||
);
|
|
||||||
/*
|
|
||||||
var switchSpeakerButton = FloatingActionButton(
|
|
||||||
heroTag: 'switchSpeaker',
|
|
||||||
child: Icon(_speakerOn ? Icons.volume_up : Icons.volume_off),
|
|
||||||
onPressed: _switchSpeaker,
|
|
||||||
foregroundColor: Colors.black54,
|
|
||||||
backgroundColor: Theme.of(widget.context).backgroundColor,
|
|
||||||
);
|
|
||||||
*/
|
|
||||||
final hangupButton = FloatingActionButton(
|
|
||||||
heroTag: 'hangup',
|
|
||||||
onPressed: _hangUp,
|
|
||||||
tooltip: 'Hangup',
|
|
||||||
backgroundColor: _state == CallState.kEnded ? Colors.black45 : Colors.red,
|
|
||||||
child: const Icon(Icons.call_end),
|
|
||||||
);
|
|
||||||
|
|
||||||
final answerButton = FloatingActionButton(
|
|
||||||
heroTag: 'answer',
|
|
||||||
onPressed: _answerCall,
|
|
||||||
tooltip: 'Answer',
|
|
||||||
backgroundColor: Colors.green,
|
|
||||||
child: const Icon(Icons.phone),
|
|
||||||
);
|
|
||||||
|
|
||||||
final muteMicButton = FloatingActionButton(
|
|
||||||
heroTag: 'muteMic',
|
|
||||||
onPressed: _muteMic,
|
|
||||||
foregroundColor: isMicrophoneMuted ? Colors.black26 : Colors.white,
|
|
||||||
backgroundColor: isMicrophoneMuted ? Colors.white : Colors.black45,
|
|
||||||
child: Icon(isMicrophoneMuted ? Icons.mic_off : Icons.mic),
|
|
||||||
);
|
|
||||||
|
|
||||||
final screenSharingButton = FloatingActionButton(
|
|
||||||
heroTag: 'screenSharing',
|
|
||||||
onPressed: _screenSharing,
|
|
||||||
foregroundColor: isScreensharingEnabled ? Colors.black26 : Colors.white,
|
|
||||||
backgroundColor: isScreensharingEnabled ? Colors.white : Colors.black45,
|
|
||||||
child: const Icon(Icons.desktop_mac),
|
|
||||||
);
|
|
||||||
|
|
||||||
final holdButton = FloatingActionButton(
|
|
||||||
heroTag: 'hold',
|
|
||||||
onPressed: _remoteOnHold,
|
|
||||||
foregroundColor: isRemoteOnHold ? Colors.black26 : Colors.white,
|
|
||||||
backgroundColor: isRemoteOnHold ? Colors.white : Colors.black45,
|
|
||||||
child: const Icon(Icons.pause),
|
|
||||||
);
|
|
||||||
|
|
||||||
final muteCameraButton = FloatingActionButton(
|
|
||||||
heroTag: 'muteCam',
|
|
||||||
onPressed: _muteCamera,
|
|
||||||
foregroundColor: isLocalVideoMuted ? Colors.black26 : Colors.white,
|
|
||||||
backgroundColor: isLocalVideoMuted ? Colors.white : Colors.black45,
|
|
||||||
child: Icon(isLocalVideoMuted ? Icons.videocam_off : Icons.videocam),
|
|
||||||
);
|
|
||||||
|
|
||||||
switch (_state) {
|
|
||||||
case CallState.kRinging:
|
|
||||||
case CallState.kInviteSent:
|
|
||||||
case CallState.kCreateAnswer:
|
|
||||||
case CallState.kConnecting:
|
|
||||||
return call.isOutgoing
|
|
||||||
? <Widget>[hangupButton]
|
|
||||||
: <Widget>[answerButton, hangupButton];
|
|
||||||
case CallState.kConnected:
|
|
||||||
return <Widget>[
|
|
||||||
muteMicButton,
|
|
||||||
//switchSpeakerButton,
|
|
||||||
if (!voiceonly && !kIsWeb) switchCameraButton,
|
|
||||||
if (!voiceonly) muteCameraButton,
|
|
||||||
if (PlatformInfos.isMobile || PlatformInfos.isWeb)
|
|
||||||
screenSharingButton,
|
|
||||||
holdButton,
|
|
||||||
hangupButton,
|
|
||||||
];
|
|
||||||
case CallState.kEnded:
|
|
||||||
return <Widget>[hangupButton];
|
|
||||||
case CallState.kFledgling:
|
|
||||||
case CallState.kWaitLocalMedia:
|
|
||||||
case CallState.kCreateOffer:
|
|
||||||
case CallState.kEnding:
|
|
||||||
case null:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
return <Widget>[];
|
|
||||||
}
|
|
||||||
|
|
||||||
List<Widget> _buildContent(Orientation orientation, bool isFloating) {
|
|
||||||
final stackWidgets = <Widget>[];
|
|
||||||
|
|
||||||
final call = this.call;
|
|
||||||
if (call.callHasEnded) {
|
|
||||||
return stackWidgets;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (call.localHold || call.remoteOnHold) {
|
|
||||||
var title = '';
|
|
||||||
if (call.localHold) {
|
|
||||||
title =
|
|
||||||
'${call.room.getLocalizedDisplayname(MatrixLocals(L10n.of(widget.context)))} held the call.';
|
|
||||||
} else if (call.remoteOnHold) {
|
|
||||||
title = 'You held the call.';
|
|
||||||
}
|
|
||||||
stackWidgets.add(
|
|
||||||
Center(
|
|
||||||
child: Column(
|
|
||||||
mainAxisAlignment: .center,
|
|
||||||
children: [
|
|
||||||
const Icon(Icons.pause, size: 48.0, color: Colors.white),
|
|
||||||
Text(
|
|
||||||
title,
|
|
||||||
style: const TextStyle(color: Colors.white, fontSize: 24.0),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
return stackWidgets;
|
|
||||||
}
|
|
||||||
|
|
||||||
var primaryStream =
|
|
||||||
call.remoteScreenSharingStream ??
|
|
||||||
call.localScreenSharingStream ??
|
|
||||||
call.remoteUserMediaStream ??
|
|
||||||
call.localUserMediaStream;
|
|
||||||
|
|
||||||
if (!connected) {
|
|
||||||
primaryStream = call.localUserMediaStream;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (primaryStream != null) {
|
|
||||||
stackWidgets.add(
|
|
||||||
Center(
|
|
||||||
child: _StreamView(
|
|
||||||
primaryStream,
|
|
||||||
mainView: true,
|
|
||||||
matrixClient: widget.client,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isFloating || !connected) {
|
|
||||||
return stackWidgets;
|
|
||||||
}
|
|
||||||
|
|
||||||
_resizeLocalVideo(orientation);
|
|
||||||
|
|
||||||
if (call.getRemoteStreams.isEmpty) {
|
|
||||||
return stackWidgets;
|
|
||||||
}
|
|
||||||
|
|
||||||
final secondaryStreamViews = <Widget>[];
|
|
||||||
|
|
||||||
if (call.remoteScreenSharingStream != null) {
|
|
||||||
final remoteUserMediaStream = call.remoteUserMediaStream;
|
|
||||||
secondaryStreamViews.add(
|
|
||||||
SizedBox(
|
|
||||||
width: _localVideoWidth,
|
|
||||||
height: _localVideoHeight,
|
|
||||||
child: _StreamView(
|
|
||||||
remoteUserMediaStream!,
|
|
||||||
matrixClient: widget.client,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
secondaryStreamViews.add(const SizedBox(height: 10));
|
|
||||||
}
|
|
||||||
|
|
||||||
final localStream =
|
|
||||||
call.localUserMediaStream ?? call.localScreenSharingStream;
|
|
||||||
if (localStream != null && !isFloating) {
|
|
||||||
secondaryStreamViews.add(
|
|
||||||
SizedBox(
|
|
||||||
width: _localVideoWidth,
|
|
||||||
height: _localVideoHeight,
|
|
||||||
child: _StreamView(localStream, matrixClient: widget.client),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
secondaryStreamViews.add(const SizedBox(height: 10));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (call.localScreenSharingStream != null && !isFloating) {
|
|
||||||
secondaryStreamViews.add(
|
|
||||||
SizedBox(
|
|
||||||
width: _localVideoWidth,
|
|
||||||
height: _localVideoHeight,
|
|
||||||
child: _StreamView(
|
|
||||||
call.remoteUserMediaStream!,
|
|
||||||
matrixClient: widget.client,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
secondaryStreamViews.add(const SizedBox(height: 10));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (secondaryStreamViews.isNotEmpty) {
|
|
||||||
stackWidgets.add(
|
|
||||||
Container(
|
|
||||||
padding: const EdgeInsets.only(top: 20, bottom: 120),
|
|
||||||
alignment: Alignment.bottomRight,
|
|
||||||
child: Container(
|
|
||||||
width: _localVideoWidth,
|
|
||||||
margin: _localVideoMargin,
|
|
||||||
child: Column(children: secondaryStreamViews),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return stackWidgets;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return PIPView(
|
return PIPView(
|
||||||
builder: (context, isFloating) {
|
builder: (context, isFloating) {
|
||||||
|
// Build action buttons
|
||||||
|
final switchCameraButton = FloatingActionButton(
|
||||||
|
heroTag: 'switchCamera',
|
||||||
|
onPressed: _switchCamera,
|
||||||
|
backgroundColor: Colors.black45,
|
||||||
|
child: const Icon(Icons.switch_camera),
|
||||||
|
);
|
||||||
|
final hangupButton = FloatingActionButton(
|
||||||
|
heroTag: 'hangup',
|
||||||
|
onPressed: _hangUp,
|
||||||
|
tooltip: 'Hangup',
|
||||||
|
backgroundColor: _state == CallState.kEnded
|
||||||
|
? Colors.black45
|
||||||
|
: Colors.red,
|
||||||
|
child: const Icon(Icons.call_end),
|
||||||
|
);
|
||||||
|
final answerButton = FloatingActionButton(
|
||||||
|
heroTag: 'answer',
|
||||||
|
onPressed: _answerCall,
|
||||||
|
tooltip: 'Answer',
|
||||||
|
backgroundColor: Colors.green,
|
||||||
|
child: const Icon(Icons.phone),
|
||||||
|
);
|
||||||
|
final muteMicButton = FloatingActionButton(
|
||||||
|
heroTag: 'muteMic',
|
||||||
|
onPressed: _muteMic,
|
||||||
|
foregroundColor: isMicrophoneMuted ? Colors.black26 : Colors.white,
|
||||||
|
backgroundColor: isMicrophoneMuted ? Colors.white : Colors.black45,
|
||||||
|
child: Icon(isMicrophoneMuted ? Icons.mic_off : Icons.mic),
|
||||||
|
);
|
||||||
|
final screenSharingButton = FloatingActionButton(
|
||||||
|
heroTag: 'screenSharing',
|
||||||
|
onPressed: _screenSharing,
|
||||||
|
foregroundColor: isScreensharingEnabled
|
||||||
|
? Colors.black26
|
||||||
|
: Colors.white,
|
||||||
|
backgroundColor: isScreensharingEnabled
|
||||||
|
? Colors.white
|
||||||
|
: Colors.black45,
|
||||||
|
child: const Icon(Icons.desktop_mac),
|
||||||
|
);
|
||||||
|
final holdButton = FloatingActionButton(
|
||||||
|
heroTag: 'hold',
|
||||||
|
onPressed: _remoteOnHold,
|
||||||
|
foregroundColor: isRemoteOnHold ? Colors.black26 : Colors.white,
|
||||||
|
backgroundColor: isRemoteOnHold ? Colors.white : Colors.black45,
|
||||||
|
child: const Icon(Icons.pause),
|
||||||
|
);
|
||||||
|
final muteCameraButton = FloatingActionButton(
|
||||||
|
heroTag: 'muteCam',
|
||||||
|
onPressed: _muteCamera,
|
||||||
|
foregroundColor: isLocalVideoMuted ? Colors.black26 : Colors.white,
|
||||||
|
backgroundColor: isLocalVideoMuted ? Colors.white : Colors.black45,
|
||||||
|
child: Icon(isLocalVideoMuted ? Icons.videocam_off : Icons.videocam),
|
||||||
|
);
|
||||||
|
|
||||||
|
late final List<Widget> actionButtons;
|
||||||
|
if (!isFloating) {
|
||||||
|
switch (_state) {
|
||||||
|
case CallState.kRinging:
|
||||||
|
case CallState.kInviteSent:
|
||||||
|
case CallState.kCreateAnswer:
|
||||||
|
case CallState.kConnecting:
|
||||||
|
actionButtons = call.isOutgoing
|
||||||
|
? <Widget>[hangupButton]
|
||||||
|
: <Widget>[answerButton, hangupButton];
|
||||||
|
break;
|
||||||
|
case CallState.kConnected:
|
||||||
|
actionButtons = <Widget>[
|
||||||
|
muteMicButton,
|
||||||
|
if (!voiceonly && !kIsWeb) switchCameraButton,
|
||||||
|
if (!voiceonly) muteCameraButton,
|
||||||
|
if (PlatformInfos.isMobile || PlatformInfos.isWeb)
|
||||||
|
screenSharingButton,
|
||||||
|
holdButton,
|
||||||
|
hangupButton,
|
||||||
|
];
|
||||||
|
break;
|
||||||
|
case CallState.kEnded:
|
||||||
|
actionButtons = <Widget>[hangupButton];
|
||||||
|
break;
|
||||||
|
case CallState.kFledgling:
|
||||||
|
case CallState.kWaitLocalMedia:
|
||||||
|
case CallState.kCreateOffer:
|
||||||
|
case CallState.kEnding:
|
||||||
|
case null:
|
||||||
|
actionButtons = <Widget>[];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
actionButtons = <Widget>[];
|
||||||
|
}
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
resizeToAvoidBottomInset: !isFloating,
|
resizeToAvoidBottomInset: !isFloating,
|
||||||
floatingActionButtonLocation:
|
floatingActionButtonLocation:
|
||||||
|
|
@ -577,16 +445,147 @@ class MyCallingPage extends State<Calling> {
|
||||||
height: 150.0,
|
height: 150.0,
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment: .spaceAround,
|
mainAxisAlignment: .spaceAround,
|
||||||
children: _buildActionButtons(isFloating),
|
children: actionButtons,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
body: OrientationBuilder(
|
body: OrientationBuilder(
|
||||||
builder: (BuildContext context, Orientation orientation) {
|
builder: (BuildContext context, Orientation orientation) {
|
||||||
|
final stackWidgets = <Widget>[];
|
||||||
|
|
||||||
|
final callHasEnded = call.callHasEnded;
|
||||||
|
if (!callHasEnded) {
|
||||||
|
if (call.localHold || call.remoteOnHold) {
|
||||||
|
var title = '';
|
||||||
|
if (call.localHold) {
|
||||||
|
title =
|
||||||
|
'${call.room.getLocalizedDisplayname(MatrixLocals(L10n.of(widget.context)))} held the call.';
|
||||||
|
} else if (call.remoteOnHold) {
|
||||||
|
title = 'You held the call.';
|
||||||
|
}
|
||||||
|
stackWidgets.add(
|
||||||
|
Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: .center,
|
||||||
|
children: [
|
||||||
|
const Icon(
|
||||||
|
Icons.pause,
|
||||||
|
size: 48.0,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
title,
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontSize: 24.0,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
var primaryStream =
|
||||||
|
call.remoteScreenSharingStream ??
|
||||||
|
call.localScreenSharingStream ??
|
||||||
|
call.remoteUserMediaStream ??
|
||||||
|
call.localUserMediaStream;
|
||||||
|
|
||||||
|
if (!connected) {
|
||||||
|
primaryStream = call.localUserMediaStream;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (primaryStream != null) {
|
||||||
|
stackWidgets.add(
|
||||||
|
Center(
|
||||||
|
child: _StreamView(
|
||||||
|
primaryStream,
|
||||||
|
mainView: true,
|
||||||
|
matrixClient: widget.client,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isFloating && connected) {
|
||||||
|
_resizeLocalVideo(orientation);
|
||||||
|
|
||||||
|
if (call.getRemoteStreams.isNotEmpty) {
|
||||||
|
final secondaryStreamViews = <Widget>[];
|
||||||
|
|
||||||
|
if (call.remoteScreenSharingStream != null) {
|
||||||
|
final remoteUserMediaStream =
|
||||||
|
call.remoteUserMediaStream;
|
||||||
|
secondaryStreamViews.add(
|
||||||
|
SizedBox(
|
||||||
|
width: _localVideoWidth,
|
||||||
|
height: _localVideoHeight,
|
||||||
|
child: _StreamView(
|
||||||
|
remoteUserMediaStream!,
|
||||||
|
matrixClient: widget.client,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
secondaryStreamViews.add(const SizedBox(height: 10));
|
||||||
|
}
|
||||||
|
|
||||||
|
final localStream =
|
||||||
|
call.localUserMediaStream ??
|
||||||
|
call.localScreenSharingStream;
|
||||||
|
if (localStream != null && !isFloating) {
|
||||||
|
secondaryStreamViews.add(
|
||||||
|
SizedBox(
|
||||||
|
width: _localVideoWidth,
|
||||||
|
height: _localVideoHeight,
|
||||||
|
child: _StreamView(
|
||||||
|
localStream,
|
||||||
|
matrixClient: widget.client,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
secondaryStreamViews.add(const SizedBox(height: 10));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (call.localScreenSharingStream != null &&
|
||||||
|
!isFloating) {
|
||||||
|
secondaryStreamViews.add(
|
||||||
|
SizedBox(
|
||||||
|
width: _localVideoWidth,
|
||||||
|
height: _localVideoHeight,
|
||||||
|
child: _StreamView(
|
||||||
|
call.remoteUserMediaStream!,
|
||||||
|
matrixClient: widget.client,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
secondaryStreamViews.add(const SizedBox(height: 10));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (secondaryStreamViews.isNotEmpty) {
|
||||||
|
stackWidgets.add(
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.only(
|
||||||
|
top: 20,
|
||||||
|
bottom: 120,
|
||||||
|
),
|
||||||
|
alignment: Alignment.bottomRight,
|
||||||
|
child: Container(
|
||||||
|
width: _localVideoWidth,
|
||||||
|
margin: _localVideoMargin,
|
||||||
|
child: Column(children: secondaryStreamViews),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
decoration: const BoxDecoration(color: Colors.black87),
|
decoration: const BoxDecoration(color: Colors.black87),
|
||||||
child: Stack(
|
child: Stack(
|
||||||
children: [
|
children: [
|
||||||
..._buildContent(orientation, isFloating),
|
...stackWidgets,
|
||||||
if (!isFloating)
|
if (!isFloating)
|
||||||
Positioned(
|
Positioned(
|
||||||
top: 24.0,
|
top: 24.0,
|
||||||
|
|
|
||||||
|
|
@ -3,128 +3,136 @@ import 'package:flutter/material.dart';
|
||||||
import 'package:fluffychat/l10n/l10n.dart';
|
import 'package:fluffychat/l10n/l10n.dart';
|
||||||
import 'package:fluffychat/widgets/adaptive_dialogs/show_text_input_dialog.dart';
|
import 'package:fluffychat/widgets/adaptive_dialogs/show_text_input_dialog.dart';
|
||||||
|
|
||||||
Widget markdownContextBuilder(
|
class MarkdownContextBuilder extends StatelessWidget {
|
||||||
BuildContext context,
|
final EditableTextState editableTextState;
|
||||||
EditableTextState editableTextState,
|
final TextEditingController controller;
|
||||||
TextEditingController controller,
|
|
||||||
) {
|
|
||||||
final value = editableTextState.textEditingValue;
|
|
||||||
final selectedText = value.selection.textInside(value.text);
|
|
||||||
final buttonItems = editableTextState.contextMenuButtonItems;
|
|
||||||
final l10n = L10n.of(context);
|
|
||||||
|
|
||||||
return AdaptiveTextSelectionToolbar.buttonItems(
|
const MarkdownContextBuilder({
|
||||||
anchors: editableTextState.contextMenuAnchors,
|
required this.editableTextState,
|
||||||
buttonItems: [
|
required this.controller,
|
||||||
...buttonItems,
|
super.key,
|
||||||
if (selectedText.isNotEmpty) ...[
|
});
|
||||||
ContextMenuButtonItem(
|
|
||||||
label: l10n.link,
|
|
||||||
onPressed: () async {
|
|
||||||
final input = await showTextInputDialog(
|
|
||||||
context: context,
|
|
||||||
title: l10n.addLink,
|
|
||||||
okLabel: l10n.ok,
|
|
||||||
cancelLabel: l10n.cancel,
|
|
||||||
validator: (text) {
|
|
||||||
if (text.isEmpty) {
|
|
||||||
return l10n.pleaseFillOut;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
text.startsWith('http') ? Uri.parse(text) : Uri.https(text);
|
|
||||||
} catch (_) {
|
|
||||||
return l10n.invalidUrl;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
hintText: 'www...',
|
|
||||||
keyboardType: TextInputType.url,
|
|
||||||
);
|
|
||||||
final urlString = input;
|
|
||||||
if (urlString == null) return;
|
|
||||||
final url = urlString.startsWith('http')
|
|
||||||
? Uri.parse(urlString)
|
|
||||||
: Uri.https(urlString);
|
|
||||||
final selection = controller.selection;
|
|
||||||
controller.text = controller.text.replaceRange(
|
|
||||||
selection.start,
|
|
||||||
selection.end,
|
|
||||||
'[$selectedText](${url.toString()})',
|
|
||||||
);
|
|
||||||
ContextMenuController.removeAny();
|
|
||||||
},
|
|
||||||
),
|
|
||||||
ContextMenuButtonItem(
|
|
||||||
label: l10n.checkList,
|
|
||||||
onPressed: () {
|
|
||||||
final text = controller.text;
|
|
||||||
final selection = controller.selection;
|
|
||||||
|
|
||||||
var start = selection.textBefore(text).lastIndexOf('\n');
|
@override
|
||||||
if (start == -1) start = 0;
|
Widget build(BuildContext context) {
|
||||||
final end = selection.end;
|
final value = editableTextState.textEditingValue;
|
||||||
|
final selectedText = value.selection.textInside(value.text);
|
||||||
|
final buttonItems = editableTextState.contextMenuButtonItems;
|
||||||
|
final l10n = L10n.of(context);
|
||||||
|
|
||||||
final fullLineSelection = TextSelection(
|
return AdaptiveTextSelectionToolbar.buttonItems(
|
||||||
baseOffset: start,
|
anchors: editableTextState.contextMenuAnchors,
|
||||||
extentOffset: end,
|
buttonItems: [
|
||||||
);
|
...buttonItems,
|
||||||
|
if (selectedText.isNotEmpty) ...[
|
||||||
|
ContextMenuButtonItem(
|
||||||
|
label: l10n.link,
|
||||||
|
onPressed: () async {
|
||||||
|
final input = await showTextInputDialog(
|
||||||
|
context: context,
|
||||||
|
title: l10n.addLink,
|
||||||
|
okLabel: l10n.ok,
|
||||||
|
cancelLabel: l10n.cancel,
|
||||||
|
validator: (text) {
|
||||||
|
if (text.isEmpty) {
|
||||||
|
return l10n.pleaseFillOut;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
text.startsWith('http') ? Uri.parse(text) : Uri.https(text);
|
||||||
|
} catch (_) {
|
||||||
|
return l10n.invalidUrl;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
hintText: 'www...',
|
||||||
|
keyboardType: TextInputType.url,
|
||||||
|
);
|
||||||
|
final urlString = input;
|
||||||
|
if (urlString == null) return;
|
||||||
|
final url = urlString.startsWith('http')
|
||||||
|
? Uri.parse(urlString)
|
||||||
|
: Uri.https(urlString);
|
||||||
|
final selection = controller.selection;
|
||||||
|
controller.text = controller.text.replaceRange(
|
||||||
|
selection.start,
|
||||||
|
selection.end,
|
||||||
|
'[$selectedText](${url.toString()})',
|
||||||
|
);
|
||||||
|
ContextMenuController.removeAny();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
ContextMenuButtonItem(
|
||||||
|
label: l10n.checkList,
|
||||||
|
onPressed: () {
|
||||||
|
final text = controller.text;
|
||||||
|
final selection = controller.selection;
|
||||||
|
|
||||||
const checkBox = '- [ ]';
|
var start = selection.textBefore(text).lastIndexOf('\n');
|
||||||
|
if (start == -1) start = 0;
|
||||||
|
final end = selection.end;
|
||||||
|
|
||||||
final replacedRange = fullLineSelection
|
final fullLineSelection = TextSelection(
|
||||||
.textInside(text)
|
baseOffset: start,
|
||||||
.split('\n')
|
extentOffset: end,
|
||||||
.map(
|
);
|
||||||
(line) => line.startsWith(checkBox) || line.isEmpty
|
|
||||||
? line
|
const checkBox = '- [ ]';
|
||||||
: '$checkBox $line',
|
|
||||||
)
|
final replacedRange = fullLineSelection
|
||||||
.join('\n');
|
.textInside(text)
|
||||||
controller.text = controller.text.replaceRange(
|
.split('\n')
|
||||||
start,
|
.map(
|
||||||
end,
|
(line) => line.startsWith(checkBox) || line.isEmpty
|
||||||
replacedRange,
|
? line
|
||||||
);
|
: '$checkBox $line',
|
||||||
ContextMenuController.removeAny();
|
)
|
||||||
},
|
.join('\n');
|
||||||
),
|
controller.text = controller.text.replaceRange(
|
||||||
ContextMenuButtonItem(
|
start,
|
||||||
label: l10n.boldText,
|
end,
|
||||||
onPressed: () {
|
replacedRange,
|
||||||
final selection = controller.selection;
|
);
|
||||||
controller.text = controller.text.replaceRange(
|
ContextMenuController.removeAny();
|
||||||
selection.start,
|
},
|
||||||
selection.end,
|
),
|
||||||
'**$selectedText**',
|
ContextMenuButtonItem(
|
||||||
);
|
label: l10n.boldText,
|
||||||
ContextMenuController.removeAny();
|
onPressed: () {
|
||||||
},
|
final selection = controller.selection;
|
||||||
),
|
controller.text = controller.text.replaceRange(
|
||||||
ContextMenuButtonItem(
|
selection.start,
|
||||||
label: l10n.italicText,
|
selection.end,
|
||||||
onPressed: () {
|
'**$selectedText**',
|
||||||
final selection = controller.selection;
|
);
|
||||||
controller.text = controller.text.replaceRange(
|
ContextMenuController.removeAny();
|
||||||
selection.start,
|
},
|
||||||
selection.end,
|
),
|
||||||
'*$selectedText*',
|
ContextMenuButtonItem(
|
||||||
);
|
label: l10n.italicText,
|
||||||
ContextMenuController.removeAny();
|
onPressed: () {
|
||||||
},
|
final selection = controller.selection;
|
||||||
),
|
controller.text = controller.text.replaceRange(
|
||||||
ContextMenuButtonItem(
|
selection.start,
|
||||||
label: l10n.strikeThrough,
|
selection.end,
|
||||||
onPressed: () {
|
'*$selectedText*',
|
||||||
final selection = controller.selection;
|
);
|
||||||
controller.text = controller.text.replaceRange(
|
ContextMenuController.removeAny();
|
||||||
selection.start,
|
},
|
||||||
selection.end,
|
),
|
||||||
'~~$selectedText~~',
|
ContextMenuButtonItem(
|
||||||
);
|
label: l10n.strikeThrough,
|
||||||
ContextMenuController.removeAny();
|
onPressed: () {
|
||||||
},
|
final selection = controller.selection;
|
||||||
),
|
controller.text = controller.text.replaceRange(
|
||||||
|
selection.start,
|
||||||
|
selection.end,
|
||||||
|
'~~$selectedText~~',
|
||||||
|
);
|
||||||
|
ContextMenuController.removeAny();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
],
|
],
|
||||||
],
|
);
|
||||||
);
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -128,15 +128,6 @@ class _MxcImageState extends State<MxcImage> {
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) => _tryLoad());
|
WidgetsBinding.instance.addPostFrameCallback((_) => _tryLoad());
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget placeholder(BuildContext context) =>
|
|
||||||
widget.placeholder?.call(context) ??
|
|
||||||
Container(
|
|
||||||
width: widget.width,
|
|
||||||
height: widget.height,
|
|
||||||
alignment: Alignment.center,
|
|
||||||
child: const CircularProgressIndicator.adaptive(strokeWidth: 2),
|
|
||||||
);
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final data = _imageData;
|
final data = _imageData;
|
||||||
|
|
@ -172,7 +163,34 @@ class _MxcImageState extends State<MxcImage> {
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
: placeholder(context),
|
: _MxcImagePlaceholder(
|
||||||
|
width: widget.width,
|
||||||
|
height: widget.height,
|
||||||
|
placeholder: widget.placeholder,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _MxcImagePlaceholder extends StatelessWidget {
|
||||||
|
final double? width;
|
||||||
|
final double? height;
|
||||||
|
final Widget Function(BuildContext context)? placeholder;
|
||||||
|
|
||||||
|
const _MxcImagePlaceholder({
|
||||||
|
required this.width,
|
||||||
|
required this.height,
|
||||||
|
required this.placeholder,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return placeholder?.call(context) ??
|
||||||
|
Container(
|
||||||
|
width: width,
|
||||||
|
height: height,
|
||||||
|
alignment: Alignment.center,
|
||||||
|
child: const CircularProgressIndicator.adaptive(strokeWidth: 2),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue