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