From b471ab09908678ebfa08ea6ef5d499b324b0419a Mon Sep 17 00:00:00 2001 From: Janez T Date: Sun, 8 Mar 2026 20:29:55 +0100 Subject: [PATCH] Add message font scale setting --- lib/providers/app_provider.dart | 27 +++++ lib/screens/settings_screen.dart | 22 +++++ lib/widgets/messages/message_bubble.dart | 104 +++++++++++++------- lib/widgets/messages/messages_composer.dart | 10 +- 4 files changed, 128 insertions(+), 35 deletions(-) diff --git a/lib/providers/app_provider.dart b/lib/providers/app_provider.dart index e538d00..07dd84e 100644 --- a/lib/providers/app_provider.dart +++ b/lib/providers/app_provider.dart @@ -90,6 +90,8 @@ class AppProvider with ChangeNotifier { bool get isVoiceCompressorEnabled => _isVoiceCompressorEnabled; bool _isVoiceLimiterEnabled = true; bool get isVoiceLimiterEnabled => _isVoiceLimiterEnabled; + double _messageFontScale = 1.0; + double get messageFontScale => _messageFontScale; bool _autoAddDiscoveredContacts = false; bool get autoAddDiscoveredContacts => _autoAddDiscoveredContacts; bool _autoRouteRotationEnabled = @@ -144,6 +146,7 @@ class AppProvider with ChangeNotifier { _loadVoiceBandPassFilterEnabled(); _loadVoiceCompressorEnabled(); _loadVoiceLimiterEnabled(); + _loadMessageFontScale(); _loadAutoAddDiscoveredContacts(); _loadMessagingRouteSettings(); unawaited(_pathHistoryService.initialize()); @@ -431,6 +434,30 @@ class AppProvider with ChangeNotifier { } } + Future _loadMessageFontScale() async { + try { + final prefs = await SharedPreferences.getInstance(); + _messageFontScale = (prefs.getDouble('message_font_scale') ?? 1.0).clamp( + 0.85, + 1.4, + ); + notifyListeners(); + } catch (e) { + debugPrint('Error loading message font scale setting: $e'); + } + } + + Future setMessageFontScale(double scale) async { + try { + _messageFontScale = scale.clamp(0.85, 1.4); + final prefs = await SharedPreferences.getInstance(); + await prefs.setDouble('message_font_scale', _messageFontScale); + notifyListeners(); + } catch (e) { + debugPrint('Error saving message font scale setting: $e'); + } + } + /// Load auto-add discovered contacts setting from shared preferences. Future _loadAutoAddDiscoveredContacts() async { try { diff --git a/lib/screens/settings_screen.dart b/lib/screens/settings_screen.dart index 0984c9c..64128b2 100644 --- a/lib/screens/settings_screen.dart +++ b/lib/screens/settings_screen.dart @@ -1023,6 +1023,28 @@ class _SettingsScreenState extends State { subtitle: const Text('Delete all stored message history'), onTap: _clearMessages, ), + Consumer( + builder: (context, appProvider, child) => ListTile( + leading: const Icon(Icons.format_size), + title: const Text('Message font size'), + subtitle: Text( + '${(appProvider.messageFontScale * 100).round()}% of default', + ), + trailing: SizedBox( + width: 150, + child: Slider( + value: appProvider.messageFontScale, + min: 0.85, + max: 1.4, + divisions: 11, + label: '${(appProvider.messageFontScale * 100).round()}%', + onChanged: (value) { + appProvider.setMessageFontScale(value); + }, + ), + ), + ), + ), ]), _buildSectionHeader('Voice'), diff --git a/lib/widgets/messages/message_bubble.dart b/lib/widgets/messages/message_bubble.dart index a454633..639b898 100644 --- a/lib/widgets/messages/message_bubble.dart +++ b/lib/widgets/messages/message_bubble.dart @@ -13,6 +13,7 @@ import '../../providers/connection_provider.dart'; import '../../providers/drawing_provider.dart'; import '../../providers/voice_provider.dart'; import '../../providers/image_provider.dart' as ip; +import '../../providers/app_provider.dart'; import '../drawing_minimap_preview.dart'; import '../../models/ble_packet_log.dart'; import '../../services/sar_template_service.dart'; @@ -1649,6 +1650,36 @@ class _MessageBubbleState extends State { return null; } + Widget _buildDirectMessageStatusIndicator( + BuildContext context, + Message message, + ) { + switch (message.deliveryStatus) { + case MessageDeliveryStatus.delivered: + return Icon( + Icons.done_all, + size: 16, + color: Theme.of(context).colorScheme.primary, + ); + case MessageDeliveryStatus.sent: + return Icon( + Icons.done, + size: 16, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ); + case MessageDeliveryStatus.sending: + return Icon(Icons.schedule, size: 15, color: Colors.orange); + case MessageDeliveryStatus.failed: + return const Icon(Icons.error_outline, size: 15, color: Colors.red); + case MessageDeliveryStatus.received: + return Icon( + Icons.done_all, + size: 16, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ); + } + } + @override Widget build(BuildContext context) { // Display system messages with minimal styling @@ -1666,6 +1697,7 @@ class _MessageBubbleState extends State { } final isSarMarker = message.isSarMarker; final isDarkMode = Theme.of(context).brightness == Brightness.dark; + final messageFontScale = context.watch().messageFontScale; // Determine if this is own message final connectionProvider = context.read(); @@ -1779,6 +1811,9 @@ class _MessageBubbleState extends State { : null; final shouldFloatBubble = widget.isCompact; + final baseBodyStyle = Theme.of( + context, + ).textTheme.bodyMedium?.copyWith(fontSize: 14 * messageFontScale); final bubble = ConstrainedBox( constraints: BoxConstraints( maxWidth: shouldFloatBubble @@ -2197,10 +2232,7 @@ class _MessageBubbleState extends State { ); if (drawing == null) { - return Text( - message.text, - style: Theme.of(context).textTheme.bodyMedium, - ); + return Text(message.text, style: baseBodyStyle); } final String drawingTypeLabel; @@ -2286,10 +2318,7 @@ class _MessageBubbleState extends State { ) // Regular message content else if (!message.isDrawing || widget.isCompact) - Text( - message.text, - style: Theme.of(context).textTheme.bodyMedium, - ), + Text(message.text, style: baseBodyStyle), if (!widget.isCompact && !isSarMarker && @@ -2509,33 +2538,42 @@ class _MessageBubbleState extends State { Row( mainAxisSize: MainAxisSize.max, children: [ - Icon( - getDeliveryStatusIcon(message.deliveryStatus), - size: 12, - color: getDeliveryStatusColor( - message.deliveryStatus, - ), - ), - const SizedBox(width: 3), - Expanded( - child: Align( - alignment: Alignment.centerLeft, - child: Text( - message.getLocalizedDeliveryStatus(context), - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: Theme.of(context) - .textTheme - .labelSmall - ?.copyWith( - color: getDeliveryStatusColor( - message.deliveryStatus, - ), - fontStyle: FontStyle.italic, - ), + if (message.isContactMessage) + _buildDirectMessageStatusIndicator( + context, + message, + ) + else ...[ + Icon( + getDeliveryStatusIcon(message.deliveryStatus), + size: 12, + color: getDeliveryStatusColor( + message.deliveryStatus, ), ), - ), + const SizedBox(width: 3), + Expanded( + child: Align( + alignment: Alignment.centerLeft, + child: Text( + message.getLocalizedDeliveryStatus( + context, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: Theme.of(context) + .textTheme + .labelSmall + ?.copyWith( + color: getDeliveryStatusColor( + message.deliveryStatus, + ), + fontStyle: FontStyle.italic, + ), + ), + ), + ), + ], // Show retry button for failed messages if (message.deliveryStatus == MessageDeliveryStatus.failed) ...[ diff --git a/lib/widgets/messages/messages_composer.dart b/lib/widgets/messages/messages_composer.dart index 7a3ad16..8b492fa 100644 --- a/lib/widgets/messages/messages_composer.dart +++ b/lib/widgets/messages/messages_composer.dart @@ -1,7 +1,9 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:provider/provider.dart'; import '../../l10n/app_localizations.dart'; +import '../../providers/app_provider.dart'; class MessagesComposer extends StatelessWidget { final TextEditingController textController; @@ -253,6 +255,10 @@ class _MessageInput extends StatelessWidget { @override Widget build(BuildContext context) { + final messageFontScale = context.watch().messageFontScale; + const baseFontSize = 15.0; + final resolvedFontSize = baseFontSize * messageFontScale; + return AnimatedContainer( duration: const Duration(milliseconds: 180), constraints: const BoxConstraints(minHeight: 46, maxHeight: 132), @@ -286,12 +292,12 @@ class _MessageInput extends StatelessWidget { maxLines: 4, keyboardType: TextInputType.multiline, inputFormatters: [messageByteLimiter], - style: const TextStyle(fontSize: 15), + style: TextStyle(fontSize: resolvedFontSize), textAlignVertical: TextAlignVertical.center, decoration: InputDecoration( hintText: AppLocalizations.of(context)!.typeYourMessage, hintStyle: TextStyle( - fontSize: 15, + fontSize: resolvedFontSize, color: Theme.of( context, ).colorScheme.onSurfaceVariant.withValues(alpha: 0.9),