Merge pull request #2611 from krille-chan/krille/avoid-returning-widgets

refactor: Enable avoid-returning-widgets lint
This commit is contained in:
Krille-chan 2026-02-24 09:21:52 +01:00 committed by GitHub
commit f797bce8d0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 497 additions and 473 deletions

View file

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

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

View file

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

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

View file

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

View file

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