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
# 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,34 +339,26 @@ class MyCallingPage extends State<Calling> {
}
*/
List<Widget> _buildActionButtons(bool isFloating) {
if (isFloating) {
return [];
}
@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),
);
/*
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,
backgroundColor: _state == CallState.kEnded
? Colors.black45
: Colors.red,
child: const Icon(Icons.call_end),
);
final answerButton = FloatingActionButton(
heroTag: 'answer',
onPressed: _answerCall,
@ -374,7 +366,6 @@ class MyCallingPage extends State<Calling> {
backgroundColor: Colors.green,
child: const Icon(Icons.phone),
);
final muteMicButton = FloatingActionButton(
heroTag: 'muteMic',
onPressed: _muteMic,
@ -382,15 +373,17 @@ class MyCallingPage extends State<Calling> {
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,
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,
@ -398,7 +391,6 @@ class MyCallingPage extends State<Calling> {
backgroundColor: isRemoteOnHold ? Colors.white : Colors.black45,
child: const Icon(Icons.pause),
);
final muteCameraButton = FloatingActionButton(
heroTag: 'muteCam',
onPressed: _muteCamera,
@ -407,18 +399,20 @@ class MyCallingPage extends State<Calling> {
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:
return call.isOutgoing
actionButtons = call.isOutgoing
? <Widget>[hangupButton]
: <Widget>[answerButton, hangupButton];
break;
case CallState.kConnected:
return <Widget>[
actionButtons = <Widget>[
muteMicButton,
//switchSpeakerButton,
if (!voiceonly && !kIsWeb) switchCameraButton,
if (!voiceonly) muteCameraButton,
if (PlatformInfos.isMobile || PlatformInfos.isWeb)
@ -426,26 +420,40 @@ class MyCallingPage extends State<Calling> {
holdButton,
hangupButton,
];
break;
case CallState.kEnded:
return <Widget>[hangupButton];
actionButtons = <Widget>[hangupButton];
break;
case CallState.kFledgling:
case CallState.kWaitLocalMedia:
case CallState.kCreateOffer:
case CallState.kEnding:
case null:
actionButtons = <Widget>[];
break;
}
return <Widget>[];
} else {
actionButtons = <Widget>[];
}
List<Widget> _buildContent(Orientation orientation, bool isFloating) {
return Scaffold(
resizeToAvoidBottomInset: !isFloating,
floatingActionButtonLocation:
FloatingActionButtonLocation.centerFloat,
floatingActionButton: SizedBox(
width: 320.0,
height: 150.0,
child: Row(
mainAxisAlignment: .spaceAround,
children: actionButtons,
),
),
body: OrientationBuilder(
builder: (BuildContext context, Orientation orientation) {
final stackWidgets = <Widget>[];
final call = this.call;
if (call.callHasEnded) {
return stackWidgets;
}
final callHasEnded = call.callHasEnded;
if (!callHasEnded) {
if (call.localHold || call.remoteOnHold) {
var title = '';
if (call.localHold) {
@ -459,18 +467,23 @@ class MyCallingPage extends State<Calling> {
child: Column(
mainAxisAlignment: .center,
children: [
const Icon(Icons.pause, size: 48.0, color: Colors.white),
const Icon(
Icons.pause,
size: 48.0,
color: Colors.white,
),
Text(
title,
style: const TextStyle(color: Colors.white, fontSize: 24.0),
style: const TextStyle(
color: Colors.white,
fontSize: 24.0,
),
),
],
),
),
);
return stackWidgets;
}
} else {
var primaryStream =
call.remoteScreenSharingStream ??
call.localScreenSharingStream ??
@ -493,20 +506,15 @@ class MyCallingPage extends State<Calling> {
);
}
if (isFloating || !connected) {
return stackWidgets;
}
if (!isFloating && connected) {
_resizeLocalVideo(orientation);
if (call.getRemoteStreams.isEmpty) {
return stackWidgets;
}
if (call.getRemoteStreams.isNotEmpty) {
final secondaryStreamViews = <Widget>[];
if (call.remoteScreenSharingStream != null) {
final remoteUserMediaStream = call.remoteUserMediaStream;
final remoteUserMediaStream =
call.remoteUserMediaStream;
secondaryStreamViews.add(
SizedBox(
width: _localVideoWidth,
@ -521,19 +529,24 @@ class MyCallingPage extends State<Calling> {
}
final localStream =
call.localUserMediaStream ?? call.localScreenSharingStream;
call.localUserMediaStream ??
call.localScreenSharingStream;
if (localStream != null && !isFloating) {
secondaryStreamViews.add(
SizedBox(
width: _localVideoWidth,
height: _localVideoHeight,
child: _StreamView(localStream, matrixClient: widget.client),
child: _StreamView(
localStream,
matrixClient: widget.client,
),
),
);
secondaryStreamViews.add(const SizedBox(height: 10));
}
if (call.localScreenSharingStream != null && !isFloating) {
if (call.localScreenSharingStream != null &&
!isFloating) {
secondaryStreamViews.add(
SizedBox(
width: _localVideoWidth,
@ -550,7 +563,10 @@ class MyCallingPage extends State<Calling> {
if (secondaryStreamViews.isNotEmpty) {
stackWidgets.add(
Container(
padding: const EdgeInsets.only(top: 20, bottom: 120),
padding: const EdgeInsets.only(
top: 20,
bottom: 120,
),
alignment: Alignment.bottomRight,
child: Container(
width: _localVideoWidth,
@ -560,33 +576,16 @@ class MyCallingPage extends State<Calling> {
),
);
}
return stackWidgets;
}
}
}
}
@override
Widget build(BuildContext context) {
return PIPView(
builder: (context, isFloating) {
return Scaffold(
resizeToAvoidBottomInset: !isFloating,
floatingActionButtonLocation:
FloatingActionButtonLocation.centerFloat,
floatingActionButton: SizedBox(
width: 320.0,
height: 150.0,
child: Row(
mainAxisAlignment: .spaceAround,
children: _buildActionButtons(isFloating),
),
),
body: OrientationBuilder(
builder: (BuildContext context, Orientation orientation) {
return Container(
decoration: const BoxDecoration(color: Colors.black87),
child: Stack(
children: [
..._buildContent(orientation, isFloating),
...stackWidgets,
if (!isFloating)
Positioned(
top: 24.0,

View file

@ -3,11 +3,18 @@ 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,
) {
class MarkdownContextBuilder extends StatelessWidget {
final EditableTextState editableTextState;
final TextEditingController controller;
const MarkdownContextBuilder({
required this.editableTextState,
required this.controller,
super.key,
});
@override
Widget build(BuildContext context) {
final value = editableTextState.textEditingValue;
final selectedText = value.selection.textInside(value.text);
final buttonItems = editableTextState.contextMenuButtonItems;
@ -128,3 +135,4 @@ Widget markdownContextBuilder(
],
);
}
}

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