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
|
||||
# TODO:
|
||||
# - member-ordering
|
||||
# - avoid-late-keyword
|
||||
# - avoid-non-null-assertion
|
||||
# - avoid-global-state
|
||||
# - prefer-match-file-name
|
||||
# - avoid-banned-imports:
|
||||
|
|
@ -61,6 +59,7 @@ dart_code_linter:
|
|||
- prefer-media-query-direct-access
|
||||
- avoid-wrapping-in-padding
|
||||
- prefer-correct-edge-insets-constructor
|
||||
- avoid-returning-widgets
|
||||
# TODO:
|
||||
# - avoid-returning-widgets
|
||||
# - prefer-single-widget-per-file:
|
||||
|
|
|
|||
|
|
@ -34,120 +34,6 @@ class ChatView extends StatelessWidget {
|
|||
|
||||
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
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
|
|
@ -238,7 +124,118 @@ class ChatView extends StatelessWidget {
|
|||
),
|
||||
titleSpacing: FluffyThemes.isColumnMode(context) ? 24 : 0,
|
||||
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(
|
||||
preferredSize: Size.fromHeight(appbarBottomHeight),
|
||||
child: Column(
|
||||
|
|
|
|||
|
|
@ -392,7 +392,10 @@ class InputBar extends StatelessWidget {
|
|||
controller: controller,
|
||||
focusNode: focusNode,
|
||||
readOnly: readOnly,
|
||||
contextMenuBuilder: (c, e) => markdownContextBuilder(c, e, controller),
|
||||
contextMenuBuilder: (c, e) => MarkdownContextBuilder(
|
||||
editableTextState: e,
|
||||
controller: controller,
|
||||
),
|
||||
contentInsertionConfiguration: ContentInsertionConfiguration(
|
||||
onContentInserted: (KeyboardInsertedContent content) {
|
||||
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
|
||||
Widget build(BuildContext context) {
|
||||
return PIPView(
|
||||
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(
|
||||
resizeToAvoidBottomInset: !isFloating,
|
||||
floatingActionButtonLocation:
|
||||
|
|
@ -577,16 +445,147 @@ class MyCallingPage extends State<Calling> {
|
|||
height: 150.0,
|
||||
child: Row(
|
||||
mainAxisAlignment: .spaceAround,
|
||||
children: _buildActionButtons(isFloating),
|
||||
children: actionButtons,
|
||||
),
|
||||
),
|
||||
body: OrientationBuilder(
|
||||
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(
|
||||
decoration: const BoxDecoration(color: Colors.black87),
|
||||
child: Stack(
|
||||
children: [
|
||||
..._buildContent(orientation, isFloating),
|
||||
...stackWidgets,
|
||||
if (!isFloating)
|
||||
Positioned(
|
||||
top: 24.0,
|
||||
|
|
|
|||
|
|
@ -3,128 +3,136 @@ import 'package:flutter/material.dart';
|
|||
import 'package:fluffychat/l10n/l10n.dart';
|
||||
import 'package:fluffychat/widgets/adaptive_dialogs/show_text_input_dialog.dart';
|
||||
|
||||
Widget markdownContextBuilder(
|
||||
BuildContext context,
|
||||
EditableTextState editableTextState,
|
||||
TextEditingController controller,
|
||||
) {
|
||||
final value = editableTextState.textEditingValue;
|
||||
final selectedText = value.selection.textInside(value.text);
|
||||
final buttonItems = editableTextState.contextMenuButtonItems;
|
||||
final l10n = L10n.of(context);
|
||||
class MarkdownContextBuilder extends StatelessWidget {
|
||||
final EditableTextState editableTextState;
|
||||
final TextEditingController controller;
|
||||
|
||||
return AdaptiveTextSelectionToolbar.buttonItems(
|
||||
anchors: editableTextState.contextMenuAnchors,
|
||||
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 MarkdownContextBuilder({
|
||||
required this.editableTextState,
|
||||
required this.controller,
|
||||
super.key,
|
||||
});
|
||||
|
||||
var start = selection.textBefore(text).lastIndexOf('\n');
|
||||
if (start == -1) start = 0;
|
||||
final end = selection.end;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final value = editableTextState.textEditingValue;
|
||||
final selectedText = value.selection.textInside(value.text);
|
||||
final buttonItems = editableTextState.contextMenuButtonItems;
|
||||
final l10n = L10n.of(context);
|
||||
|
||||
final fullLineSelection = TextSelection(
|
||||
baseOffset: start,
|
||||
extentOffset: end,
|
||||
);
|
||||
return AdaptiveTextSelectionToolbar.buttonItems(
|
||||
anchors: editableTextState.contextMenuAnchors,
|
||||
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
|
||||
.textInside(text)
|
||||
.split('\n')
|
||||
.map(
|
||||
(line) => line.startsWith(checkBox) || line.isEmpty
|
||||
? line
|
||||
: '$checkBox $line',
|
||||
)
|
||||
.join('\n');
|
||||
controller.text = controller.text.replaceRange(
|
||||
start,
|
||||
end,
|
||||
replacedRange,
|
||||
);
|
||||
ContextMenuController.removeAny();
|
||||
},
|
||||
),
|
||||
ContextMenuButtonItem(
|
||||
label: l10n.boldText,
|
||||
onPressed: () {
|
||||
final selection = controller.selection;
|
||||
controller.text = controller.text.replaceRange(
|
||||
selection.start,
|
||||
selection.end,
|
||||
'**$selectedText**',
|
||||
);
|
||||
ContextMenuController.removeAny();
|
||||
},
|
||||
),
|
||||
ContextMenuButtonItem(
|
||||
label: l10n.italicText,
|
||||
onPressed: () {
|
||||
final selection = controller.selection;
|
||||
controller.text = controller.text.replaceRange(
|
||||
selection.start,
|
||||
selection.end,
|
||||
'*$selectedText*',
|
||||
);
|
||||
ContextMenuController.removeAny();
|
||||
},
|
||||
),
|
||||
ContextMenuButtonItem(
|
||||
label: l10n.strikeThrough,
|
||||
onPressed: () {
|
||||
final selection = controller.selection;
|
||||
controller.text = controller.text.replaceRange(
|
||||
selection.start,
|
||||
selection.end,
|
||||
'~~$selectedText~~',
|
||||
);
|
||||
ContextMenuController.removeAny();
|
||||
},
|
||||
),
|
||||
final fullLineSelection = TextSelection(
|
||||
baseOffset: start,
|
||||
extentOffset: end,
|
||||
);
|
||||
|
||||
const checkBox = '- [ ]';
|
||||
|
||||
final replacedRange = fullLineSelection
|
||||
.textInside(text)
|
||||
.split('\n')
|
||||
.map(
|
||||
(line) => line.startsWith(checkBox) || line.isEmpty
|
||||
? line
|
||||
: '$checkBox $line',
|
||||
)
|
||||
.join('\n');
|
||||
controller.text = controller.text.replaceRange(
|
||||
start,
|
||||
end,
|
||||
replacedRange,
|
||||
);
|
||||
ContextMenuController.removeAny();
|
||||
},
|
||||
),
|
||||
ContextMenuButtonItem(
|
||||
label: l10n.boldText,
|
||||
onPressed: () {
|
||||
final selection = controller.selection;
|
||||
controller.text = controller.text.replaceRange(
|
||||
selection.start,
|
||||
selection.end,
|
||||
'**$selectedText**',
|
||||
);
|
||||
ContextMenuController.removeAny();
|
||||
},
|
||||
),
|
||||
ContextMenuButtonItem(
|
||||
label: l10n.italicText,
|
||||
onPressed: () {
|
||||
final selection = controller.selection;
|
||||
controller.text = controller.text.replaceRange(
|
||||
selection.start,
|
||||
selection.end,
|
||||
'*$selectedText*',
|
||||
);
|
||||
ContextMenuController.removeAny();
|
||||
},
|
||||
),
|
||||
ContextMenuButtonItem(
|
||||
label: l10n.strikeThrough,
|
||||
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());
|
||||
}
|
||||
|
||||
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
|
||||
Widget build(BuildContext context) {
|
||||
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