mirror of
https://github.com/dz0ny/meshcore-sar.git
synced 2026-08-11 16:30:28 +00:00
Improve message input layout
This commit is contained in:
@@ -103,15 +103,28 @@ class _HomeScreenState extends State<HomeScreen>
|
|||||||
void _initTabController() {
|
void _initTabController() {
|
||||||
_tabController = TabController(length: _enabledTabs.length, vsync: this);
|
_tabController = TabController(length: _enabledTabs.length, vsync: this);
|
||||||
_tabController.addListener(_onTabChanged);
|
_tabController.addListener(_onTabChanged);
|
||||||
|
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
if (!mounted) return;
|
||||||
|
_handleTabActivated(_currentTab);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onTabChanged() {
|
void _onTabChanged() {
|
||||||
|
final previousTab = _currentTab;
|
||||||
|
final nextIndex = _tabController.index;
|
||||||
|
|
||||||
setState(() {
|
setState(() {
|
||||||
_currentIndex = _tabController.index;
|
_currentIndex = nextIndex;
|
||||||
if (_currentTab != _HomeTab.map) {
|
if (_currentTab != _HomeTab.map) {
|
||||||
_isMapFullscreen = false;
|
_isMapFullscreen = false;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
final nextTab = _currentTab;
|
||||||
|
if (previousTab != nextTab) {
|
||||||
|
_handleTabActivated(nextTab);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _updateTabController({
|
void _updateTabController({
|
||||||
@@ -158,6 +171,23 @@ class _HomeScreenState extends State<HomeScreen>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _handleTabActivated(_HomeTab tab) {
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
if (!mounted) return;
|
||||||
|
|
||||||
|
switch (tab) {
|
||||||
|
case _HomeTab.messages:
|
||||||
|
context.read<MessagesProvider>().markAllAsRead();
|
||||||
|
break;
|
||||||
|
case _HomeTab.contacts:
|
||||||
|
context.read<ContactsProvider>().markAllAsViewed();
|
||||||
|
break;
|
||||||
|
case _HomeTab.map:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _loadRxTxPreference() async {
|
Future<void> _loadRxTxPreference() async {
|
||||||
final prefs = await SharedPreferences.getInstance();
|
final prefs = await SharedPreferences.getInstance();
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
|
|||||||
@@ -287,6 +287,18 @@ class _MessagesTabState extends State<MessagesTab> {
|
|||||||
return 'Select recipient';
|
return 'Select recipient';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String _getDestinationLabel() {
|
||||||
|
if (_destinationType ==
|
||||||
|
MessageDestinationPreferences.destinationTypeChannel &&
|
||||||
|
_selectedRecipient != null) {
|
||||||
|
return _selectedRecipient!.getLocalizedDisplayName(context);
|
||||||
|
}
|
||||||
|
if (_selectedRecipient != null) {
|
||||||
|
return _selectedRecipient!.displayName;
|
||||||
|
}
|
||||||
|
return 'Public Channel';
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _sendMessage() async {
|
Future<void> _sendMessage() async {
|
||||||
final text = _textController.text.trim();
|
final text = _textController.text.trim();
|
||||||
if (text.isEmpty) return;
|
if (text.isEmpty) return;
|
||||||
@@ -1561,95 +1573,182 @@ class _MessagesTabState extends State<MessagesTab> {
|
|||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
Padding(
|
SafeArea(
|
||||||
padding: const EdgeInsets.symmetric(
|
top: false,
|
||||||
horizontal: 8,
|
child: Padding(
|
||||||
vertical: 8,
|
padding: const EdgeInsets.fromLTRB(12, 12, 12, 12),
|
||||||
),
|
child: Column(
|
||||||
child: Row(
|
mainAxisSize: MainAxisSize.min,
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
// Quick actions (+) button
|
Row(
|
||||||
IconButton(
|
children: [
|
||||||
icon: Icon(_isRecording ? Icons.stop : Icons.add),
|
Container(
|
||||||
|
width: 46,
|
||||||
|
height: 46,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Theme.of(context)
|
||||||
|
.colorScheme
|
||||||
|
.primaryContainer
|
||||||
|
.withValues(alpha: 0.95),
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
child: IconButton(
|
||||||
|
icon: Icon(
|
||||||
|
_isRecording ? Icons.stop : Icons.add,
|
||||||
|
),
|
||||||
tooltip: _isRecording
|
tooltip: _isRecording
|
||||||
? 'Stop recording'
|
? 'Stop recording'
|
||||||
: 'More actions',
|
: 'More actions',
|
||||||
onPressed: _isRecording
|
onPressed: _isRecording
|
||||||
? _stopAndSendVoice
|
? _stopAndSendVoice
|
||||||
: _showComposerActions,
|
: _showComposerActions,
|
||||||
style: IconButton.styleFrom(
|
color: _isRecording
|
||||||
backgroundColor: Theme.of(
|
|
||||||
context,
|
|
||||||
).colorScheme.primaryContainer,
|
|
||||||
foregroundColor: _isRecording
|
|
||||||
? Colors.red
|
? Colors.red
|
||||||
: Theme.of(
|
: Theme.of(
|
||||||
context,
|
context,
|
||||||
).colorScheme.onPrimaryContainer,
|
).colorScheme.onPrimaryContainer,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 4),
|
const SizedBox(width: 10),
|
||||||
// Destination switcher button
|
Expanded(
|
||||||
IconButton(
|
child: Material(
|
||||||
icon: Icon(_getDestinationIcon()),
|
color: Colors.transparent,
|
||||||
tooltip: _getDestinationTooltip(),
|
child: InkWell(
|
||||||
onPressed: _showRecipientSelector,
|
borderRadius: BorderRadius.circular(22),
|
||||||
style: IconButton.styleFrom(
|
onTap: _showRecipientSelector,
|
||||||
backgroundColor:
|
child: Ink(
|
||||||
|
height: 46,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color:
|
||||||
|
_destinationType ==
|
||||||
|
MessageDestinationPreferences
|
||||||
|
.destinationTypeChannel
|
||||||
|
? Theme.of(context)
|
||||||
|
.colorScheme
|
||||||
|
.surfaceContainerHighest
|
||||||
|
: Theme.of(
|
||||||
|
context,
|
||||||
|
).colorScheme.secondaryContainer,
|
||||||
|
borderRadius: BorderRadius.circular(22),
|
||||||
|
border: Border.all(
|
||||||
|
color: Theme.of(
|
||||||
|
context,
|
||||||
|
).dividerColor.withValues(alpha: 0.7),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 14,
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
_getDestinationIcon(),
|
||||||
|
size: 18,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 10),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
_getDestinationLabel(),
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 15,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color:
|
||||||
_destinationType ==
|
_destinationType ==
|
||||||
MessageDestinationPreferences
|
MessageDestinationPreferences
|
||||||
.destinationTypeChannel
|
.destinationTypeChannel
|
||||||
? Theme.of(
|
? Theme.of(
|
||||||
context,
|
context,
|
||||||
).colorScheme.surfaceContainerHighest
|
).colorScheme.onSurface
|
||||||
: Theme.of(
|
: Theme.of(context)
|
||||||
context,
|
.colorScheme
|
||||||
).colorScheme.secondaryContainer,
|
.onSecondaryContainer,
|
||||||
foregroundColor:
|
|
||||||
_destinationType ==
|
|
||||||
MessageDestinationPreferences
|
|
||||||
.destinationTypeChannel
|
|
||||||
? Theme.of(context).colorScheme.onSurface
|
|
||||||
: Theme.of(
|
|
||||||
context,
|
|
||||||
).colorScheme.onSecondaryContainer,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 4),
|
),
|
||||||
|
Icon(
|
||||||
|
Icons.expand_more_rounded,
|
||||||
|
size: 20,
|
||||||
|
color: Theme.of(
|
||||||
|
context,
|
||||||
|
).colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
|
child: AnimatedContainer(
|
||||||
|
duration: const Duration(milliseconds: 180),
|
||||||
|
constraints: const BoxConstraints(
|
||||||
|
minHeight: 48,
|
||||||
|
maxHeight: 140,
|
||||||
|
),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Theme.of(
|
||||||
|
context,
|
||||||
|
).colorScheme.surfaceContainerHigh,
|
||||||
|
borderRadius: BorderRadius.circular(28),
|
||||||
|
border: Border.all(
|
||||||
|
color: _focusNode.hasFocus
|
||||||
|
? Theme.of(
|
||||||
|
context,
|
||||||
|
).colorScheme.primary
|
||||||
|
: Theme.of(context).dividerColor
|
||||||
|
.withValues(alpha: 0.6),
|
||||||
|
width: _focusNode.hasFocus ? 1.5 : 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 18,
|
||||||
|
vertical: 12,
|
||||||
|
),
|
||||||
child: TextField(
|
child: TextField(
|
||||||
controller: _textController,
|
controller: _textController,
|
||||||
focusNode: _focusNode,
|
focusNode: _focusNode,
|
||||||
maxLength: _maxCharacters,
|
minLines: 1,
|
||||||
maxLines: null,
|
maxLines: 4,
|
||||||
maxLengthEnforcement: MaxLengthEnforcement.enforced,
|
keyboardType: TextInputType.multiline,
|
||||||
style: const TextStyle(fontSize: 14),
|
inputFormatters: [
|
||||||
|
LengthLimitingTextInputFormatter(
|
||||||
|
_maxCharacters,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
style: const TextStyle(fontSize: 15),
|
||||||
|
textAlignVertical:
|
||||||
|
TextAlignVertical.center,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
hintText: AppLocalizations.of(
|
hintText: AppLocalizations.of(
|
||||||
context,
|
context,
|
||||||
)!.typeYourMessage,
|
)!.typeYourMessage,
|
||||||
hintStyle: const TextStyle(fontSize: 14),
|
hintStyle: TextStyle(
|
||||||
border: OutlineInputBorder(
|
fontSize: 15,
|
||||||
borderRadius: BorderRadius.circular(24),
|
color: Theme.of(
|
||||||
),
|
|
||||||
contentPadding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: 16,
|
|
||||||
vertical: 10,
|
|
||||||
),
|
|
||||||
isDense: true,
|
|
||||||
counterText: _characterCount >= 150
|
|
||||||
? '$_characterCount/$_maxCharacters'
|
|
||||||
: '',
|
|
||||||
counterStyle: TextStyle(
|
|
||||||
fontSize: 10,
|
|
||||||
color: _characterCount > _maxCharacters * 0.9
|
|
||||||
? Colors.orange
|
|
||||||
: Theme.of(
|
|
||||||
context,
|
context,
|
||||||
).textTheme.bodySmall?.color,
|
).colorScheme.onSurfaceVariant,
|
||||||
),
|
),
|
||||||
suffixIcon: Builder(
|
border: InputBorder.none,
|
||||||
|
isCollapsed: true,
|
||||||
|
),
|
||||||
|
textInputAction: TextInputAction.newline,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 10),
|
||||||
|
Builder(
|
||||||
builder: (context) {
|
builder: (context) {
|
||||||
final canSendText =
|
final canSendText =
|
||||||
!_isRecording &&
|
!_isRecording &&
|
||||||
@@ -1698,50 +1797,101 @@ class _MessagesTabState extends State<MessagesTab> {
|
|||||||
(_voiceSupported && _isRecording)
|
(_voiceSupported && _isRecording)
|
||||||
? () => _stopAndSendVoice()
|
? () => _stopAndSendVoice()
|
||||||
: null,
|
: null,
|
||||||
child: IconButton(
|
child: Column(
|
||||||
icon: _isSendingVoice
|
mainAxisSize: MainAxisSize.min,
|
||||||
? const SizedBox(
|
children: [
|
||||||
width: 18,
|
AnimatedContainer(
|
||||||
height: 18,
|
duration: const Duration(
|
||||||
|
milliseconds: 180,
|
||||||
|
),
|
||||||
|
width: 48,
|
||||||
|
height: 48,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color:
|
||||||
|
canSendText || _isRecording
|
||||||
|
? Theme.of(
|
||||||
|
context,
|
||||||
|
).colorScheme.primary
|
||||||
|
: Theme.of(context)
|
||||||
|
.colorScheme
|
||||||
|
.surfaceContainerHighest,
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
boxShadow:
|
||||||
|
canSendText || _isRecording
|
||||||
|
? [
|
||||||
|
BoxShadow(
|
||||||
|
color:
|
||||||
|
Theme.of(context)
|
||||||
|
.colorScheme
|
||||||
|
.primary
|
||||||
|
.withValues(
|
||||||
|
alpha: 0.28,
|
||||||
|
),
|
||||||
|
blurRadius: 16,
|
||||||
|
offset: const Offset(
|
||||||
|
0,
|
||||||
|
6,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
child: _isSendingVoice
|
||||||
|
? Center(
|
||||||
child:
|
child:
|
||||||
CircularProgressIndicator(
|
CircularProgressIndicator(
|
||||||
strokeWidth: 2,
|
strokeWidth: 2,
|
||||||
|
color:
|
||||||
|
Theme.of(
|
||||||
|
context,
|
||||||
|
)
|
||||||
|
.colorScheme
|
||||||
|
.onPrimary,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
: Icon(
|
: Icon(
|
||||||
_isRecording
|
_isRecording
|
||||||
? Icons.mic
|
? Icons.mic_rounded
|
||||||
: Icons.send_rounded,
|
: Icons.send_rounded,
|
||||||
size: 22,
|
color:
|
||||||
color: _isRecording
|
canSendText ||
|
||||||
? Colors.red
|
_isRecording
|
||||||
: (_textController.text
|
? Theme.of(context)
|
||||||
.trim()
|
.colorScheme
|
||||||
.isEmpty
|
.onPrimary
|
||||||
? Theme.of(
|
|
||||||
context,
|
|
||||||
).disabledColor
|
|
||||||
: Theme.of(context)
|
: Theme.of(context)
|
||||||
.colorScheme
|
.colorScheme
|
||||||
.primary),
|
.onSurfaceVariant,
|
||||||
),
|
),
|
||||||
onPressed: canSendText
|
),
|
||||||
? _sendMessage
|
const SizedBox(height: 6),
|
||||||
: null,
|
Text(
|
||||||
|
'$_characterCount/$_maxCharacters',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color:
|
||||||
|
_characterCount >
|
||||||
|
_maxCharacters * 0.9
|
||||||
|
? Colors.orange.shade800
|
||||||
|
: Theme.of(context)
|
||||||
|
.colorScheme
|
||||||
|
.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
textInputAction: TextInputAction.send,
|
|
||||||
onSubmitted: (_) => _sendMessage(),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
Reference in New Issue
Block a user