@tduncan / Cortana / commits / 3880fae

Fix Android nav-bar overlap on key screens

abdulmominsakib committed May 31, 2026 at 15:37 UTC 3880fae37124ddde4fb721c36b9b9e221a32cf74
5 files changed +82 -18
lib/core/utils/system_insets.dart new
+11
@@ -0,0 +1,11 @@
1 +import 'package:flutter/widgets.dart';
2 +
3 +/// Returns the persistent bottom system inset (e.g. Android navigation bar).
4 +double bottomSystemInset(BuildContext context) {
5 + return MediaQuery.viewPaddingOf(context).bottom;
6 +}
7 +
8 +/// Returns the bottom inset caused by the on-screen keyboard.
9 +double bottomKeyboardInset(BuildContext context) {
10 + return MediaQuery.viewInsetsOf(context).bottom;
11 +}
lib/features/chat/views/chat_screen.dart
+15 -4
@@ -16,6 +16,7 @@ import 'package:shadcn_ui/shadcn_ui.dart';
16 import '../../../core/models/enums.dart';
17 import '../../../core/providers/app_providers.dart';
18 import '../../../core/routes/app_routes.dart';
19 +import '../../../core/utils/system_insets.dart';
20 import '../../../l10n/app_localizations.dart';
21 import '../../conversations/data/models/conversation.dart';
22 import '../../servers/data/models/server.dart';
@@ -151,6 +152,11 @@ class _ChatScreenState extends ConsumerState<ChatScreen>
152 : null;
153 final theme = Theme.of(context);
154 final isDark = theme.brightness == Brightness.dark;
155 + final keyboardBottomInset = bottomKeyboardInset(context);
156 + final systemBottomInset = bottomSystemInset(context);
157 + final effectiveBottomInset = keyboardBottomInset > 0
158 + ? 0.0
159 + : systemBottomInset;
160
161 final hasNewMessage = messages.length != _lastMessageCount;
162 final streamingProgressed =
@@ -378,13 +384,14 @@ class _ChatScreenState extends ConsumerState<ChatScreen>
384 },
385 hasSmartReplies:
386 !isStreaming &&
381 - MediaQuery.of(context).viewInsets.bottom == 0 &&
387 + keyboardBottomInset == 0 &&
388 (ref
389 .watch(smartRepliesProvider)
390 .asData
391 ?.value
392 .isNotEmpty ??
393 false),
394 + bottomInset: effectiveBottomInset,
395 ),
396
397 Positioned(
@@ -407,8 +414,7 @@ class _ChatScreenState extends ConsumerState<ChatScreen>
414 child: Column(
415 mainAxisSize: MainAxisSize.min,
416 children: [
410 - if (!isStreaming &&
411 - MediaQuery.of(context).viewInsets.bottom == 0) ...[
417 + if (!isStreaming && keyboardBottomInset == 0) ...[
418 const SizedBox(height: 4),
419 _SmartReplyChips(
420 onSend: (message) {
@@ -1313,6 +1319,7 @@ class _MessageListConsumer extends ConsumerWidget {
1319 required this.onDelete,
1320 required this.onEdit,
1321 this.hasSmartReplies = false,
1322 + this.bottomInset = 0,
1323 });
1324
1325 final ScrollController scrollController;
@@ -1322,6 +1329,7 @@ class _MessageListConsumer extends ConsumerWidget {
1329 final void Function(String) onDelete;
1330 final void Function(String messageId, String currentContent) onEdit;
1331 final bool hasSmartReplies;
1332 + final double bottomInset;
1333
1334 @override
1335 Widget build(BuildContext context, WidgetRef ref) {
@@ -1338,6 +1346,7 @@ class _MessageListConsumer extends ConsumerWidget {
1346 onDelete: onDelete,
1347 onEdit: onEdit,
1348 hasSmartReplies: hasSmartReplies,
1349 + bottomInset: bottomInset,
1350 );
1351 }
1352 }
@@ -1352,6 +1361,7 @@ class _MessageList extends StatelessWidget {
1361 required this.onDelete,
1362 required this.onEdit,
1363 this.hasSmartReplies = false,
1364 + this.bottomInset = 0,
1365 });
1366
1367 final ScrollController scrollController;
@@ -1362,6 +1372,7 @@ class _MessageList extends StatelessWidget {
1372 final void Function(String) onDelete;
1373 final void Function(String messageId, String currentContent) onEdit;
1374 final bool hasSmartReplies;
1375 + final double bottomInset;
1376
1377 @override
1378 Widget build(BuildContext context) {
@@ -1381,7 +1392,7 @@ class _MessageList extends StatelessWidget {
1392 cacheExtent: 1000,
1393 padding: EdgeInsets.only(
1394 top: 16,
1384 - bottom: 120 + (hasSmartReplies ? 64 : 0),
1395 + bottom: 120 + (hasSmartReplies ? 64 : 0) + bottomInset,
1396 ),
1397 itemCount:
1398 allMessages.length +
lib/features/servers/views/add_server_screen.dart
+28 -8
@@ -8,6 +8,7 @@ import '../../../core/constants/app_constants.dart';
8 import '../data/models/server.dart';
9 import '../../../core/models/enums.dart';
10 import '../../../core/providers/service_providers.dart';
11 +import '../../../core/utils/system_insets.dart';
12 import '../providers/server_providers.dart';
13 import 'components/server_icon_picker.dart';
14 import 'components/server_type_selector.dart';
@@ -99,7 +100,9 @@ class _AddServerScreenState extends ConsumerState<AddServerScreen> {
100 });
101 } catch (e) {
102 setState(() {
102 - _testResult = AppLocalizations.of(context)!.error_with_message(e.toString());
103 + _testResult = AppLocalizations.of(
104 + context,
105 + )!.error_with_message(e.toString());
106 });
107 } finally {
108 setState(() {
@@ -160,7 +163,9 @@ class _AddServerScreenState extends ConsumerState<AddServerScreen> {
163 if (mounted) {
164 ScaffoldMessenger.of(context).showSnackBar(
165 SnackBar(
163 - content: Text(AppLocalizations.of(context)!.error_with_message(e.toString())),
166 + content: Text(
167 + AppLocalizations.of(context)!.error_with_message(e.toString()),
168 + ),
169 backgroundColor: Colors.red,
170 ),
171 );
@@ -248,6 +253,7 @@ class _AddServerScreenState extends ConsumerState<AddServerScreen> {
253 Widget build(BuildContext context) {
254 final l10n = AppLocalizations.of(context)!;
255 final theme = Theme.of(context);
256 + final systemBottomInset = bottomSystemInset(context);
257
258 return Scaffold(
259 appBar: AppBar(
@@ -260,7 +266,7 @@ class _AddServerScreenState extends ConsumerState<AddServerScreen> {
266 body: Form(
267 key: _formKey,
268 child: ListView(
263 - padding: const EdgeInsets.all(16),
269 + padding: EdgeInsets.fromLTRB(16, 16, 16, 16 + systemBottomInset),
270 children: [
271 Text(l10n.server_type_label, style: theme.textTheme.titleSmall),
272 const SizedBox(height: 8),
@@ -376,12 +382,18 @@ class _AddServerScreenState extends ConsumerState<AddServerScreen> {
382 Container(
383 padding: const EdgeInsets.all(12),
384 decoration: BoxDecoration(
379 - color: _testResult!.contains(l10n.connection_successful.split('!')[0])
385 + color:
386 + _testResult!.contains(
387 + l10n.connection_successful.split('!')[0],
388 + )
389 ? Colors.green.withAlpha(25)
390 : Colors.red.withAlpha(25),
391 borderRadius: BorderRadius.circular(8),
392 border: Border.all(
384 - color: _testResult!.contains(l10n.connection_successful.split('!')[0])
393 + color:
394 + _testResult!.contains(
395 + l10n.connection_successful.split('!')[0],
396 + )
397 ? Colors.green
398 : Colors.red,
399 ),
@@ -389,10 +401,15 @@ class _AddServerScreenState extends ConsumerState<AddServerScreen> {
401 child: Row(
402 children: [
403 Icon(
392 - _testResult!.contains(l10n.connection_successful.split('!')[0])
404 + _testResult!.contains(
405 + l10n.connection_successful.split('!')[0],
406 + )
407 ? Icons.check_circle
408 : Icons.error,
395 - color: _testResult!.contains(l10n.connection_successful.split('!')[0])
409 + color:
410 + _testResult!.contains(
411 + l10n.connection_successful.split('!')[0],
412 + )
413 ? Colors.green
414 : Colors.red,
415 ),
@@ -401,7 +418,10 @@ class _AddServerScreenState extends ConsumerState<AddServerScreen> {
418 child: Text(
419 _testResult!,
420 style: TextStyle(
404 - color: _testResult!.contains(l10n.connection_successful.split('!')[0])
421 + color:
422 + _testResult!.contains(
423 + l10n.connection_successful.split('!')[0],
424 + )
425 ? Colors.green
426 : Colors.red,
427 ),
lib/features/servers/views/server_list_screen.dart
+25 -5
@@ -5,6 +5,7 @@ import 'package:localmind/l10n/app_localizations.dart';
5 import '../../../core/models/enums.dart';
6 import '../../../core/providers/service_providers.dart';
7 import '../../../core/routes/app_routes.dart';
8 +import '../../../core/utils/system_insets.dart';
9 import '../providers/server_providers.dart';
10 import 'components/server_card.dart';
11
@@ -18,6 +19,7 @@ class ServerListScreen extends ConsumerWidget {
19 final activeServer = ref.watch(activeServerProvider);
20 final theme = Theme.of(context);
21 final isDark = theme.brightness == Brightness.dark;
22 + final systemBottomInset = bottomSystemInset(context);
23
24 return SafeArea(
25 child: Stack(
@@ -75,7 +77,12 @@ class ServerListScreen extends ConsumerWidget {
77 }
78 },
79 child: ListView.builder(
78 - padding: const EdgeInsets.all(16),
80 + padding: EdgeInsets.fromLTRB(
81 + 16,
82 + 16,
83 + 16,
84 + 16 + systemBottomInset + 80,
85 + ),
86 itemCount: serverList.length,
87 itemBuilder: (context, index) {
88 final server = serverList[index];
@@ -101,10 +108,19 @@ class ServerListScreen extends ConsumerWidget {
108 },
109 onEdit: isOnDevice
110 ? null
104 - : () => _showEditDialog(context, ref, server),
111 + : () => _showEditDialog(
112 + context,
113 + ref,
114 + server,
115 + ),
116 onDelete: isOnDevice
117 ? null
107 - : () => _showDeleteConfirmation(context, ref, l10n, server),
118 + : () => _showDeleteConfirmation(
119 + context,
120 + ref,
121 + l10n,
122 + server,
123 + ),
124 onSetDefault: () {
125 ref
126 .read(serversProvider.notifier)
@@ -140,7 +156,7 @@ class ServerListScreen extends ConsumerWidget {
156 ],
157 ),
158 PositionedDirectional(
143 - bottom: 24,
159 + bottom: 24 + systemBottomInset,
160 end: 24,
161 child: FloatingActionButton(
162 onPressed: () => context.push(AppRoutes.addServer),
@@ -152,7 +168,11 @@ class ServerListScreen extends ConsumerWidget {
168 );
169 }
170
155 - Widget _buildEmptyState(BuildContext context, AppLocalizations l10n, ThemeData theme) {
171 + Widget _buildEmptyState(
172 + BuildContext context,
173 + AppLocalizations l10n,
174 + ThemeData theme,
175 + ) {
176 return Center(
177 child: Padding(
178 padding: const EdgeInsets.all(32),
lib/features/settings/views/settings_screen.dart
+3 -1
@@ -7,6 +7,7 @@ import '../../../core/providers/app_providers.dart';
7 import '../../../core/models/enums.dart';
8 import '../../../core/routes/app_routes.dart';
9 import '../../../core/theme/app_theme.dart';
10 +import '../../../core/utils/system_insets.dart';
11 import '../../conversations/providers/conversation_providers.dart';
12 import '../../on_device/providers/on_device_providers.dart';
13 import '../../servers/providers/server_providers.dart';
@@ -22,12 +23,13 @@ class SettingsViews extends ConsumerWidget {
23 final settings = ref.watch(settingsProvider);
24 final theme = Theme.of(context);
25 final isDark = theme.brightness == Brightness.dark;
26 + final systemBottomInset = bottomSystemInset(context);
27
28 return Scaffold(
29 appBar: AppBar(title: Text(l10n.settings_title)),
30 drawer: SidebarWidget(),
31 body: ListView(
30 - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
32 + padding: EdgeInsets.fromLTRB(16, 8, 16, 8 + systemBottomInset),
33 children: [
34 _SectionHeader(title: l10n.settings_appearance),
35 _ThemeToggle(current: settings.themeMode, ref: ref),