refactor: Enable avoid-returning-widgets lint

This commit is contained in:
Christian Kußowski 2026-02-24 09:09:49 +01:00
parent 67d9eaf5b7
commit ab976db8e7
No known key found for this signature in database
GPG key ID: E067ECD60F1A0652
6 changed files with 497 additions and 473 deletions

View file

@ -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:

View 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(

View file

@ -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;

View file

@ -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,

View file

@ -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();
},
),
],
],
],
);
);
}
}

View file

@ -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),
);
}
}