12 files changed
+278
-17
README.md
+1
@@ -5,6 +5,7 @@
5
LocalMind is a premium Flutter mobile application designed to provide a beautiful, fast, and privacy-respecting interface for local LLM servers and cloud providers.
6
7

8
+
9
10
## 🚀 Vision
11
docs/app_screenshots.webp
Binary files /dev/null and b/docs/app_screenshots.webp differ
ios/Podfile.lock
+6
@@ -34,6 +34,8 @@ PODS:
34
- DKImagePickerController/PhotoGallery
35
- Flutter
36
- Flutter (1.0.0)
37
+ - flutter_secure_storage (6.0.0):
38
+ - Flutter
39
- flutter_tts (0.0.1):
40
- Flutter
41
- haptic_feedback (0.5.1):
@@ -46,6 +48,7 @@ PODS:
48
DEPENDENCIES:
49
- file_picker (from `.symlinks/plugins/file_picker/ios`)
50
- Flutter (from `Flutter`)
51
+ - flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`)
52
- flutter_tts (from `.symlinks/plugins/flutter_tts/ios`)
53
- haptic_feedback (from `.symlinks/plugins/haptic_feedback/ios`)
54
@@ -61,6 +64,8 @@ EXTERNAL SOURCES:
64
:path: ".symlinks/plugins/file_picker/ios"
65
Flutter:
66
:path: Flutter
67
+ flutter_secure_storage:
68
+ :path: ".symlinks/plugins/flutter_secure_storage/ios"
69
flutter_tts:
70
:path: ".symlinks/plugins/flutter_tts/ios"
71
haptic_feedback:
@@ -71,6 +76,7 @@ SPEC CHECKSUMS:
76
DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60
77
file_picker: b159e0c068aef54932bb15dc9fd1571818edaf49
78
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
79
+ flutter_secure_storage: d33dac7ae2ea08509be337e775f6b59f1ff45f12
80
flutter_tts: 91ad1884033cd95ef6e9338754adad54f01f090f
81
haptic_feedback: a366a27ec760f0e865651605b95de8a9bb04ef01
82
SDWebImage: e9fc87c1aab89a8ab1bbd74eba378c6f53be8abf
lib/core/providers/storage_providers.dart
+3
-6
@@ -20,24 +20,21 @@ final conversationsBoxProvider = Provider<Box<Conversation>>((ref) {
20
return boxes.conversations;
21
});
22
23
-final conversationsProvider = Provider<List<Conversation>>((ref) {
23
+final directBoxConversationsProvider = Provider<List<Conversation>>((ref) {
24
final box = ref.watch(conversationsBoxProvider);
25
final conversations = box.values.toList();
26
conversations.sort((a, b) => b.updatedAt.compareTo(a.updatedAt));
27
return conversations;
28
});
29
30
-final recentConversationsProvider = Provider<List<Conversation>>((ref) {
31
- final all = ref.watch(conversationsProvider);
32
- return all.take(3).toList();
33
-});
30
+
31
32
final personasBoxProvider = Provider<Box<Persona>>((ref) {
33
final boxes = ref.watch(hiveBoxesProvider);
34
return boxes.personas;
35
});
36
40
-final personasProvider = Provider<List<Persona>>((ref) {
37
+final directBoxPersonasProvider = Provider<List<Persona>>((ref) {
38
final box = ref.watch(personasBoxProvider);
39
return box.values.toList();
40
});
lib/core/storage/hive_initializer.dart
+24
-5
@@ -7,6 +7,7 @@ import 'package:localmind/features/personas/data/models/persona.dart';
7
import 'package:localmind/features/settings/data/models/app_settings.dart';
8
import 'package:localmind/core/models/enums.dart';
9
import 'hive_keys.dart';
10
+import 'secure_storage_service.dart';
11
12
class HiveBoxes {
13
final Box<Server> servers;
@@ -41,13 +42,17 @@ class HiveInitializer {
42
Hive.registerAdapter(ThemeModeAdapter());
43
Hive.registerAdapter(SyntaxThemeNameAdapter());
44
44
- final servers = await Hive.openBox<Server>(HiveKeys.servers);
45
- final messages = await Hive.openBox<Message>(HiveKeys.messages);
46
- final conversations = await Hive.openBox<Conversation>(
45
+ final encryptionKey = await SecureStorageService.getOrGenerateKey();
46
+ final cipher = HiveAesCipher(encryptionKey);
47
+
48
+ final servers = await _openEncryptedBox<Server>(HiveKeys.servers, cipher);
49
+ final messages = await _openEncryptedBox<Message>(HiveKeys.messages, cipher);
50
+ final conversations = await _openEncryptedBox<Conversation>(
51
HiveKeys.conversations,
52
+ cipher,
53
);
49
- final personas = await Hive.openBox<Persona>(HiveKeys.personas);
50
- final settings = await Hive.openBox<dynamic>(HiveKeys.settings);
54
+ final personas = await _openEncryptedBox<Persona>(HiveKeys.personas, cipher);
55
+ final settings = await _openEncryptedBox<dynamic>(HiveKeys.settings, cipher);
56
57
return HiveBoxes(
58
servers: servers,
@@ -57,4 +62,18 @@ class HiveInitializer {
62
settings: settings,
63
);
64
}
65
+
66
+ static Future<Box<T>> _openEncryptedBox<T>(
67
+ String name,
68
+ HiveAesCipher cipher,
69
+ ) async {
70
+ try {
71
+ return await Hive.openBox<T>(name, encryptionCipher: cipher);
72
+ } catch (e) {
73
+ // If opening fails (likely due to encryption mismatch),
74
+ // delete the box and start fresh.
75
+ await Hive.deleteBoxFromDisk(name);
76
+ return await Hive.openBox<T>(name, encryptionCipher: cipher);
77
+ }
78
+ }
79
}
lib/core/storage/secure_storage_service.dart
new
+32
@@ -0,0 +1,32 @@
1
+import 'dart:convert';
2
+import 'dart:typed_data';
3
+import 'package:flutter_secure_storage/flutter_secure_storage.dart';
4
+import 'package:hive_ce/hive.dart';
5
+
6
+class SecureStorageService {
7
+ static const _storage = FlutterSecureStorage();
8
+ static const _keyName = 'hive_encryption_key_v1';
9
+
10
+ /// Returns the encryption key as a Uint8List.
11
+ /// If no key exists, it generates a new one and saves it.
12
+ static Future<Uint8List> getOrGenerateKey() async {
13
+ final encryptionKeyString = await _storage.read(key: _keyName);
14
+
15
+ if (encryptionKeyString == null) {
16
+ final key = Hive.generateSecureKey();
17
+ await _storage.write(
18
+ key: _keyName,
19
+ value: base64UrlEncode(key),
20
+ );
21
+ return Uint8List.fromList(key);
22
+ }
23
+
24
+ return base64Url.decode(encryptionKeyString);
25
+ }
26
+
27
+ /// Clears the encryption key.
28
+ /// CAUTION: This will make all existing encrypted data unreadable.
29
+ static Future<void> clearKey() async {
30
+ await _storage.delete(key: _keyName);
31
+ }
32
+}
lib/features/chat/providers/chat_providers.dart
+12
-1
@@ -727,10 +727,21 @@ class ChatNotifier extends Notifier<ChatState> {
727
.renameConversation(_currentConversationId!, title);
728
}
729
730
- void cancelStream() {
730
+ Future<void> cancelStream() async {
731
_streamSubscription?.cancel();
732
_streamSubscription = null;
733
ref.read(chatServiceProvider).cancelStream();
734
+
735
+ final streamingMessage = state.streamingMessage;
736
+ if (streamingMessage != null) {
737
+ final boxes = ref.read(hiveBoxesProvider);
738
+ final finalMessage = streamingMessage.copyWith(
739
+ status: MessageStatus.complete,
740
+ isProcessing: false,
741
+ );
742
+ await boxes.messages.put(finalMessage.id, finalMessage);
743
+ }
744
+
745
state = state.copyWith(isStreaming: false, clearStreaming: true);
746
}
747
lib/features/chat/views/chat_screen.dart
+1
-1
@@ -218,7 +218,7 @@ class _ChatScreenState extends ConsumerState<ChatScreen> {
218
ref.read(chatProvider.notifier).sendMessage(prompt),
219
quickPrompts: _quickPrompts,
220
recentConversations: ref.watch(
221
- recentConversationsProvider,
221
+ conv.recentConversationsProvider,
222
),
223
onSeeAll: () {
224
if (Scaffold.maybeOf(context)?.hasDrawer ?? false) {
lib/features/conversations/providers/conversation_providers.dart
+5
@@ -229,6 +229,11 @@ final filteredConversationsProvider = Provider<List<Conversation>>((ref) {
229
}).toList();
230
});
231
232
+final recentConversationsProvider = Provider<List<Conversation>>((ref) {
233
+ final all = ref.watch(conversationsProvider);
234
+ return all.take(3).toList();
235
+});
236
+
237
final groupedConversationsProvider = Provider<Map<String, List<Conversation>>>((
238
ref,
239
) {
lib/features/settings/screens/settings_screen.dart
+2
-1
@@ -7,6 +7,7 @@ import 'package:localmind/core/theme/app_theme.dart';
7
import 'package:localmind/features/conversations/providers/conversation_providers.dart';
8
import 'package:localmind/features/servers/providers/server_providers.dart';
9
import 'package:localmind/features/settings/data/models/app_settings.dart';
10
+import 'package:localmind/features/personas/providers/personas_providers.dart';
11
12
class SettingsScreen extends ConsumerWidget {
13
const SettingsScreen({super.key});
@@ -161,7 +162,7 @@ class SettingsScreen extends ConsumerWidget {
162
label: 'Default Persona',
163
currentValue: settings.defaultPersonaId,
164
items: ref
164
- .watch(storage.personasProvider)
165
+ .watch(personasNotifierProvider)
166
.map((p) => (p.id, '${p.emoji} ${p.name}'))
167
.toList(),
168
onChanged: (v) =>
pubspec.lock
+186
-2
@@ -121,6 +121,14 @@ packages:
121
url: "https://pub.dev"
122
source: hosted
123
version: "1.4.1"
124
+ charcode:
125
+ dependency: transitive
126
+ description:
127
+ name: charcode
128
+ sha256: fb0f1107cac15a5ea6ef0a6ef71a807b9e4267c713bb93e00e92d737cc8dbd8a
129
+ url: "https://pub.dev"
130
+ source: hosted
131
+ version: "1.4.0"
132
checked_yaml:
133
dependency: transitive
134
description:
@@ -137,6 +145,14 @@ packages:
145
url: "https://pub.dev"
146
source: hosted
147
version: "0.2.0"
148
+ cli_util:
149
+ dependency: transitive
150
+ description:
151
+ name: cli_util
152
+ sha256: ff6785f7e9e3c38ac98b2fb035701789de90154024a75b6cb926445e83197d1c
153
+ url: "https://pub.dev"
154
+ source: hosted
155
+ version: "0.4.2"
156
clock:
157
dependency: transitive
158
description:
@@ -241,6 +257,30 @@ packages:
257
url: "https://pub.dev"
258
source: hosted
259
version: "2.1.2"
260
+ drift:
261
+ dependency: "direct main"
262
+ description:
263
+ name: drift
264
+ sha256: "055c249d1f91be5a47fe447f88afc24c4ca6f4cd6c5ed66767b4797d48acc2e5"
265
+ url: "https://pub.dev"
266
+ source: hosted
267
+ version: "2.32.1"
268
+ drift_dev:
269
+ dependency: "direct dev"
270
+ description:
271
+ name: drift_dev
272
+ sha256: "88a9de3af8571518148a6d8a513b57779fd1e60a026d3ab8a481a878fba01d91"
273
+ url: "https://pub.dev"
274
+ source: hosted
275
+ version: "2.32.1"
276
+ drift_flutter:
277
+ dependency: "direct main"
278
+ description:
279
+ name: drift_flutter
280
+ sha256: "887fdec622174dc7eaefd0048403e34ee07cc18626ac8a7544cc3b8a4a172166"
281
+ url: "https://pub.dev"
282
+ source: hosted
283
+ version: "0.3.0"
284
extended_image:
285
dependency: transitive
286
description:
@@ -347,6 +387,54 @@ packages:
387
url: "https://pub.dev"
388
source: hosted
389
version: "3.3.1"
390
+ flutter_secure_storage:
391
+ dependency: "direct main"
392
+ description:
393
+ name: flutter_secure_storage
394
+ sha256: "9cad52d75ebc511adfae3d447d5d13da15a55a92c9410e50f67335b6d21d16ea"
395
+ url: "https://pub.dev"
396
+ source: hosted
397
+ version: "9.2.4"
398
+ flutter_secure_storage_linux:
399
+ dependency: transitive
400
+ description:
401
+ name: flutter_secure_storage_linux
402
+ sha256: be76c1d24a97d0b98f8b54bce6b481a380a6590df992d0098f868ad54dc8f688
403
+ url: "https://pub.dev"
404
+ source: hosted
405
+ version: "1.2.3"
406
+ flutter_secure_storage_macos:
407
+ dependency: transitive
408
+ description:
409
+ name: flutter_secure_storage_macos
410
+ sha256: "6c0a2795a2d1de26ae202a0d78527d163f4acbb11cde4c75c670f3a0fc064247"
411
+ url: "https://pub.dev"
412
+ source: hosted
413
+ version: "3.1.3"
414
+ flutter_secure_storage_platform_interface:
415
+ dependency: transitive
416
+ description:
417
+ name: flutter_secure_storage_platform_interface
418
+ sha256: cf91ad32ce5adef6fba4d736a542baca9daf3beac4db2d04be350b87f69ac4a8
419
+ url: "https://pub.dev"
420
+ source: hosted
421
+ version: "1.1.2"
422
+ flutter_secure_storage_web:
423
+ dependency: transitive
424
+ description:
425
+ name: flutter_secure_storage_web
426
+ sha256: f4ebff989b4f07b2656fb16b47852c0aab9fed9b4ec1c70103368337bc1886a9
427
+ url: "https://pub.dev"
428
+ source: hosted
429
+ version: "1.2.1"
430
+ flutter_secure_storage_windows:
431
+ dependency: transitive
432
+ description:
433
+ name: flutter_secure_storage_windows
434
+ sha256: b20b07cb5ed4ed74fc567b78a72936203f587eba460af1df11281c9326cd3709
435
+ url: "https://pub.dev"
436
+ source: hosted
437
+ version: "3.1.2"
438
flutter_shaders:
439
dependency: transitive
440
description:
@@ -537,10 +625,10 @@ packages:
625
dependency: transitive
626
description:
627
name: js
540
- sha256: "53385261521cc4a0c4658fd0ad07a7d14591cf8fc33abbceae306ddb974888dc"
628
+ sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3
629
url: "https://pub.dev"
630
source: hosted
543
- version: "0.7.2"
631
+ version: "0.6.7"
632
json_annotation:
633
dependency: transitive
634
description:
@@ -789,6 +877,14 @@ packages:
877
url: "https://pub.dev"
878
source: hosted
879
version: "1.5.0"
880
+ recase:
881
+ dependency: transitive
882
+ description:
883
+ name: recase
884
+ sha256: e4eb4ec2dcdee52dcf99cb4ceabaffc631d7424ee55e56f280bc039737f89213
885
+ url: "https://pub.dev"
886
+ source: hosted
887
+ version: "4.1.0"
888
riverpod:
889
dependency: transitive
890
description:
@@ -813,6 +909,62 @@ packages:
909
url: "https://pub.dev"
910
source: hosted
911
version: "0.53.3"
912
+ shared_preferences:
913
+ dependency: "direct main"
914
+ description:
915
+ name: shared_preferences
916
+ sha256: c3025c5534b01739267eb7d76959bbc25a6d10f6988e1c2a3036940133dd10bf
917
+ url: "https://pub.dev"
918
+ source: hosted
919
+ version: "2.5.5"
920
+ shared_preferences_android:
921
+ dependency: transitive
922
+ description:
923
+ name: shared_preferences_android
924
+ sha256: e8d4762b1e2e8578fc4d0fd548cebf24afd24f49719c08974df92834565e2c53
925
+ url: "https://pub.dev"
926
+ source: hosted
927
+ version: "2.4.23"
928
+ shared_preferences_foundation:
929
+ dependency: transitive
930
+ description:
931
+ name: shared_preferences_foundation
932
+ sha256: "4e7eaffc2b17ba398759f1151415869a34771ba11ebbccd1b0145472a619a64f"
933
+ url: "https://pub.dev"
934
+ source: hosted
935
+ version: "2.5.6"
936
+ shared_preferences_linux:
937
+ dependency: transitive
938
+ description:
939
+ name: shared_preferences_linux
940
+ sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f"
941
+ url: "https://pub.dev"
942
+ source: hosted
943
+ version: "2.4.1"
944
+ shared_preferences_platform_interface:
945
+ dependency: transitive
946
+ description:
947
+ name: shared_preferences_platform_interface
948
+ sha256: "649dc798a33931919ea356c4305c2d1f81619ea6e92244070b520187b5140ef9"
949
+ url: "https://pub.dev"
950
+ source: hosted
951
+ version: "2.4.2"
952
+ shared_preferences_web:
953
+ dependency: transitive
954
+ description:
955
+ name: shared_preferences_web
956
+ sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019
957
+ url: "https://pub.dev"
958
+ source: hosted
959
+ version: "2.4.3"
960
+ shared_preferences_windows:
961
+ dependency: transitive
962
+ description:
963
+ name: shared_preferences_windows
964
+ sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1"
965
+ url: "https://pub.dev"
966
+ source: hosted
967
+ version: "2.4.1"
968
shelf:
969
dependency: transitive
970
description:
@@ -906,6 +1058,38 @@ packages:
1058
url: "https://pub.dev"
1059
source: hosted
1060
version: "1.10.2"
1061
+ sqlcipher_flutter_libs:
1062
+ dependency: transitive
1063
+ description:
1064
+ name: sqlcipher_flutter_libs
1065
+ sha256: "38d62d659d2fb8739bf25a42c9a350d1fdd6c29a5a61f13a946778ec75d27929"
1066
+ url: "https://pub.dev"
1067
+ source: hosted
1068
+ version: "0.7.0+eol"
1069
+ sqlite3:
1070
+ dependency: transitive
1071
+ description:
1072
+ name: sqlite3
1073
+ sha256: b3fe250b4ebd681a8ba20b12794c42b00c1375e5b51005568f0b8731265beb03
1074
+ url: "https://pub.dev"
1075
+ source: hosted
1076
+ version: "3.3.0"
1077
+ sqlite3_flutter_libs:
1078
+ dependency: transitive
1079
+ description:
1080
+ name: sqlite3_flutter_libs
1081
+ sha256: "3ed7553eee7bb368f8950f58ba29f634e06e813c029aff6a0d60862b96de8454"
1082
+ url: "https://pub.dev"
1083
+ source: hosted
1084
+ version: "0.6.0+eol"
1085
+ sqlparser:
1086
+ dependency: transitive
1087
+ description:
1088
+ name: sqlparser
1089
+ sha256: ab2b467425f1d4f3acfa5fd11a08226f7d6c26ff102c06be1807e1dff34e050b
1090
+ url: "https://pub.dev"
1091
+ source: hosted
1092
+ version: "0.44.3"
1093
stack_trace:
1094
dependency: transitive
1095
description:
pubspec.yaml
+6
-1
@@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
16
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
17
# In Windows, build-name is used as the major, minor, and patch parts
18
# of the product and file versions while build-number is used as the build suffix.
19
-version: 1.0.0+5
19
+version: 1.0.0+6
20
21
environment:
22
sdk: ^3.11.0
@@ -40,6 +40,7 @@ dependencies:
40
hive_ce_flutter: ^2.3.4
41
shadcn_ui: ^0.53.3
42
dio: ^5.9.2
43
+ flutter_secure_storage: ^9.2.3
44
flutter_markdown_plus: ^1.0.7
45
flutter_tts: ^4.2.5
46
file_picker: ^10.3.10
@@ -50,6 +51,9 @@ dependencies:
51
flutter_syntax_view: 4.1.7
52
animations: 2.1.2
53
logger: ^2.7.0
54
+ drift: ^2.32.1
55
+ drift_flutter: ^0.3.0
56
+ shared_preferences: ^2.5.5
57
58
dev_dependencies:
59
flutter_test:
@@ -64,6 +68,7 @@ dev_dependencies:
68
build_runner: ^2.13.1
69
hive_ce_generator: ^1.11.1
70
change_app_package_name: ^1.4.0
71
+ drift_dev: ^2.32.1
72
73
# For information on the generic Dart part of this file, see the
74
# following page: https://dart.dev/tools/pub/pubspec