Refactor toward feature-based architecture for backup and search.
Extract backup import helpers and message search service into dedicated modules, and move persona picker UI into the personas feature. Co-authored-by: Cursor <cursoragent@cursor.com>
onlypuppy7 committed
Jul 1, 2026 at 10:42 UTC
dcba5a4767a32f6bcd98c4a7b549dee7456c7438
7 files changed
+118
-88
lib/core/services/data_backup_import_helpers.dart
new
+11
@@ -0,0 +1,11 @@
1
+String? backupImportString(Object? value) => value is String ? value : null;
2
+
3
+DateTime? backupImportDateTime(Object? value) {
4
+ final raw = backupImportString(value);
5
+ if (raw == null) return null;
6
+ try {
7
+ return DateTime.parse(raw);
8
+ } catch (_) {
9
+ return null;
10
+ }
11
+}
lib/core/services/data_backup_service.dart
+22
-33
@@ -5,6 +5,7 @@ import 'package:shared_preferences/shared_preferences.dart';
5
6
import '../models/enums.dart';
7
import '../storage/entities.dart';
8
+import 'data_backup_import_helpers.dart';
9
import '../../features/chat/data/models/message.dart';
10
import '../../features/conversations/data/models/conversation.dart';
11
import '../../features/personas/data/models/persona.dart';
@@ -310,10 +311,10 @@ class DataBackupService {
311
if (personas is List) {
312
for (final item in personas) {
313
if (item is! Map) continue;
313
- final id = _backupString(item['id']);
314
- final name = _backupString(item['name']);
315
- final createdAt = _backupDateTime(item['createdAt']);
316
- final updatedAt = _backupDateTime(item['updatedAt']);
314
+ final id = backupImportString(item['id']);
315
+ final name = backupImportString(item['name']);
316
+ final createdAt = backupImportDateTime(item['createdAt']);
317
+ final updatedAt = backupImportDateTime(item['updatedAt']);
318
if (id == null || name == null || createdAt == null || updatedAt == null) {
319
continue;
320
}
@@ -345,10 +346,10 @@ class DataBackupService {
346
if (servers is List) {
347
for (final item in servers) {
348
if (item is! Map) continue;
348
- final id = _backupString(item['id']);
349
- final name = _backupString(item['name']);
350
- final createdAt = _backupDateTime(item['createdAt']);
351
- final lastConnectedAt = _backupDateTime(item['lastConnectedAt']);
349
+ final id = backupImportString(item['id']);
350
+ final name = backupImportString(item['name']);
351
+ final createdAt = backupImportDateTime(item['createdAt']);
352
+ final lastConnectedAt = backupImportDateTime(item['lastConnectedAt']);
353
if (id == null ||
354
name == null ||
355
createdAt == null ||
@@ -384,9 +385,9 @@ class DataBackupService {
385
if (savedFolders is List) {
386
for (final item in savedFolders) {
387
if (item is! Map) continue;
387
- final id = _backupString(item['id']);
388
- final folderName = _backupString(item['name']);
389
- final createdAt = _backupDateTime(item['createdAt']);
388
+ final id = backupImportString(item['id']);
389
+ final folderName = backupImportString(item['name']);
390
+ final createdAt = backupImportDateTime(item['createdAt']);
391
if (id == null || folderName == null || createdAt == null) continue;
392
final entity = SavedMessageFolderEntity(
393
id: id,
@@ -408,10 +409,10 @@ class DataBackupService {
409
if (savedMessages is List) {
410
for (final item in savedMessages) {
411
if (item is! Map) continue;
411
- final id = _backupString(item['id']);
412
- final sourceMessageId = _backupString(item['sourceMessageId']);
413
- final conversationId = _backupString(item['conversationId']);
414
- final savedAt = _backupDateTime(item['savedAt']);
412
+ final id = backupImportString(item['id']);
413
+ final sourceMessageId = backupImportString(item['sourceMessageId']);
414
+ final conversationId = backupImportString(item['conversationId']);
415
+ final savedAt = backupImportDateTime(item['savedAt']);
416
if (id == null ||
417
sourceMessageId == null ||
418
conversationId == null ||
@@ -442,9 +443,9 @@ class DataBackupService {
443
if (conversations is List) {
444
for (final item in conversations) {
445
if (item is! Map) continue;
445
- final id = _backupString(item['id']);
446
- final createdAt = _backupDateTime(item['createdAt']);
447
- final updatedAt = _backupDateTime(item['updatedAt']);
446
+ final id = backupImportString(item['id']);
447
+ final createdAt = backupImportDateTime(item['createdAt']);
448
+ final updatedAt = backupImportDateTime(item['updatedAt']);
449
if (id == null || createdAt == null || updatedAt == null) continue;
450
final conversation = Conversation(
451
id: id,
@@ -484,9 +485,9 @@ class DataBackupService {
485
if (messages is List) {
486
for (final item in messages) {
487
if (item is! Map) continue;
487
- final conversationId = _backupString(item['conversationId']);
488
- final id = _backupString(item['id']);
489
- final createdAt = _backupDateTime(item['createdAt']);
488
+ final conversationId = backupImportString(item['conversationId']);
489
+ final id = backupImportString(item['id']);
490
+ final createdAt = backupImportDateTime(item['createdAt']);
491
if (conversationId == null || id == null || createdAt == null) {
492
continue;
493
}
@@ -545,15 +546,3 @@ class DataBackupService {
546
}
547
}
548
}
548
-
549
-String? _backupString(Object? value) => value is String ? value : null;
550
-
551
-DateTime? _backupDateTime(Object? value) {
552
- final raw = _backupString(value);
553
- if (raw == null) return null;
554
- try {
555
- return DateTime.parse(raw);
556
- } catch (_) {
557
- return null;
558
- }
559
-}
lib/features/chat/views/chat_screen.dart
+1
-1
@@ -41,7 +41,7 @@ import 'components/top_bar/model_top_bar.dart';
41
import 'components/top_bar/connection_banner.dart';
42
import 'components/top_bar/persona_indicator.dart';
43
import 'components/top_bar/smart_reply_chips.dart';
44
-import 'components/persona_picker_sheet.dart';
44
+import 'package:localmind/features/personas/views/components/persona_picker_sheet.dart';
45
46
class ChatScreen extends ConsumerStatefulWidget {
47
const ChatScreen({super.key});
lib/features/conversations/data/message_search_service.dart
new
+60
@@ -0,0 +1,60 @@
1
+import '../../../core/models/enums.dart';
2
+import '../../../core/storage/objectbox_store.dart';
3
+import '../../../objectbox.g.dart';
4
+import 'models/message_search_hit.dart';
5
+
6
+class MessageSearchService {
7
+ const MessageSearchService();
8
+
9
+ List<MessageSearchHit> searchMessages(
10
+ ObjectBoxStore db, {
11
+ required String query,
12
+ int limit = 50,
13
+ }) {
14
+ final normalizedQuery = query.trim().toLowerCase();
15
+ if (normalizedQuery.isEmpty) return const [];
16
+
17
+ final conversations = db.conversationBox.getAll();
18
+ final titleById = {
19
+ for (final conversation in conversations)
20
+ conversation.id: conversation.title,
21
+ };
22
+
23
+ final hits = <MessageSearchHit>[];
24
+ final messageQuery = db.messageBox
25
+ .query(
26
+ MessageEntity_.content.contains(
27
+ normalizedQuery,
28
+ caseSensitive: false,
29
+ ),
30
+ )
31
+ .build();
32
+ final messages = messageQuery.find();
33
+ messageQuery.close();
34
+
35
+ for (final entity in messages) {
36
+ if (!entity.content.toLowerCase().contains(normalizedQuery)) continue;
37
+
38
+ final title = titleById[entity.conversationUid] ?? 'Chat';
39
+ final content = entity.content;
40
+ final index = content.toLowerCase().indexOf(normalizedQuery);
41
+ final start = index > 40 ? index - 40 : 0;
42
+ final end = (index + normalizedQuery.length + 40).clamp(0, content.length);
43
+ var snippet = content.substring(start, end);
44
+ if (start > 0) snippet = '…$snippet';
45
+ if (end < content.length) snippet = '$snippet…';
46
+
47
+ hits.add(
48
+ MessageSearchHit(
49
+ messageId: entity.id,
50
+ conversationId: entity.conversationUid,
51
+ conversationTitle: title,
52
+ snippet: snippet,
53
+ role: MessageRole.values[entity.roleIndex],
54
+ ),
55
+ );
56
+ if (hits.length >= limit) break;
57
+ }
58
+ return hits;
59
+ }
60
+}
lib/features/conversations/data/models/message_search_hit.dart
new
+17
@@ -0,0 +1,17 @@
1
+import '../../../../core/models/enums.dart';
2
+
3
+class MessageSearchHit {
4
+ const MessageSearchHit({
5
+ required this.messageId,
6
+ required this.conversationId,
7
+ required this.conversationTitle,
8
+ required this.snippet,
9
+ required this.role,
10
+ });
11
+
12
+ final String messageId;
13
+ final String conversationId;
14
+ final String conversationTitle;
15
+ final String snippet;
16
+ final MessageRole role;
17
+}
lib/features/conversations/providers/conversation_providers.dart
+7
-54
@@ -1,9 +1,10 @@
1
import 'package:flutter_riverpod/flutter_riverpod.dart';
2
3
-import '../../../core/models/enums.dart';
3
import '../../../core/providers/storage_providers.dart';
4
import '../../../core/storage/entities.dart';
5
import '../../../objectbox.g.dart';
6
+import '../data/message_search_service.dart';
7
+import '../data/models/message_search_hit.dart';
8
import '../data/models/conversation.dart';
9
import '../data/models/conversation_folder.dart';
10
@@ -715,22 +716,6 @@ class ConversationFoldersNotifier extends AsyncNotifier<List<ConversationFolder>
716
}
717
}
718
718
-class MessageSearchHit {
719
- const MessageSearchHit({
720
- required this.messageId,
721
- required this.conversationId,
722
- required this.conversationTitle,
723
- required this.snippet,
724
- required this.role,
725
- });
726
-
727
- final String messageId;
728
- final String conversationId;
729
- final String conversationTitle;
730
- final String snippet;
731
- final MessageRole role;
732
-}
733
-
719
final searchMessageContentsProvider =
720
NotifierProvider<SearchMessageContentsNotifier, bool>(() {
721
return SearchMessageContentsNotifier();
@@ -773,46 +758,14 @@ class ScrollToMessageNotifier extends Notifier<String?> {
758
void clear() => state = null;
759
}
760
761
+const _messageSearchService = MessageSearchService();
762
+
763
final messageSearchResultsProvider = Provider<List<MessageSearchHit>>((ref) {
777
- final query = ref.watch(conversationSearchProvider).trim().toLowerCase();
778
- if (query.isEmpty || !ref.watch(searchMessageContentsProvider)) {
764
+ final query = ref.watch(conversationSearchProvider);
765
+ if (query.trim().isEmpty || !ref.watch(searchMessageContentsProvider)) {
766
return const [];
767
}
768
769
final db = ref.watch(databaseProvider);
783
- final conversations = db.conversationBox.getAll();
784
- final titleById = {
785
- for (final conv in conversations) conv.id: conv.title,
786
- };
787
-
788
- final hits = <MessageSearchHit>[];
789
- final messageQuery = db.messageBox
790
- .query(MessageEntity_.content.contains(query, caseSensitive: false))
791
- .build();
792
- final messages = messageQuery.find();
793
- messageQuery.close();
794
-
795
- for (final entity in messages) {
796
- if (!entity.content.toLowerCase().contains(query)) continue;
797
- final title = titleById[entity.conversationUid] ?? 'Chat';
798
- final content = entity.content;
799
- final index = content.toLowerCase().indexOf(query);
800
- final start = index > 40 ? index - 40 : 0;
801
- final end = (index + query.length + 40).clamp(0, content.length);
802
- var snippet = content.substring(start, end);
803
- if (start > 0) snippet = '…$snippet';
804
- if (end < content.length) snippet = '$snippet…';
805
-
806
- hits.add(
807
- MessageSearchHit(
808
- messageId: entity.id,
809
- conversationId: entity.conversationUid,
810
- conversationTitle: title,
811
- snippet: snippet,
812
- role: MessageRole.values[entity.roleIndex],
813
- ),
814
- );
815
- if (hits.length >= 50) break;
816
- }
817
- return hits;
770
+ return _messageSearchService.searchMessages(db, query: query);
771
});