66 files changed
+3289
-1880
ios/Podfile
+1
-1
@@ -1,5 +1,5 @@
1
# Uncomment this line to define a global platform for your project
2
-# platform :ios, '13.0'
2
+platform :ios, '15.0'
3
4
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
5
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
ios/Podfile.lock
+18
-1
@@ -40,9 +40,16 @@ PODS:
40
- Flutter
41
- haptic_feedback (0.5.1):
42
- Flutter
43
+ - ObjectBox (5.3.0-beta.4)
44
+ - objectbox_flutter_libs (0.0.1):
45
+ - Flutter
46
+ - ObjectBox (= 5.3.0-beta.4)
47
- SDWebImage (5.21.7):
48
- SDWebImage/Core (= 5.21.7)
49
- SDWebImage/Core (5.21.7)
50
+ - shared_preferences_foundation (0.0.1):
51
+ - Flutter
52
+ - FlutterMacOS
53
- SwiftyGif (5.4.5)
54
55
DEPENDENCIES:
@@ -51,11 +58,14 @@ DEPENDENCIES:
58
- flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`)
59
- flutter_tts (from `.symlinks/plugins/flutter_tts/ios`)
60
- haptic_feedback (from `.symlinks/plugins/haptic_feedback/ios`)
61
+ - objectbox_flutter_libs (from `.symlinks/plugins/objectbox_flutter_libs/ios`)
62
+ - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
63
64
SPEC REPOS:
65
trunk:
66
- DKImagePickerController
67
- DKPhotoGallery
68
+ - ObjectBox
69
- SDWebImage
70
- SwiftyGif
71
@@ -70,6 +80,10 @@ EXTERNAL SOURCES:
80
:path: ".symlinks/plugins/flutter_tts/ios"
81
haptic_feedback:
82
:path: ".symlinks/plugins/haptic_feedback/ios"
83
+ objectbox_flutter_libs:
84
+ :path: ".symlinks/plugins/objectbox_flutter_libs/ios"
85
+ shared_preferences_foundation:
86
+ :path: ".symlinks/plugins/shared_preferences_foundation/darwin"
87
88
SPEC CHECKSUMS:
89
DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c
@@ -79,9 +93,12 @@ SPEC CHECKSUMS:
93
flutter_secure_storage: d33dac7ae2ea08509be337e775f6b59f1ff45f12
94
flutter_tts: 91ad1884033cd95ef6e9338754adad54f01f090f
95
haptic_feedback: a366a27ec760f0e865651605b95de8a9bb04ef01
96
+ ObjectBox: eccb95ea2054c39d81dfa2d4ccc5f1e31187228a
97
+ objectbox_flutter_libs: 893a4322e8ec87438912bae6dfa745929adc6bf5
98
SDWebImage: e9fc87c1aab89a8ab1bbd74eba378c6f53be8abf
99
+ shared_preferences_foundation: 5086985c1d43c5ba4d5e69a4e8083a389e2909e6
100
SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4
101
85
-PODFILE CHECKSUM: 3c63482e143d1b91d2d2560aee9fb04ecc74ac7e
102
+PODFILE CHECKSUM: 53a6aebc29ccee84c41f92f409fc20cd4ca011f1
103
104
COCOAPODS: 1.16.2
ios/Runner.xcodeproj/project.pbxproj
+61
-61
@@ -10,13 +10,13 @@
10
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
11
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
12
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
13
- 708E3CFC8892B5DE565829CF /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A429165A8A305E5EA5D3366F /* Pods_RunnerTests.framework */; };
13
+ 593F905FB75129A384E40AED /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2F6AFCBB8B5168921CF0CF27 /* Pods_Runner.framework */; };
14
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
15
7884E8682EC3CC0700C636F2 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */; };
16
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
17
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
18
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
19
- D6C3B421FD01BF16D2875FF4 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 68B72B458011EED590BB10A7 /* Pods_Runner.framework */; };
19
+ E426CC5FB0C84B59EA4C9455 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F3F7E264AC2540C04D71A2E0 /* Pods_RunnerTests.framework */; };
20
/* End PBXBuildFile section */
21
22
/* Begin PBXContainerItemProxy section */
@@ -45,17 +45,14 @@
45
/* Begin PBXFileReference section */
46
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
47
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
48
+ 1E28B77AE84C180CE237C2CF /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = "<group>"; };
49
+ 2910D1CEAE7EC5D481C55D03 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = "<group>"; };
50
+ 2F6AFCBB8B5168921CF0CF27 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
51
331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
52
331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
53
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
51
- 4453DDD0EC8DAE1A94449906 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
52
- 49416490449ABCCBD5CB71F5 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
53
- 546EC27D20F12194C4550A61 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = "<group>"; };
54
- 68B72B458011EED590BB10A7 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
55
- 73343CA27827EBEDA45E3AE4 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
54
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
55
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
58
- 764D6FCF354D4C047C088B4E /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = "<group>"; };
56
7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = "<group>"; };
57
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
58
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
@@ -65,8 +62,11 @@
62
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
63
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
64
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
68
- A429165A8A305E5EA5D3366F /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
69
- E4C8076B210B690E52E1A9BD /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = "<group>"; };
65
+ AA0C333EA96851D712658FE5 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = "<group>"; };
66
+ AE6E886B14A635226D09D311 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
67
+ C7571B6B9D4A156690DB988C /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
68
+ F0DD11FD888D55FAA2DAFD54 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
69
+ F3F7E264AC2540C04D71A2E0 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
70
/* End PBXFileReference section */
71
72
/* Begin PBXFrameworksBuildPhase section */
@@ -74,7 +74,7 @@
74
isa = PBXFrameworksBuildPhase;
75
buildActionMask = 2147483647;
76
files = (
77
- D6C3B421FD01BF16D2875FF4 /* Pods_Runner.framework in Frameworks */,
77
+ 593F905FB75129A384E40AED /* Pods_Runner.framework in Frameworks */,
78
);
79
runOnlyForDeploymentPostprocessing = 0;
80
};
@@ -82,7 +82,7 @@
82
isa = PBXFrameworksBuildPhase;
83
buildActionMask = 2147483647;
84
files = (
85
- 708E3CFC8892B5DE565829CF /* Pods_RunnerTests.framework in Frameworks */,
85
+ E426CC5FB0C84B59EA4C9455 /* Pods_RunnerTests.framework in Frameworks */,
86
);
87
runOnlyForDeploymentPostprocessing = 0;
88
};
@@ -97,20 +97,6 @@
97
path = RunnerTests;
98
sourceTree = "<group>";
99
};
100
- 3DD528BE2C5F86FFDE137E7C /* Pods */ = {
101
- isa = PBXGroup;
102
- children = (
103
- 49416490449ABCCBD5CB71F5 /* Pods-Runner.debug.xcconfig */,
104
- 73343CA27827EBEDA45E3AE4 /* Pods-Runner.release.xcconfig */,
105
- 4453DDD0EC8DAE1A94449906 /* Pods-Runner.profile.xcconfig */,
106
- 764D6FCF354D4C047C088B4E /* Pods-RunnerTests.debug.xcconfig */,
107
- 546EC27D20F12194C4550A61 /* Pods-RunnerTests.release.xcconfig */,
108
- E4C8076B210B690E52E1A9BD /* Pods-RunnerTests.profile.xcconfig */,
109
- );
110
- name = Pods;
111
- path = Pods;
112
- sourceTree = "<group>";
113
- };
100
9740EEB11CF90186004384FC /* Flutter */ = {
101
isa = PBXGroup;
102
children = (
@@ -129,8 +115,8 @@
115
97C146F01CF9000F007C117D /* Runner */,
116
97C146EF1CF9000F007C117D /* Products */,
117
331C8082294A63A400263BE5 /* RunnerTests */,
132
- 3DD528BE2C5F86FFDE137E7C /* Pods */,
133
- C5595798F4C168CEA6EC3592 /* Frameworks */,
118
+ B5F0F4932A555741EAC185FD /* Pods */,
119
+ C9B81A28CEFDE833459BBAE4 /* Frameworks */,
120
);
121
sourceTree = "<group>";
122
};
@@ -159,11 +145,25 @@
145
path = Runner;
146
sourceTree = "<group>";
147
};
162
- C5595798F4C168CEA6EC3592 /* Frameworks */ = {
148
+ B5F0F4932A555741EAC185FD /* Pods */ = {
149
+ isa = PBXGroup;
150
+ children = (
151
+ F0DD11FD888D55FAA2DAFD54 /* Pods-Runner.debug.xcconfig */,
152
+ C7571B6B9D4A156690DB988C /* Pods-Runner.release.xcconfig */,
153
+ AE6E886B14A635226D09D311 /* Pods-Runner.profile.xcconfig */,
154
+ 1E28B77AE84C180CE237C2CF /* Pods-RunnerTests.debug.xcconfig */,
155
+ AA0C333EA96851D712658FE5 /* Pods-RunnerTests.release.xcconfig */,
156
+ 2910D1CEAE7EC5D481C55D03 /* Pods-RunnerTests.profile.xcconfig */,
157
+ );
158
+ name = Pods;
159
+ path = Pods;
160
+ sourceTree = "<group>";
161
+ };
162
+ C9B81A28CEFDE833459BBAE4 /* Frameworks */ = {
163
isa = PBXGroup;
164
children = (
165
- 68B72B458011EED590BB10A7 /* Pods_Runner.framework */,
166
- A429165A8A305E5EA5D3366F /* Pods_RunnerTests.framework */,
165
+ 2F6AFCBB8B5168921CF0CF27 /* Pods_Runner.framework */,
166
+ F3F7E264AC2540C04D71A2E0 /* Pods_RunnerTests.framework */,
167
);
168
name = Frameworks;
169
sourceTree = "<group>";
@@ -175,7 +175,7 @@
175
isa = PBXNativeTarget;
176
buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
177
buildPhases = (
178
- 6CE0A9A100FDD12D7D51A4C9 /* [CP] Check Pods Manifest.lock */,
178
+ E66B1B93294383A7AD82BEE0 /* [CP] Check Pods Manifest.lock */,
179
331C807D294A63A400263BE5 /* Sources */,
180
331C807F294A63A400263BE5 /* Resources */,
181
9AA24F4969F6FB2D761614D7 /* Frameworks */,
@@ -194,14 +194,14 @@
194
isa = PBXNativeTarget;
195
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
196
buildPhases = (
197
- 9D758D28133E372A39BC3817 /* [CP] Check Pods Manifest.lock */,
197
+ AF1553571D404C2B52FA3EE8 /* [CP] Check Pods Manifest.lock */,
198
9740EEB61CF901F6004384FC /* Run Script */,
199
97C146EA1CF9000F007C117D /* Sources */,
200
97C146EB1CF9000F007C117D /* Frameworks */,
201
97C146EC1CF9000F007C117D /* Resources */,
202
9705A1C41CF9048500538489 /* Embed Frameworks */,
203
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
204
- 9659998E99141CEDEBCD85F9 /* [CP] Embed Pods Frameworks */,
204
+ 6F568F0CE167E332F9CAF0D6 /* [CP] Embed Pods Frameworks */,
205
);
206
buildRules = (
207
);
@@ -289,29 +289,7 @@
289
shellPath = /bin/sh;
290
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
291
};
292
- 6CE0A9A100FDD12D7D51A4C9 /* [CP] Check Pods Manifest.lock */ = {
293
- isa = PBXShellScriptBuildPhase;
294
- buildActionMask = 2147483647;
295
- files = (
296
- );
297
- inputFileListPaths = (
298
- );
299
- inputPaths = (
300
- "${PODS_PODFILE_DIR_PATH}/Podfile.lock",
301
- "${PODS_ROOT}/Manifest.lock",
302
- );
303
- name = "[CP] Check Pods Manifest.lock";
304
- outputFileListPaths = (
305
- );
306
- outputPaths = (
307
- "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt",
308
- );
309
- runOnlyForDeploymentPostprocessing = 0;
310
- shellPath = /bin/sh;
311
- shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
312
- showEnvVarsInLog = 0;
313
- };
314
- 9659998E99141CEDEBCD85F9 /* [CP] Embed Pods Frameworks */ = {
292
+ 6F568F0CE167E332F9CAF0D6 /* [CP] Embed Pods Frameworks */ = {
293
isa = PBXShellScriptBuildPhase;
294
buildActionMask = 2147483647;
295
files = (
@@ -343,7 +321,7 @@
321
shellPath = /bin/sh;
322
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
323
};
346
- 9D758D28133E372A39BC3817 /* [CP] Check Pods Manifest.lock */ = {
324
+ AF1553571D404C2B52FA3EE8 /* [CP] Check Pods Manifest.lock */ = {
325
isa = PBXShellScriptBuildPhase;
326
buildActionMask = 2147483647;
327
files = (
@@ -365,6 +343,28 @@
343
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
344
showEnvVarsInLog = 0;
345
};
346
+ E66B1B93294383A7AD82BEE0 /* [CP] Check Pods Manifest.lock */ = {
347
+ isa = PBXShellScriptBuildPhase;
348
+ buildActionMask = 2147483647;
349
+ files = (
350
+ );
351
+ inputFileListPaths = (
352
+ );
353
+ inputPaths = (
354
+ "${PODS_PODFILE_DIR_PATH}/Podfile.lock",
355
+ "${PODS_ROOT}/Manifest.lock",
356
+ );
357
+ name = "[CP] Check Pods Manifest.lock";
358
+ outputFileListPaths = (
359
+ );
360
+ outputPaths = (
361
+ "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt",
362
+ );
363
+ runOnlyForDeploymentPostprocessing = 0;
364
+ shellPath = /bin/sh;
365
+ shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
366
+ showEnvVarsInLog = 0;
367
+ };
368
/* End PBXShellScriptBuildPhase section */
369
370
/* Begin PBXSourcesBuildPhase section */
@@ -492,7 +492,7 @@
492
};
493
331C8088294A63A400263BE5 /* Debug */ = {
494
isa = XCBuildConfiguration;
495
- baseConfigurationReference = 764D6FCF354D4C047C088B4E /* Pods-RunnerTests.debug.xcconfig */;
495
+ baseConfigurationReference = 1E28B77AE84C180CE237C2CF /* Pods-RunnerTests.debug.xcconfig */;
496
buildSettings = {
497
BUNDLE_LOADER = "$(TEST_HOST)";
498
CODE_SIGN_STYLE = Automatic;
@@ -510,7 +510,7 @@
510
};
511
331C8089294A63A400263BE5 /* Release */ = {
512
isa = XCBuildConfiguration;
513
- baseConfigurationReference = 546EC27D20F12194C4550A61 /* Pods-RunnerTests.release.xcconfig */;
513
+ baseConfigurationReference = AA0C333EA96851D712658FE5 /* Pods-RunnerTests.release.xcconfig */;
514
buildSettings = {
515
BUNDLE_LOADER = "$(TEST_HOST)";
516
CODE_SIGN_STYLE = Automatic;
@@ -526,7 +526,7 @@
526
};
527
331C808A294A63A400263BE5 /* Profile */ = {
528
isa = XCBuildConfiguration;
529
- baseConfigurationReference = E4C8076B210B690E52E1A9BD /* Pods-RunnerTests.profile.xcconfig */;
529
+ baseConfigurationReference = 2910D1CEAE7EC5D481C55D03 /* Pods-RunnerTests.profile.xcconfig */;
530
buildSettings = {
531
BUNDLE_LOADER = "$(TEST_HOST)";
532
CODE_SIGN_STYLE = Automatic;
lib/app.dart
+18
-17
@@ -2,23 +2,24 @@ import 'package:flutter/material.dart';
2
import 'package:flutter_riverpod/flutter_riverpod.dart';
3
import 'package:go_router/go_router.dart';
4
import 'package:shadcn_ui/shadcn_ui.dart';
5
-import 'package:localmind/core/theme/app_theme.dart';
6
-import 'package:localmind/core/providers/app_providers.dart';
7
-import 'package:localmind/features/servers/data/models/server.dart';
8
-import 'package:localmind/features/chat/views/chat_screen.dart';
9
-import 'package:localmind/features/sidebar/sidebar_widget.dart';
10
-import 'package:localmind/features/sidebar/sidebar_drawer.dart';
11
-import 'package:localmind/features/servers/views/server_list_screen.dart';
12
-import 'package:localmind/features/servers/views/add_server_screen.dart';
13
-import 'package:localmind/features/personas/views/persona_list_screen.dart';
14
-import 'package:localmind/features/personas/views/create_persona_screen.dart';
15
-import 'package:localmind/features/settings/screens/settings_screen.dart';
16
-import 'package:localmind/features/conversations/views/chat_history_screen.dart';
17
-import 'package:localmind/features/onboarding/screens/onboarding_server_type_screen.dart';
18
-import 'package:localmind/features/onboarding/screens/onboarding_server_setup_screen.dart';
19
-import 'package:localmind/features/onboarding/screens/onboarding_theme_screen.dart';
20
-import 'package:localmind/core/routes/app_routes.dart';
21
-import 'package:localmind/core/models/enums.dart';
5
+
6
+import 'core/models/enums.dart';
7
+import 'core/providers/app_providers.dart';
8
+import 'core/routes/app_routes.dart';
9
+import 'core/theme/app_theme.dart';
10
+import 'features/chat/views/chat_screen.dart';
11
+import 'features/conversations/views/chat_history_screen.dart';
12
+import 'features/onboarding/screens/onboarding_server_setup_screen.dart';
13
+import 'features/onboarding/screens/onboarding_server_type_screen.dart';
14
+import 'features/onboarding/screens/onboarding_theme_screen.dart';
15
+import 'features/personas/views/create_persona_screen.dart';
16
+import 'features/personas/views/persona_list_screen.dart';
17
+import 'features/servers/data/models/server.dart';
18
+import 'features/servers/views/add_server_screen.dart';
19
+import 'features/servers/views/server_list_screen.dart';
20
+import 'features/settings/screens/settings_screen.dart';
21
+import 'features/sidebar/sidebar_drawer.dart';
22
+import 'features/sidebar/sidebar_widget.dart';
23
24
final _rootNavigatorKey = GlobalKey<NavigatorState>();
25
final _shellNavigatorKey = GlobalKey<NavigatorState>();
lib/core/models/enums.dart
+1
-92
@@ -1,5 +1,4 @@
1
-import 'package:flutter/material.dart';
2
-import 'package:hive_ce/hive.dart';
1
+
2
3
enum ServerType { lmStudio, openAICompatible, ollama, openRouter }
4
@@ -10,93 +9,3 @@ enum MessageRole { user, assistant, system, tool }
9
enum MessageStatus { sending, streaming, complete, error }
10
11
enum ModelStatus { unloaded, loading, loaded, preloaded, thinking }
13
-
14
-class ServerTypeAdapter extends TypeAdapter<ServerType> {
15
- @override
16
- final int typeId = 10;
17
-
18
- @override
19
- ServerType read(BinaryReader reader) {
20
- return ServerType.values[reader.readInt()];
21
- }
22
-
23
- @override
24
- void write(BinaryWriter writer, ServerType obj) {
25
- writer.writeInt(obj.index);
26
- }
27
-}
28
-
29
-class ConnectionStatusAdapter extends TypeAdapter<ConnectionStatus> {
30
- @override
31
- final int typeId = 11;
32
-
33
- @override
34
- ConnectionStatus read(BinaryReader reader) {
35
- return ConnectionStatus.values[reader.readInt()];
36
- }
37
-
38
- @override
39
- void write(BinaryWriter writer, ConnectionStatus obj) {
40
- writer.writeInt(obj.index);
41
- }
42
-}
43
-
44
-class MessageRoleAdapter extends TypeAdapter<MessageRole> {
45
- @override
46
- final int typeId = 12;
47
-
48
- @override
49
- MessageRole read(BinaryReader reader) {
50
- return MessageRole.values[reader.readInt()];
51
- }
52
-
53
- @override
54
- void write(BinaryWriter writer, MessageRole obj) {
55
- writer.writeInt(obj.index);
56
- }
57
-}
58
-
59
-class MessageStatusAdapter extends TypeAdapter<MessageStatus> {
60
- @override
61
- final int typeId = 13;
62
-
63
- @override
64
- MessageStatus read(BinaryReader reader) {
65
- return MessageStatus.values[reader.readInt()];
66
- }
67
-
68
- @override
69
- void write(BinaryWriter writer, MessageStatus obj) {
70
- writer.writeInt(obj.index);
71
- }
72
-}
73
-
74
-class ModelStatusAdapter extends TypeAdapter<ModelStatus> {
75
- @override
76
- final int typeId = 15;
77
-
78
- @override
79
- ModelStatus read(BinaryReader reader) {
80
- return ModelStatus.values[reader.readInt()];
81
- }
82
-
83
- @override
84
- void write(BinaryWriter writer, ModelStatus obj) {
85
- writer.writeInt(obj.index);
86
- }
87
-}
88
-
89
-class ThemeModeAdapter extends TypeAdapter<ThemeMode> {
90
- @override
91
- final int typeId = 14;
92
-
93
- @override
94
- ThemeMode read(BinaryReader reader) {
95
- return ThemeMode.values[reader.readInt()];
96
- }
97
-
98
- @override
99
- void write(BinaryWriter writer, ThemeMode obj) {
100
- writer.writeInt(obj.index);
101
- }
102
-}
lib/core/providers/app_providers.dart
+18
-17
@@ -1,7 +1,6 @@
1
import 'package:flutter_riverpod/flutter_riverpod.dart';
2
-import 'package:localmind/features/settings/data/models/app_settings.dart';
2
+import '../../features/settings/data/models/app_settings.dart';
3
import 'storage_providers.dart';
4
-
4
import '../theme/app_theme.dart';
5
6
final themeModeProvider = NotifierProvider<ThemeModeNotifier, AppThemeType>(() {
@@ -11,9 +10,9 @@ final themeModeProvider = NotifierProvider<ThemeModeNotifier, AppThemeType>(() {
10
class ThemeModeNotifier extends Notifier<AppThemeType> {
11
@override
12
AppThemeType build() {
14
- final boxes = ref.watch(hiveBoxesProvider);
15
- final savedMode = boxes.settings.get('themeMode');
16
- if (savedMode is int &&
13
+ final prefs = ref.watch(sharedPreferencesProvider);
14
+ final savedMode = prefs.getInt('themeMode');
15
+ if (savedMode != null &&
16
savedMode >= 0 &&
17
savedMode < AppThemeType.values.length) {
18
return AppThemeType.values[savedMode];
@@ -22,9 +21,9 @@ class ThemeModeNotifier extends Notifier<AppThemeType> {
21
}
22
23
void setThemeMode(AppThemeType mode) {
25
- final boxes = ref.read(hiveBoxesProvider);
24
+ final prefs = ref.read(sharedPreferencesProvider);
25
state = mode;
27
- boxes.settings.put('themeMode', mode.index);
26
+ prefs.setInt('themeMode', mode.index);
27
}
28
}
29
@@ -35,18 +34,20 @@ final settingsProvider = NotifierProvider<SettingsNotifier, AppSettings>(() {
34
class SettingsNotifier extends Notifier<AppSettings> {
35
@override
36
AppSettings build() {
38
- final boxes = ref.watch(hiveBoxesProvider);
39
- final saved = boxes.settings.get('appSettings');
40
- if (saved is AppSettings) {
41
- return saved;
37
+ final prefs = ref.watch(sharedPreferencesProvider);
38
+ final savedJson = prefs.getString('appSettings');
39
+ if (savedJson != null) {
40
+ try {
41
+ return AppSettings.fromJson(savedJson);
42
+ } catch (_) {}
43
}
44
return AppSettings();
45
}
46
47
Future<void> updateSettings(AppSettings appSettings) async {
47
- final boxes = ref.read(hiveBoxesProvider);
48
+ final prefs = ref.read(sharedPreferencesProvider);
49
state = appSettings;
49
- await boxes.settings.put('appSettings', appSettings);
50
+ await prefs.setString('appSettings', appSettings.toJson());
51
}
52
53
void setTemperature(double value) =>
@@ -82,13 +83,13 @@ class SettingsNotifier extends Notifier<AppSettings> {
83
84
Future<void> _update(AppSettings updated) async {
85
state = updated;
85
- final boxes = ref.read(hiveBoxesProvider);
86
- await boxes.settings.put('appSettings', updated);
86
+ final prefs = ref.read(sharedPreferencesProvider);
87
+ await prefs.setString('appSettings', updated.toJson());
88
}
89
90
Future<void> resetToDefaults() async {
91
state = AppSettings();
91
- final boxes = ref.read(hiveBoxesProvider);
92
- await boxes.settings.put('appSettings', state);
92
+ final prefs = ref.read(sharedPreferencesProvider);
93
+ await prefs.setString('appSettings', state.toJson());
94
}
95
}
lib/core/providers/service_providers.dart
+1
-1
@@ -1,7 +1,7 @@
1
import 'package:flutter_riverpod/flutter_riverpod.dart';
2
import 'package:dio/dio.dart';
3
import '../constants/app_constants.dart';
4
-import 'package:localmind/features/servers/data/server_api_service.dart';
4
+import '../../features/servers/data/server_api_service.dart';
5
6
final dioProvider = Provider<Dio>((ref) {
7
return Dio(
lib/core/providers/storage_providers.dart
+5
-32
@@ -1,42 +1,15 @@
1
import 'dart:io';
2
import 'package:flutter_riverpod/flutter_riverpod.dart';
3
import 'package:path_provider/path_provider.dart';
4
-import 'package:hive_ce/hive.dart';
5
-import 'package:localmind/features/conversations/data/models/conversation.dart';
6
-import 'package:localmind/features/personas/data/models/persona.dart';
7
-import '../storage/hive_initializer.dart';
4
+import 'package:shared_preferences/shared_preferences.dart';
5
+import '../storage/objectbox_store.dart';
6
9
-final hiveBoxesProvider = Provider<HiveBoxes>((ref) {
7
+final databaseProvider = Provider<ObjectBoxStore>((ref) {
8
throw UnimplementedError('Must be overridden in main.dart');
9
});
10
13
-final settingsBoxProvider = Provider<dynamic>((ref) {
14
- final boxes = ref.watch(hiveBoxesProvider);
15
- return boxes.settings;
16
-});
17
-
18
-final conversationsBoxProvider = Provider<Box<Conversation>>((ref) {
19
- final boxes = ref.watch(hiveBoxesProvider);
20
- return boxes.conversations;
21
-});
22
-
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
-
31
-
32
-final personasBoxProvider = Provider<Box<Persona>>((ref) {
33
- final boxes = ref.watch(hiveBoxesProvider);
34
- return boxes.personas;
35
-});
36
-
37
-final directBoxPersonasProvider = Provider<List<Persona>>((ref) {
38
- final box = ref.watch(personasBoxProvider);
39
- return box.values.toList();
11
+final sharedPreferencesProvider = Provider<SharedPreferences>((ref) {
12
+ throw UnimplementedError('Must be overridden in main.dart');
13
});
14
15
final storageDirectoryProvider = FutureProvider<Directory>((ref) async {
lib/core/repository/server/server_repository.dart
+2
-2
@@ -1,5 +1,5 @@
1
-import 'package:localmind/features/models/data/models/model_info.dart';
2
-import 'package:localmind/features/servers/data/models/server.dart';
1
+import '../../../features/models/data/models/model_info.dart';
2
+import '../../../features/servers/data/models/server.dart';
3
4
abstract class ServerRepository {
5
Future<bool> testConnection(Server server);
lib/core/storage/entities.dart
new
+332
@@ -0,0 +1,332 @@
1
+import 'dart:convert';
2
+import 'package:objectbox/objectbox.dart';
3
+import '../models/enums.dart';
4
+import '../../features/chat/data/models/message.dart';
5
+import '../../features/conversations/data/models/conversation.dart';
6
+import '../../features/personas/data/models/persona.dart';
7
+import '../../features/servers/data/models/server.dart';
8
+
9
+@Entity()
10
+class ServerEntity {
11
+ @Id()
12
+ int internalId = 0;
13
+
14
+ @Index()
15
+ String id;
16
+ String name;
17
+
18
+ @Property(type: PropertyType.int)
19
+ int typeIndex;
20
+
21
+ String host;
22
+ int port;
23
+ String? apiKey;
24
+ bool isDefault;
25
+
26
+ @Property(type: PropertyType.date)
27
+ DateTime createdAt;
28
+
29
+ @Property(type: PropertyType.date)
30
+ DateTime lastConnectedAt;
31
+
32
+ @Property(type: PropertyType.int)
33
+ int statusIndex;
34
+
35
+ String? iconName;
36
+
37
+ ServerEntity({
38
+ this.internalId = 0,
39
+ required this.id,
40
+ required this.name,
41
+ required this.typeIndex,
42
+ required this.host,
43
+ required this.port,
44
+ this.apiKey,
45
+ this.isDefault = false,
46
+ required this.createdAt,
47
+ required this.lastConnectedAt,
48
+ required this.statusIndex,
49
+ this.iconName,
50
+ });
51
+
52
+ factory ServerEntity.fromDomain(Server server) {
53
+ return ServerEntity(
54
+ id: server.id,
55
+ name: server.name,
56
+ typeIndex: server.type.index,
57
+ host: server.host,
58
+ port: server.port,
59
+ apiKey: server.apiKey,
60
+ isDefault: server.isDefault,
61
+ createdAt: server.createdAt,
62
+ lastConnectedAt: server.lastConnectedAt,
63
+ statusIndex: server.status.index,
64
+ iconName: server.iconName,
65
+ );
66
+ }
67
+
68
+ Server toDomain() {
69
+ return Server(
70
+ id: id,
71
+ name: name,
72
+ type: ServerType.values[typeIndex],
73
+ host: host,
74
+ port: port,
75
+ apiKey: apiKey,
76
+ isDefault: isDefault,
77
+ createdAt: createdAt,
78
+ lastConnectedAt: lastConnectedAt,
79
+ status: ConnectionStatus.values[statusIndex],
80
+ iconName: iconName,
81
+ );
82
+ }
83
+}
84
+
85
+@Entity()
86
+class PersonaEntity {
87
+ @Id()
88
+ int internalId = 0;
89
+
90
+ @Index()
91
+ String id;
92
+ String name;
93
+ String emoji;
94
+ String systemPrompt;
95
+ String? description;
96
+ bool isBuiltIn;
97
+
98
+ @Property(type: PropertyType.date)
99
+ DateTime createdAt;
100
+
101
+ @Property(type: PropertyType.date)
102
+ DateTime updatedAt;
103
+
104
+ String? category;
105
+ String? preferredParamsJson;
106
+
107
+ PersonaEntity({
108
+ this.internalId = 0,
109
+ required this.id,
110
+ required this.name,
111
+ required this.emoji,
112
+ required this.systemPrompt,
113
+ this.description,
114
+ this.isBuiltIn = false,
115
+ required this.createdAt,
116
+ required this.updatedAt,
117
+ this.category,
118
+ this.preferredParamsJson,
119
+ });
120
+
121
+ factory PersonaEntity.fromDomain(Persona persona) {
122
+ return PersonaEntity(
123
+ id: persona.id,
124
+ name: persona.name,
125
+ emoji: persona.emoji,
126
+ systemPrompt: persona.systemPrompt,
127
+ description: persona.description,
128
+ isBuiltIn: persona.isBuiltIn,
129
+ createdAt: persona.createdAt,
130
+ updatedAt: persona.updatedAt,
131
+ category: persona.category,
132
+ preferredParamsJson: persona.preferredParams != null
133
+ ? jsonEncode(persona.preferredParams)
134
+ : null,
135
+ );
136
+ }
137
+
138
+ Persona toDomain() {
139
+ return Persona(
140
+ id: id,
141
+ name: name,
142
+ emoji: emoji,
143
+ systemPrompt: systemPrompt,
144
+ description: description,
145
+ isBuiltIn: isBuiltIn,
146
+ createdAt: createdAt,
147
+ updatedAt: updatedAt,
148
+ category: category,
149
+ preferredParams: preferredParamsJson != null
150
+ ? Map<String, dynamic>.from(jsonDecode(preferredParamsJson!))
151
+ : null,
152
+ );
153
+ }
154
+}
155
+
156
+@Entity()
157
+class ConversationEntity {
158
+ @Id()
159
+ int internalId = 0;
160
+
161
+ @Index()
162
+ String id;
163
+ String title;
164
+
165
+ @Property(type: PropertyType.date)
166
+ DateTime createdAt;
167
+
168
+ @Property(type: PropertyType.date)
169
+ DateTime updatedAt;
170
+
171
+ bool isPinned;
172
+ String? personaId;
173
+ String? serverId;
174
+ String? modelId;
175
+ int messageCount;
176
+ String? lastMessagePreview;
177
+ String? systemPrompt;
178
+
179
+ @Backlink()
180
+ final messages = ToMany<MessageEntity>();
181
+
182
+ ConversationEntity({
183
+ this.internalId = 0,
184
+ required this.id,
185
+ required this.title,
186
+ required this.createdAt,
187
+ required this.updatedAt,
188
+ this.isPinned = false,
189
+ this.personaId,
190
+ this.serverId,
191
+ this.modelId,
192
+ this.messageCount = 0,
193
+ this.lastMessagePreview,
194
+ this.systemPrompt,
195
+ });
196
+
197
+ factory ConversationEntity.fromDomain(Conversation conversation) {
198
+ return ConversationEntity(
199
+ id: conversation.id,
200
+ title: conversation.title,
201
+ createdAt: conversation.createdAt,
202
+ updatedAt: conversation.updatedAt,
203
+ isPinned: conversation.isPinned,
204
+ personaId: conversation.personaId,
205
+ serverId: conversation.serverId,
206
+ modelId: conversation.modelId,
207
+ messageCount: conversation.messageCount,
208
+ lastMessagePreview: conversation.lastMessagePreview,
209
+ systemPrompt: conversation.systemPrompt,
210
+ );
211
+ }
212
+
213
+ Conversation toDomain() {
214
+ return Conversation(
215
+ id: id,
216
+ title: title,
217
+ createdAt: createdAt,
218
+ updatedAt: updatedAt,
219
+ isPinned: isPinned,
220
+ personaId: personaId,
221
+ serverId: serverId,
222
+ modelId: modelId,
223
+ messageCount: messageCount,
224
+ lastMessagePreview: lastMessagePreview,
225
+ systemPrompt: systemPrompt,
226
+ );
227
+ }
228
+}
229
+
230
+@Entity()
231
+class MessageEntity {
232
+ @Id()
233
+ int internalId = 0;
234
+
235
+ @Index()
236
+ String id;
237
+
238
+ final conversation = ToOne<ConversationEntity>();
239
+
240
+ @Index()
241
+ String conversationUid;
242
+
243
+ @Property(type: PropertyType.int)
244
+ int roleIndex;
245
+
246
+ String content;
247
+
248
+ @Property(type: PropertyType.date)
249
+ DateTime createdAt;
250
+
251
+ @Property(type: PropertyType.int)
252
+ int statusIndex;
253
+
254
+ String? modelId;
255
+ int? tokenCount;
256
+ String? errorMessage;
257
+ String? attachmentPathsJson;
258
+ int? generationTimeMs;
259
+ String? reasoningContent;
260
+ String? toolCallsJson;
261
+ String? toolCallId;
262
+ bool isProcessing;
263
+
264
+ MessageEntity({
265
+ this.internalId = 0,
266
+ required this.id,
267
+ required this.conversationUid,
268
+ required this.roleIndex,
269
+ required this.content,
270
+ required this.createdAt,
271
+ required this.statusIndex,
272
+ this.modelId,
273
+ this.tokenCount,
274
+ this.errorMessage,
275
+ this.attachmentPathsJson,
276
+ this.generationTimeMs,
277
+ this.reasoningContent,
278
+ this.toolCallsJson,
279
+ this.toolCallId,
280
+ this.isProcessing = false,
281
+ });
282
+
283
+ factory MessageEntity.fromDomain(Message message) {
284
+ return MessageEntity(
285
+ id: message.id,
286
+ conversationUid: message.conversationId,
287
+ roleIndex: message.role.index,
288
+ content: message.content,
289
+ createdAt: message.createdAt,
290
+ statusIndex: message.status.index,
291
+ modelId: message.modelId,
292
+ tokenCount: message.tokenCount,
293
+ errorMessage: message.errorMessage,
294
+ attachmentPathsJson: message.attachmentPaths != null
295
+ ? jsonEncode(message.attachmentPaths)
296
+ : null,
297
+ generationTimeMs: message.generationTimeMs,
298
+ reasoningContent: message.reasoningContent,
299
+ toolCallsJson: message.toolCalls != null
300
+ ? jsonEncode(message.toolCalls!.map((e) => e.toMap()).toList())
301
+ : null,
302
+ toolCallId: message.toolCallId,
303
+ isProcessing: message.isProcessing,
304
+ );
305
+ }
306
+
307
+ Message toDomain() {
308
+ return Message(
309
+ id: id,
310
+ conversationId: conversationUid,
311
+ role: MessageRole.values[roleIndex],
312
+ content: content,
313
+ createdAt: createdAt,
314
+ status: MessageStatus.values[statusIndex],
315
+ modelId: modelId,
316
+ tokenCount: tokenCount,
317
+ errorMessage: errorMessage,
318
+ attachmentPaths: attachmentPathsJson != null
319
+ ? List<String>.from(jsonDecode(attachmentPathsJson!))
320
+ : null,
321
+ generationTimeMs: generationTimeMs,
322
+ reasoningContent: reasoningContent,
323
+ toolCalls: toolCallsJson != null
324
+ ? (jsonDecode(toolCallsJson!) as List)
325
+ .map((e) => ToolCallData.fromMap(Map<String, dynamic>.from(e)))
326
+ .toList()
327
+ : null,
328
+ toolCallId: toolCallId,
329
+ isProcessing: isProcessing,
330
+ );
331
+ }
332
+}
lib/core/storage/hive_initializer.dart
deleted
-79
@@ -1,79 +0,0 @@
1
-import 'package:hive_ce_flutter/hive_flutter.dart';
2
-import 'package:path_provider/path_provider.dart';
3
-import 'package:localmind/features/servers/data/models/server.dart';
4
-import 'package:localmind/features/chat/data/models/message.dart';
5
-import 'package:localmind/features/conversations/data/models/conversation.dart';
6
-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;
14
- final Box<Message> messages;
15
- final Box<Conversation> conversations;
16
- final Box<Persona> personas;
17
- final Box<dynamic> settings;
18
-
19
- const HiveBoxes({
20
- required this.servers,
21
- required this.messages,
22
- required this.conversations,
23
- required this.personas,
24
- required this.settings,
25
- });
26
-}
27
-
28
-class HiveInitializer {
29
- static Future<HiveBoxes> initialize() async {
30
- final appDir = await getApplicationDocumentsDirectory();
31
- await Hive.initFlutter(appDir.path);
32
-
33
- Hive.registerAdapter(ServerTypeAdapter());
34
- Hive.registerAdapter(ConnectionStatusAdapter());
35
- Hive.registerAdapter(MessageRoleAdapter());
36
- Hive.registerAdapter(MessageStatusAdapter());
37
- Hive.registerAdapter(ServerAdapter());
38
- Hive.registerAdapter(MessageAdapter());
39
- Hive.registerAdapter(ConversationAdapter());
40
- Hive.registerAdapter(PersonaAdapter());
41
- Hive.registerAdapter(AppSettingsAdapter());
42
- Hive.registerAdapter(ThemeModeAdapter());
43
- Hive.registerAdapter(SyntaxThemeNameAdapter());
44
-
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
- );
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,
59
- messages: messages,
60
- conversations: conversations,
61
- personas: personas,
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/hive_keys.dart
deleted
-7
@@ -1,7 +0,0 @@
1
-class HiveKeys {
2
- static const String servers = 'servers';
3
- static const String messages = 'messages';
4
- static const String conversations = 'conversations';
5
- static const String personas = 'personas';
6
- static const String settings = 'settings';
7
-}
lib/core/storage/objectbox_store.dart
new
+34
@@ -0,0 +1,34 @@
1
+import 'dart:io';
2
+import 'package:path_provider/path_provider.dart';
3
+import 'package:path/path.dart' as p;
4
+import '../../objectbox.g.dart';
5
+import 'entities.dart';
6
+
7
+class ObjectBoxStore {
8
+ late final Store store;
9
+
10
+ late final Box<ServerEntity> serverBox;
11
+ late final Box<PersonaEntity> personaBox;
12
+ late final Box<ConversationEntity> conversationBox;
13
+ late final Box<MessageEntity> messageBox;
14
+
15
+ ObjectBoxStore._create(this.store) {
16
+ serverBox = Box<ServerEntity>(store);
17
+ personaBox = Box<PersonaEntity>(store);
18
+ conversationBox = Box<ConversationEntity>(store);
19
+ messageBox = Box<MessageEntity>(store);
20
+ }
21
+
22
+ /// Create an instance of ObjectBox storage.
23
+ static Future<ObjectBoxStore> create() async {
24
+ final docsDir = await getApplicationDocumentsDirectory();
25
+ final storeDir = Directory(p.join(docsDir.path, "localmind_objectbox"));
26
+
27
+ if (!await storeDir.exists()) {
28
+ await storeDir.create(recursive: true);
29
+ }
30
+
31
+ final store = await openStore(directory: storeDir.path);
32
+ return ObjectBoxStore._create(store);
33
+ }
34
+}
lib/core/storage/secure_storage_service.dart
deleted
-32
@@ -1,32 +0,0 @@
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/core/theme/app_theme.dart
+45
-44
@@ -1,14 +1,10 @@
1
import 'package:flutter/material.dart';
2
import 'package:shadcn_ui/shadcn_ui.dart';
3
+
4
import 'colors.dart';
5
import 'typography.dart';
6
6
-enum AppThemeType {
7
- system,
8
- light,
9
- dark,
10
- claude
11
-}
7
+enum AppThemeType { system, light, dark, claude }
8
9
class AppTheme {
10
static ShadThemeData get darkShadTheme => ShadThemeData(
@@ -114,9 +110,7 @@ class AppTheme {
110
style: ElevatedButton.styleFrom(
111
backgroundColor: AppColors.darkAccent,
112
foregroundColor: AppColors.darkBackground,
117
- shape: RoundedRectangleBorder(
118
- borderRadius: BorderRadius.circular(8),
119
- ),
113
+ shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
114
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
115
),
116
),
@@ -124,18 +118,14 @@ class AppTheme {
118
style: OutlinedButton.styleFrom(
119
foregroundColor: AppColors.darkPrimaryText,
120
side: const BorderSide(color: AppColors.darkBorder),
127
- shape: RoundedRectangleBorder(
128
- borderRadius: BorderRadius.circular(8),
129
- ),
121
+ shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
122
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
123
),
124
),
125
textButtonTheme: TextButtonThemeData(
126
style: TextButton.styleFrom(
127
foregroundColor: AppColors.darkPrimaryText,
136
- shape: RoundedRectangleBorder(
137
- borderRadius: BorderRadius.circular(8),
138
- ),
128
+ shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
129
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
130
),
131
),
@@ -147,16 +137,12 @@ class AppTheme {
137
),
138
dialogTheme: DialogThemeData(
139
backgroundColor: AppColors.darkSurface,
150
- shape: RoundedRectangleBorder(
151
- borderRadius: BorderRadius.circular(12),
152
- ),
140
+ shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
141
),
142
snackBarTheme: SnackBarThemeData(
143
backgroundColor: AppColors.darkSurface,
144
contentTextStyle: AppTypography.darkTextTheme.bodyMedium,
157
- shape: RoundedRectangleBorder(
158
- borderRadius: BorderRadius.circular(8),
159
- ),
145
+ shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
146
behavior: SnackBarBehavior.floating,
147
),
148
listTileTheme: const ListTileThemeData(
@@ -167,9 +153,17 @@ class AppTheme {
153
indicatorColor: AppColors.darkSurface,
154
labelTextStyle: WidgetStateProperty.resolveWith((states) {
155
if (states.contains(WidgetState.selected)) {
170
- return const TextStyle(color: AppColors.darkPrimaryText, fontSize: 12, fontWeight: FontWeight.w600);
156
+ return const TextStyle(
157
+ color: AppColors.darkPrimaryText,
158
+ fontSize: 12,
159
+ fontWeight: FontWeight.w600,
160
+ );
161
}
172
- return const TextStyle(color: AppColors.darkMutedText, fontSize: 12, fontWeight: FontWeight.w500);
162
+ return const TextStyle(
163
+ color: AppColors.darkMutedText,
164
+ fontSize: 12,
165
+ fontWeight: FontWeight.w500,
166
+ );
167
}),
168
iconTheme: WidgetStateProperty.resolveWith((states) {
169
if (states.contains(WidgetState.selected)) {
@@ -234,9 +228,7 @@ class AppTheme {
228
style: ElevatedButton.styleFrom(
229
backgroundColor: AppColors.lightAccent,
230
foregroundColor: Colors.white,
237
- shape: RoundedRectangleBorder(
238
- borderRadius: BorderRadius.circular(8),
239
- ),
231
+ shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
232
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
233
),
234
),
@@ -244,9 +236,7 @@ class AppTheme {
236
style: OutlinedButton.styleFrom(
237
foregroundColor: AppColors.lightPrimaryText,
238
side: const BorderSide(color: AppColors.lightBorder),
247
- shape: RoundedRectangleBorder(
248
- borderRadius: BorderRadius.circular(8),
249
- ),
239
+ shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
240
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
241
),
242
),
@@ -258,18 +248,24 @@ class AppTheme {
248
),
249
dialogTheme: DialogThemeData(
250
backgroundColor: AppColors.lightSurface,
261
- shape: RoundedRectangleBorder(
262
- borderRadius: BorderRadius.circular(12),
263
- ),
251
+ shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
252
),
253
navigationBarTheme: NavigationBarThemeData(
254
backgroundColor: AppColors.lightBackground,
255
indicatorColor: AppColors.lightSurface,
256
labelTextStyle: WidgetStateProperty.resolveWith((states) {
257
if (states.contains(WidgetState.selected)) {
270
- return const TextStyle(color: AppColors.lightPrimaryText, fontSize: 12, fontWeight: FontWeight.w600);
258
+ return const TextStyle(
259
+ color: AppColors.lightPrimaryText,
260
+ fontSize: 12,
261
+ fontWeight: FontWeight.w600,
262
+ );
263
}
272
- return const TextStyle(color: AppColors.lightMutedText, fontSize: 12, fontWeight: FontWeight.w500);
264
+ return const TextStyle(
265
+ color: AppColors.lightMutedText,
266
+ fontSize: 12,
267
+ fontWeight: FontWeight.w500,
268
+ );
269
}),
270
iconTheme: WidgetStateProperty.resolveWith((states) {
271
if (states.contains(WidgetState.selected)) {
@@ -334,9 +330,7 @@ class AppTheme {
330
style: ElevatedButton.styleFrom(
331
backgroundColor: AppColors.claudeAccent,
332
foregroundColor: Colors.white,
337
- shape: RoundedRectangleBorder(
338
- borderRadius: BorderRadius.circular(12),
339
- ),
333
+ shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
334
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
335
),
336
),
@@ -345,7 +339,9 @@ class AppTheme {
339
foregroundColor: AppColors.claudePrimaryText,
340
side: const BorderSide(color: AppColors.claudeBorder),
341
shape: RoundedRectangleBorder(
348
- borderRadius: BorderRadius.circular(8), // Secondary buttons are less rounded
342
+ borderRadius: BorderRadius.circular(
343
+ 8,
344
+ ), // Secondary buttons are less rounded
345
),
346
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
347
),
@@ -358,18 +354,24 @@ class AppTheme {
354
),
355
dialogTheme: DialogThemeData(
356
backgroundColor: AppColors.claudeSurface,
361
- shape: RoundedRectangleBorder(
362
- borderRadius: BorderRadius.circular(16),
363
- ),
357
+ shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
358
),
359
navigationBarTheme: NavigationBarThemeData(
360
backgroundColor: AppColors.claudeBackground,
361
indicatorColor: AppColors.claudeSurface,
362
labelTextStyle: WidgetStateProperty.resolveWith((states) {
363
if (states.contains(WidgetState.selected)) {
370
- return const TextStyle(color: AppColors.claudePrimaryText, fontSize: 12, fontWeight: FontWeight.w600);
364
+ return const TextStyle(
365
+ color: AppColors.claudePrimaryText,
366
+ fontSize: 12,
367
+ fontWeight: FontWeight.w600,
368
+ );
369
}
372
- return const TextStyle(color: AppColors.claudeSecondaryText, fontSize: 12, fontWeight: FontWeight.w500);
370
+ return const TextStyle(
371
+ color: AppColors.claudeSecondaryText,
372
+ fontSize: 12,
373
+ fontWeight: FontWeight.w500,
374
+ );
375
}),
376
iconTheme: WidgetStateProperty.resolveWith((states) {
377
if (states.contains(WidgetState.selected)) {
@@ -379,5 +381,4 @@ class AppTheme {
381
}),
382
),
383
);
382
-
384
}
lib/core/theme/typography.dart
+2
@@ -1,5 +1,6 @@
1
import 'package:flutter/material.dart';
2
import 'package:google_fonts/google_fonts.dart';
3
+
4
import 'colors.dart';
5
6
class AppTypography {
@@ -49,6 +50,7 @@ class AppTypography {
50
),
51
);
52
}
53
+
54
static TextTheme get claudeTextTheme {
55
return GoogleFonts.loraTextTheme(
56
const TextTheme(
lib/features/chat/data/chat_service.dart
+10
-8
@@ -1,12 +1,12 @@
1
import 'dart:async';
2
import 'dart:convert';
3
import 'package:dio/dio.dart';
4
-import 'package:localmind/features/servers/data/models/server.dart';
5
-import 'package:localmind/features/chat/data/models/message.dart';
6
-import 'package:localmind/features/chat/data/models/chat_parameters.dart';
7
-import 'package:localmind/features/chat/data/models/mcp_integration.dart';
8
-import 'package:localmind/core/models/enums.dart';
9
-import 'package:localmind/core/logger/app_logger.dart';
4
+import '../../servers/data/models/server.dart';
5
+import 'models/message.dart';
6
+import 'models/chat_parameters.dart';
7
+import 'models/mcp_integration.dart';
8
+import '../../../core/models/enums.dart';
9
+import '../../../core/logger/app_logger.dart';
10
11
abstract class ChatService {
12
Stream<ChatResponse> sendMessage({
@@ -173,7 +173,8 @@ class LMStudioChatService implements ChatService {
173
responseType: ResponseType.stream,
174
headers: {
175
'Content-Type': 'application/json',
176
- if (server.apiKey != null) 'Authorization': 'Bearer ${server.apiKey}',
176
+ if (server.apiKey != null)
177
+ 'Authorization': 'Bearer ${server.apiKey}',
178
},
179
),
180
cancelToken: _cancelToken,
@@ -433,7 +434,8 @@ class OpenAICompatibleChatService implements ChatService {
434
responseType: ResponseType.stream,
435
headers: {
436
'Content-Type': 'application/json',
436
- if (server.apiKey != null) 'Authorization': 'Bearer ${server.apiKey}',
437
+ if (server.apiKey != null)
438
+ 'Authorization': 'Bearer ${server.apiKey}',
439
},
440
),
441
cancelToken: _cancelToken,
lib/features/chat/data/export_service.dart
+2
-2
@@ -1,7 +1,7 @@
1
import 'dart:io';
2
import 'package:path_provider/path_provider.dart';
3
-import 'package:localmind/features/chat/data/models/message.dart';
4
-import 'package:localmind/core/models/enums.dart';
3
+import 'models/message.dart';
4
+import '../../../core/models/enums.dart';
5
6
class ExportService {
7
static Future<String> exportAsMarkdown(
lib/features/chat/data/mcp_server_manager.dart
+1
-1
@@ -1,4 +1,4 @@
1
-import 'package:localmind/features/chat/data/mcp_client.dart';
1
+import 'mcp_client.dart';
2
3
class McpServerManager {
4
final Map<String, McpClient> _clients = {};
lib/features/chat/data/models/chat_parameters.dart
+1
-1
@@ -1,4 +1,4 @@
1
-import 'package:localmind/core/constants/app_constants.dart';
1
+import '../../../../core/constants/app_constants.dart';
2
3
class ChatParameters {
4
final double temperature;
lib/features/chat/data/models/message.dart
+26
-34
@@ -1,53 +1,22 @@
1
-import 'package:hive_ce/hive.dart';
2
-import 'package:localmind/core/models/enums.dart';
1
+import 'dart:convert';
2
4
-part 'message.g.dart';
3
+import '../../../../core/models/enums.dart';
4
6
-@HiveType(typeId: 1)
7
-class Message extends HiveObject {
8
- @HiveField(0)
5
+class Message {
6
final String id;
10
-
11
- @HiveField(1)
7
final String conversationId;
13
-
14
- @HiveField(2)
8
final MessageRole role;
16
-
17
- @HiveField(3)
9
final String content;
19
-
20
- @HiveField(4)
10
final DateTime createdAt;
22
-
23
- @HiveField(5)
11
final MessageStatus status;
25
-
26
- @HiveField(6)
12
final String? modelId;
28
-
29
- @HiveField(7)
13
final int? tokenCount;
31
-
32
- @HiveField(8)
14
final String? errorMessage;
34
-
35
- @HiveField(9)
15
final List<String>? attachmentPaths;
37
-
38
- @HiveField(10)
16
final int? generationTimeMs;
40
-
41
- @HiveField(11)
17
final String? reasoningContent;
43
-
44
- @HiveField(12)
18
final List<ToolCallData>? toolCalls;
46
-
47
- @HiveField(13)
19
final String? toolCallId;
49
-
50
- @HiveField(14)
20
final bool isProcessing;
21
22
Message({
@@ -117,4 +86,27 @@ class ToolCallData {
86
required this.arguments,
87
this.result,
88
});
89
+
90
+ Map<String, dynamic> toMap() {
91
+ return {
92
+ 'id': id,
93
+ 'toolName': toolName,
94
+ 'arguments': arguments,
95
+ 'result': result,
96
+ };
97
+ }
98
+
99
+ factory ToolCallData.fromMap(Map<String, dynamic> map) {
100
+ return ToolCallData(
101
+ id: map['id'],
102
+ toolName: map['toolName'],
103
+ arguments: Map<String, dynamic>.from(map['arguments'] ?? {}),
104
+ result: map['result'],
105
+ );
106
+ }
107
+
108
+ String toJson() => json.encode(toMap());
109
+
110
+ factory ToolCallData.fromJson(String source) =>
111
+ ToolCallData.fromMap(json.decode(source));
112
}
lib/features/chat/data/models/message.g.dart
deleted
-85
@@ -1,85 +0,0 @@
1
-// GENERATED CODE - DO NOT MODIFY BY HAND
2
-
3
-part of 'message.dart';
4
-
5
-// **************************************************************************
6
-// TypeAdapterGenerator
7
-// **************************************************************************
8
-
9
-class MessageAdapter extends TypeAdapter<Message> {
10
- @override
11
- final typeId = 1;
12
-
13
- @override
14
- Message read(BinaryReader reader) {
15
- final numOfFields = reader.readByte();
16
- final fields = <int, dynamic>{
17
- for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
18
- };
19
- return Message(
20
- id: fields[0] as String,
21
- conversationId: fields[1] as String,
22
- role: fields[2] as MessageRole,
23
- content: fields[3] as String,
24
- createdAt: fields[4] as DateTime,
25
- status: fields[5] == null
26
- ? MessageStatus.complete
27
- : fields[5] as MessageStatus,
28
- modelId: fields[6] as String?,
29
- tokenCount: (fields[7] as num?)?.toInt(),
30
- errorMessage: fields[8] as String?,
31
- attachmentPaths: (fields[9] as List?)?.cast<String>(),
32
- generationTimeMs: (fields[10] as num?)?.toInt(),
33
- reasoningContent: fields[11] as String?,
34
- toolCalls: (fields[12] as List?)?.cast<ToolCallData>(),
35
- toolCallId: fields[13] as String?,
36
- isProcessing: fields[14] == null ? false : fields[14] as bool,
37
- );
38
- }
39
-
40
- @override
41
- void write(BinaryWriter writer, Message obj) {
42
- writer
43
- ..writeByte(15)
44
- ..writeByte(0)
45
- ..write(obj.id)
46
- ..writeByte(1)
47
- ..write(obj.conversationId)
48
- ..writeByte(2)
49
- ..write(obj.role)
50
- ..writeByte(3)
51
- ..write(obj.content)
52
- ..writeByte(4)
53
- ..write(obj.createdAt)
54
- ..writeByte(5)
55
- ..write(obj.status)
56
- ..writeByte(6)
57
- ..write(obj.modelId)
58
- ..writeByte(7)
59
- ..write(obj.tokenCount)
60
- ..writeByte(8)
61
- ..write(obj.errorMessage)
62
- ..writeByte(9)
63
- ..write(obj.attachmentPaths)
64
- ..writeByte(10)
65
- ..write(obj.generationTimeMs)
66
- ..writeByte(11)
67
- ..write(obj.reasoningContent)
68
- ..writeByte(12)
69
- ..write(obj.toolCalls)
70
- ..writeByte(13)
71
- ..write(obj.toolCallId)
72
- ..writeByte(14)
73
- ..write(obj.isProcessing);
74
- }
75
-
76
- @override
77
- int get hashCode => typeId.hashCode;
78
-
79
- @override
80
- bool operator ==(Object other) =>
81
- identical(this, other) ||
82
- other is MessageAdapter &&
83
- runtimeType == other.runtimeType &&
84
- typeId == other.typeId;
85
-}
lib/features/chat/providers/chat_providers.dart
+422
-260
@@ -1,20 +1,24 @@
1
import 'dart:async';
2
import 'dart:io';
3
import 'dart:math';
4
+
5
import 'package:flutter_riverpod/flutter_riverpod.dart';
5
-import 'package:localmind/core/models/enums.dart';
6
-import 'package:localmind/core/providers/service_providers.dart';
7
-import 'package:localmind/core/providers/storage_providers.dart';
8
-import 'package:localmind/features/chat/data/chat_service.dart';
9
-import 'package:localmind/features/chat/data/models/chat_parameters.dart';
10
-import 'package:localmind/features/chat/data/models/message.dart';
11
-import 'package:localmind/features/conversations/data/models/conversation.dart';
6
+import '../../../core/providers/app_providers.dart';
7
import 'package:localmind/features/conversations/providers/conversation_providers.dart'
8
as conv;
9
import 'package:localmind/features/models/data/models/model_info.dart';
15
-import 'package:localmind/features/servers/providers/server_providers.dart';
16
-import 'package:localmind/core/providers/app_providers.dart';
10
import 'package:localmind/features/personas/providers/personas_providers.dart';
11
+import 'package:localmind/features/servers/providers/server_providers.dart';
12
+
13
+import '../../../core/models/enums.dart';
14
+import '../../../core/providers/service_providers.dart';
15
+import '../../../core/providers/storage_providers.dart';
16
+import '../../../core/storage/entities.dart';
17
+import '../../../objectbox.g.dart';
18
+import '../../conversations/data/models/conversation.dart';
19
+import '../data/chat_service.dart';
20
+import '../data/models/chat_parameters.dart';
21
+import '../data/models/message.dart';
22
23
final selectedModelProvider =
24
NotifierProvider<SelectedModelNotifier, ModelInfo?>(() {
@@ -35,29 +39,56 @@ class SelectedModelNotifier extends Notifier<ModelInfo?> {
39
}
40
41
final autoSelectFirstLoadedModelProvider = FutureProvider<void>((ref) async {
42
+ final activeServer = ref.watch(activeServerProvider);
43
+ final status = ref.watch(connectionStatusProvider);
44
+
45
+ if (activeServer == null) {
46
+ Future.microtask(() => ref.read(selectedModelProvider.notifier).clear());
47
+ return;
48
+ }
49
+
50
+ // If server is not connected, we can't fetch loaded models
51
+ if (status != ConnectionStatus.connected) return;
52
+
53
final selectedModel = ref.read(selectedModelProvider);
39
- if (selectedModel != null) return;
54
41
- final activeServer = ref.read(activeServerProvider);
42
- if (activeServer == null) return;
55
+ // If a model is selected but belongs to a different server, clear it
56
+ if (selectedModel != null && selectedModel.serverId != activeServer.id) {
57
+ Future.microtask(() => ref.read(selectedModelProvider.notifier).clear());
58
+ // Continue to try auto-selecting for the new server
59
+ } else if (selectedModel != null) {
60
+ // Already have a valid model selected for this server
61
+ return;
62
+ }
63
+
64
+ // Only auto-select for servers that support listing running models
65
if (activeServer.type == ServerType.openRouter) return;
66
45
- final apiService = ref.read(serverApiServiceProvider);
46
- final loadedModels = await apiService.fetchRunningModels(activeServer);
47
- if (loadedModels.isEmpty) return;
67
+ try {
68
+ final apiService = ref.read(serverApiServiceProvider);
69
+ final loadedModels = await apiService.fetchRunningModels(activeServer);
70
+ if (loadedModels.isEmpty) return;
71
49
- final availableModels = await ref.read(
50
- availableModelsProvider(activeServer.id).future,
51
- );
52
- if (availableModels.isEmpty) return;
72
+ final availableModels = await ref.read(
73
+ availableModelsProvider(activeServer.id).future,
74
+ );
75
+ if (availableModels.isEmpty) return;
76
54
- final typedModels = availableModels.cast<ModelInfo>();
55
- final firstLoadedModel = typedModels
56
- .where((m) => loadedModels.contains(m.id))
57
- .firstOrNull;
77
+ final typedModels = availableModels.cast<ModelInfo>();
78
59
- if (firstLoadedModel != null) {
60
- ref.read(selectedModelProvider.notifier).setModel(firstLoadedModel);
79
+ // For LM Studio, often the 'key' is what's used. For Ollama, it's 'name'.
80
+ // ServerApiService parses these into the same ID field.
81
+ final firstLoadedModel = typedModels
82
+ .where((m) => loadedModels.contains(m.id))
83
+ .firstOrNull;
84
+
85
+ if (firstLoadedModel != null) {
86
+ Future.microtask(() {
87
+ ref.read(selectedModelProvider.notifier).setModel(firstLoadedModel);
88
+ });
89
+ }
90
+ } catch (e) {
91
+ // Silently fail auto-selection
92
}
93
});
94
@@ -141,22 +172,21 @@ final chatParamsProvider = Provider<ChatParameters>((ref) {
172
if (activeConv?.personaId != null) {
173
final personaId = activeConv!.personaId;
174
try {
144
- final boxes = ref.read(hiveBoxesProvider);
145
- final persona = boxes.personas.values.cast<dynamic>().firstWhere(
175
+ final personasAsync = ref.read(personasNotifierProvider);
176
+ final personas = personasAsync.value ?? [];
177
+ final persona = personas.firstWhere(
178
(p) => p.id == personaId,
147
- orElse: () => null,
179
+ orElse: () => throw Exception('Persona not found'),
180
);
149
- if (persona != null) {
150
- systemPrompt = persona.systemPrompt;
151
- if (persona.preferredParams != null) {
152
- final params = persona.preferredParams as Map<String, dynamic>;
153
- if (params['temperature'] != null) {
154
- temperature = (params['temperature'] as num).toDouble();
155
- }
156
- if (params['topP'] != null) topP = (params['topP'] as num).toDouble();
157
- if (params['maxTokens'] != null) {
158
- maxTokens = (params['maxTokens'] as num).toInt();
159
- }
181
+ systemPrompt = persona.systemPrompt;
182
+ if (persona.preferredParams != null) {
183
+ final params = persona.preferredParams as Map<String, dynamic>;
184
+ if (params['temperature'] != null) {
185
+ temperature = (params['temperature'] as num).toDouble();
186
+ }
187
+ if (params['topP'] != null) topP = (params['topP'] as num).toDouble();
188
+ if (params['maxTokens'] != null) {
189
+ maxTokens = (params['maxTokens'] as num).toInt();
190
}
191
}
192
} catch (_) {}
@@ -171,10 +201,10 @@ final chatParamsProvider = Provider<ChatParameters>((ref) {
201
);
202
});
203
174
-final chatServiceProvider = Provider<ChatService>((ref) {
204
+final chatServiceProvider = Provider<ChatService?>((ref) {
205
final server = ref.watch(activeServerProvider);
206
if (server == null) {
177
- throw StateError('No active server');
207
+ return null;
208
}
209
return ChatService.forServer(server.type, ref.read(dioProvider));
210
});
@@ -236,12 +266,14 @@ final smartRepliesProvider = Provider<List<String>>((ref) {
266
class ChatState {
267
final List<Message> messages;
268
final bool isStreaming;
269
+ final bool isLoading;
270
final String? errorMessage;
271
final Message? streamingMessage;
272
273
const ChatState({
274
this.messages = const [],
275
this.isStreaming = false,
276
+ this.isLoading = false,
277
this.errorMessage,
278
this.streamingMessage,
279
});
@@ -249,6 +281,7 @@ class ChatState {
281
ChatState copyWith({
282
List<Message>? messages,
283
bool? isStreaming,
284
+ bool? isLoading,
285
String? errorMessage,
286
Message? streamingMessage,
287
bool clearError = false,
@@ -257,6 +290,7 @@ class ChatState {
290
return ChatState(
291
messages: messages ?? this.messages,
292
isStreaming: isStreaming ?? this.isStreaming,
293
+ isLoading: isLoading ?? this.isLoading,
294
errorMessage: clearError ? null : (errorMessage ?? this.errorMessage),
295
streamingMessage: clearStreaming
296
? null
@@ -282,20 +316,39 @@ class ChatNotifier extends Notifier<ChatState> {
316
}
317
318
Future<void> loadConversation(Conversation conversation) async {
319
+ await cancelStream();
320
_currentConversationId = conversation.id;
286
- final boxes = ref.read(hiveBoxesProvider);
287
- final messages = boxes.messages.values
288
- .where((m) => m.conversationId == conversation.id)
289
- .toList();
290
- messages.sort((a, b) => a.createdAt.compareTo(b.createdAt));
321
+ state = state.copyWith(
322
+ isLoading: true,
323
+ errorMessage: null,
324
+ clearError: true,
325
+ );
326
292
- state = ChatState(messages: messages);
293
- ref
294
- .read(conv.activeConversationProvider.notifier)
295
- .setActiveConversation(conversation);
327
+ try {
328
+ final db = ref.read(databaseProvider);
329
+ final query = db.messageBox
330
+ .query(MessageEntity_.conversationUid.equals(conversation.id))
331
+ .build();
332
+ final entities = query.find();
333
+ query.close();
334
+
335
+ final messages = entities.map((e) => e.toDomain()).toList();
336
+ messages.sort((a, b) => a.createdAt.compareTo(b.createdAt));
337
+
338
+ state = ChatState(messages: messages, isLoading: false);
339
+ ref
340
+ .read(conv.activeConversationProvider.notifier)
341
+ .setActiveConversation(conversation);
342
+ } catch (e) {
343
+ state = state.copyWith(
344
+ isLoading: false,
345
+ errorMessage: 'Failed to load conversation: $e',
346
+ );
347
+ }
348
}
349
350
Future<void> startNewConversation() async {
351
+ await cancelStream();
352
await clearConversation();
353
ref
354
.read(conv.activeConversationProvider.notifier)
@@ -323,7 +376,6 @@ class ChatNotifier extends Notifier<ChatState> {
376
final selectedModel = ref.read(selectedModelProvider);
377
final chatParams = ref.read(chatParamsProvider);
378
final chatService = ref.read(chatServiceProvider);
326
- final boxes = ref.read(hiveBoxesProvider);
379
380
if (server == null) {
381
state = state.copyWith(errorMessage: 'No server connected');
@@ -401,8 +453,8 @@ class ChatNotifier extends Notifier<ChatState> {
453
clearError: true,
454
);
455
404
- await boxes.messages.put(userMessage.id, userMessage);
405
- await boxes.messages.put(assistantMessage.id, assistantMessage);
456
+ await _saveMessage(userMessage);
457
+ await _saveMessage(assistantMessage);
458
459
final messagesForApi = _buildMessagesForApi(selectedModel);
460
@@ -410,208 +462,263 @@ class ChatNotifier extends Notifier<ChatState> {
462
_streamSubscription?.cancel();
463
464
String reasoningContent = '';
465
+ var streamingAssistantMessage = assistantMessage;
466
+
467
+ if (chatService != null) {
468
+ _streamSubscription = chatService
469
+ .sendMessage(
470
+ server: server,
471
+ modelId: selectedModel?.id ?? 'default',
472
+ messages: messagesForApi,
473
+ params: chatParams,
474
+ )
475
+ .listen(
476
+ (response) async {
477
+ final streamConvId = assistantMessage.conversationId;
478
+ final isCurrentContext = _currentConversationId == streamConvId;
479
+
480
+ switch (response.type) {
481
+ case ChatResponseType.message:
482
+ streamingAssistantMessage = streamingAssistantMessage
483
+ .copyWith(
484
+ content:
485
+ streamingAssistantMessage.content +
486
+ (response.content ?? ''),
487
+ isProcessing: false,
488
+ );
489
414
- _streamSubscription = chatService
415
- .sendMessage(
416
- server: server,
417
- modelId: selectedModel?.id ?? 'default',
418
- messages: messagesForApi,
419
- params: chatParams,
420
- )
421
- .listen(
422
- (response) async {
423
- switch (response.type) {
424
- case ChatResponseType.message:
425
- final currentContent = state.streamingMessage?.content ?? '';
426
- final updatedMessage =
427
- (state.streamingMessage ?? assistantMessage).copyWith(
428
- content: currentContent + (response.content ?? ''),
429
- isProcessing: false,
490
+ if (isCurrentContext) {
491
+ final messageIndex = state.messages.indexWhere(
492
+ (m) => m.id == assistantMessageId,
493
);
431
- final messageIndex = state.messages.indexWhere(
432
- (m) => m.id == assistantMessageId,
433
- );
434
- if (messageIndex != -1) {
435
- final updatedMessages = List<Message>.from(state.messages);
436
- updatedMessages[messageIndex] = updatedMessage;
437
- state = state.copyWith(
438
- streamingMessage: updatedMessage,
439
- messages: updatedMessages,
440
- );
441
- } else {
442
- state = state.copyWith(streamingMessage: updatedMessage);
443
- }
444
- break;
445
- case ChatResponseType.reasoning:
446
- reasoningContent += response.reasoningContent ?? '';
447
- final reasoningMessage =
448
- (state.streamingMessage ?? assistantMessage).copyWith(
449
- reasoningContent: reasoningContent,
450
- isProcessing: false,
494
+ if (messageIndex != -1) {
495
+ final updatedMessages = List<Message>.from(
496
+ state.messages,
497
+ );
498
+ updatedMessages[messageIndex] =
499
+ streamingAssistantMessage;
500
+ state = state.copyWith(
501
+ streamingMessage: streamingAssistantMessage,
502
+ messages: updatedMessages,
503
+ );
504
+ } else {
505
+ state = state.copyWith(
506
+ streamingMessage: streamingAssistantMessage,
507
+ );
508
+ }
509
+ }
510
+ break;
511
+ case ChatResponseType.reasoning:
512
+ reasoningContent += response.reasoningContent ?? '';
513
+ streamingAssistantMessage = streamingAssistantMessage
514
+ .copyWith(
515
+ reasoningContent: reasoningContent,
516
+ isProcessing: false,
517
+ );
518
+
519
+ if (isCurrentContext) {
520
+ state = state.copyWith(
521
+ streamingMessage: streamingAssistantMessage,
522
);
452
- state = state.copyWith(streamingMessage: reasoningMessage);
453
- final msgIndex = state.messages.indexWhere(
454
- (m) => m.id == assistantMessageId,
455
- );
456
- if (msgIndex != -1) {
457
- final updatedMessages = List<Message>.from(state.messages);
458
- updatedMessages[msgIndex] = reasoningMessage;
459
- state = state.copyWith(messages: updatedMessages);
460
- }
461
- break;
462
- case ChatResponseType.processing:
463
- // Model is working but hasn't output content yet
464
- final processingMessage =
465
- (state.streamingMessage ?? assistantMessage).copyWith(
466
- isProcessing: true,
523
+ final msgIndex = state.messages.indexWhere(
524
+ (m) => m.id == assistantMessageId,
525
);
468
- state = state.copyWith(streamingMessage: processingMessage);
469
- final procIndex = state.messages.indexWhere(
470
- (m) => m.id == assistantMessageId,
471
- );
472
- if (procIndex != -1) {
473
- final updatedMessages = List<Message>.from(state.messages);
474
- updatedMessages[procIndex] = processingMessage;
475
- state = state.copyWith(messages: updatedMessages);
476
- }
477
- break;
478
- case ChatResponseType.timeoutError:
479
- case ChatResponseType.error:
480
- // Error reached without receiving content or mid-stream
481
- final errorMessage =
482
- (state.streamingMessage ?? assistantMessage).copyWith(
483
- status: MessageStatus.error,
484
- errorMessage:
485
- response.content ??
486
- (response.type == ChatResponseType.timeoutError
487
- ? 'Model is taking too long to respond. This may happen with free tier models.'
488
- : 'An unknown error occurred.'),
489
- isProcessing: false,
526
+ if (msgIndex != -1) {
527
+ final updatedMessages = List<Message>.from(
528
+ state.messages,
529
+ );
530
+ updatedMessages[msgIndex] = streamingAssistantMessage;
531
+ state = state.copyWith(messages: updatedMessages);
532
+ }
533
+ }
534
+ break;
535
+ case ChatResponseType.processing:
536
+ // Model is working but hasn't output content yet
537
+ streamingAssistantMessage = streamingAssistantMessage
538
+ .copyWith(isProcessing: true);
539
+
540
+ if (isCurrentContext) {
541
+ state = state.copyWith(
542
+ streamingMessage: streamingAssistantMessage,
543
);
491
- state = state.copyWith(
492
- streamingMessage: errorMessage,
493
- isStreaming: false,
494
- );
495
- final errIndex = state.messages.indexWhere(
496
- (m) => m.id == assistantMessageId,
497
- );
498
- if (errIndex != -1) {
499
- final updatedMessages = List<Message>.from(state.messages);
500
- updatedMessages[errIndex] = errorMessage;
501
- state = state.copyWith(
502
- messages: updatedMessages,
503
- errorMessage: errorMessage.errorMessage,
504
- clearStreaming: true,
505
- );
506
- }
507
- // Save error message to storage
508
- await boxes.messages.put(errorMessage.id, errorMessage);
509
- break;
510
- case ChatResponseType.toolCall:
511
- case ChatResponseType.invalidToolCall:
512
- case ChatResponseType.done:
513
- break;
514
- }
515
- },
516
- onDone: () async {
517
- final streamingMessage = state.streamingMessage;
518
- if (streamingMessage != null) {
519
- // Check if content is empty - this happens when free models refuse or fail
520
- final hasContent =
521
- streamingMessage.content.isNotEmpty ||
522
- (streamingMessage.reasoningContent?.isNotEmpty ?? false);
523
-
524
- if (!hasContent) {
525
- // Mark as error if no content received
526
- final errorMessage = streamingMessage.copyWith(
527
- status: MessageStatus.error,
528
- errorMessage:
529
- 'Model failed to respond. This may happen with free tier models that refuse certain prompts or when the service is busy.',
530
- isProcessing: false,
531
- );
532
- await boxes.messages.put(errorMessage.id, errorMessage);
533
- final messageIndex = state.messages.indexWhere(
534
- (m) => m.id == assistantMessageId,
535
- );
536
- if (messageIndex != -1) {
537
- final updatedMessages = List<Message>.from(state.messages);
538
- updatedMessages[messageIndex] = errorMessage;
539
- state = state.copyWith(
540
- messages: updatedMessages,
541
- isStreaming: false,
542
- errorMessage: errorMessage.errorMessage,
543
- clearStreaming: true,
544
+ final procIndex = state.messages.indexWhere(
545
+ (m) => m.id == assistantMessageId,
546
+ );
547
+ if (procIndex != -1) {
548
+ final updatedMessages = List<Message>.from(
549
+ state.messages,
550
+ );
551
+ updatedMessages[procIndex] = streamingAssistantMessage;
552
+ state = state.copyWith(messages: updatedMessages);
553
+ }
554
+ }
555
+ break;
556
+ case ChatResponseType.timeoutError:
557
+ case ChatResponseType.error:
558
+ // Error reached without receiving content or mid-stream
559
+ streamingAssistantMessage = streamingAssistantMessage.copyWith(
560
+ status: MessageStatus.error,
561
+ errorMessage:
562
+ response.content ??
563
+ (response.type == ChatResponseType.timeoutError
564
+ ? 'Model is taking too long to respond. This may happen with free tier models.'
565
+ : 'An unknown error occurred.'),
566
+ isProcessing: false,
567
);
545
- }
546
- } else {
547
- // Normal completion with content
548
- final finalMessage = streamingMessage.copyWith(
549
- status: MessageStatus.complete,
550
- isProcessing: false,
551
- );
552
- await boxes.messages.put(finalMessage.id, finalMessage);
553
- final messageIndex = state.messages.indexWhere(
554
- (m) => m.id == assistantMessageId,
555
- );
556
- if (messageIndex != -1) {
557
- final updatedMessages = List<Message>.from(state.messages);
558
- updatedMessages[messageIndex] = finalMessage;
559
- state = state.copyWith(
560
- messages: updatedMessages,
561
- isStreaming: false,
562
- clearStreaming: true,
568
+
569
+ if (isCurrentContext) {
570
+ state = state.copyWith(
571
+ streamingMessage: streamingAssistantMessage,
572
+ isStreaming: false,
573
+ );
574
+ final errIndex = state.messages.indexWhere(
575
+ (m) => m.id == assistantMessageId,
576
+ );
577
+ if (errIndex != -1) {
578
+ final updatedMessages = List<Message>.from(
579
+ state.messages,
580
+ );
581
+ updatedMessages[errIndex] = streamingAssistantMessage;
582
+ state = state.copyWith(
583
+ messages: updatedMessages,
584
+ errorMessage: streamingAssistantMessage.errorMessage,
585
+ clearStreaming: true,
586
+ );
587
+ }
588
+ }
589
+ // Save error message to storage
590
+ await _saveMessage(streamingAssistantMessage);
591
+ break;
592
+ case ChatResponseType.toolCall:
593
+ case ChatResponseType.invalidToolCall:
594
+ case ChatResponseType.done:
595
+ break;
596
+ }
597
+ },
598
+ onDone: () async {
599
+ final streamConvId = assistantMessage.conversationId;
600
+ final isCurrentContext = _currentConversationId == streamConvId;
601
+ final streamingMessage = streamingAssistantMessage;
602
+ // ignore: unnecessary_null_comparison
603
+ if (streamingMessage != null) {
604
+ // Check if content is empty - this happens when free models refuse or fail
605
+ final hasContent =
606
+ streamingMessage.content.isNotEmpty ||
607
+ (streamingMessage.reasoningContent?.isNotEmpty ?? false);
608
+
609
+ if (!hasContent) {
610
+ // Mark as error if no content received
611
+ final errorMessage = streamingMessage.copyWith(
612
+ status: MessageStatus.error,
613
+ errorMessage:
614
+ 'Model failed to respond. This may happen with free tier models that refuse certain prompts or when the service is busy.',
615
+ isProcessing: false,
616
);
564
- }
565
- if (_currentConversationId != null) {
566
- final preview = finalMessage.content.length > 100
567
- ? '${finalMessage.content.substring(0, 100)}...'
568
- : finalMessage.content;
569
- await ref
570
- .read(conv.conversationsProvider.notifier)
571
- .updatePreview(
572
- _currentConversationId!,
573
- preview,
574
- DateTime.now(),
617
+ await _saveMessage(errorMessage);
618
+ if (isCurrentContext) {
619
+ final messageIndex = state.messages.indexWhere(
620
+ (m) => m.id == assistantMessageId,
621
+ );
622
+ if (messageIndex != -1) {
623
+ final updatedMessages = List<Message>.from(
624
+ state.messages,
625
);
576
-
577
- final userMessage = state.messages
578
- .where((m) => m.role == MessageRole.user)
579
- .firstOrNull;
580
- if (userMessage != null &&
581
- userMessage.content.length > 10) {
582
- _autoGenerateTitle(
583
- userMessage.content,
584
- finalMessage.content,
626
+ updatedMessages[messageIndex] = errorMessage;
627
+ state = state.copyWith(
628
+ messages: updatedMessages,
629
+ isStreaming: false,
630
+ errorMessage: errorMessage.errorMessage,
631
+ clearStreaming: true,
632
+ );
633
+ }
634
+ }
635
+ } else {
636
+ // Normal completion with content
637
+ final finalMessage = streamingMessage.copyWith(
638
+ status: MessageStatus.complete,
639
+ isProcessing: false,
640
+ );
641
+ await _saveMessage(finalMessage);
642
+ if (isCurrentContext) {
643
+ final messageIndex = state.messages.indexWhere(
644
+ (m) => m.id == assistantMessageId,
645
);
646
+ if (messageIndex != -1) {
647
+ final updatedMessages = List<Message>.from(
648
+ state.messages,
649
+ );
650
+ updatedMessages[messageIndex] = finalMessage;
651
+ state = state.copyWith(
652
+ messages: updatedMessages,
653
+ isStreaming: false,
654
+ clearStreaming: true,
655
+ );
656
+ }
657
+ }
658
+ if (_currentConversationId != null) {
659
+ final preview = finalMessage.content.length > 100
660
+ ? '${finalMessage.content.substring(0, 100)}...'
661
+ : finalMessage.content;
662
+ await ref
663
+ .read(conv.conversationsProvider.notifier)
664
+ .updatePreview(
665
+ _currentConversationId!,
666
+ preview,
667
+ DateTime.now(),
668
+ );
669
+
670
+ final userMessage = state.messages
671
+ .where((m) => m.role == MessageRole.user)
672
+ .firstOrNull;
673
+ if (userMessage != null &&
674
+ userMessage.content.length > 10) {
675
+ _autoGenerateTitle(
676
+ userMessage.content,
677
+ finalMessage.content,
678
+ );
679
+ }
680
}
681
}
682
}
589
- }
590
- },
591
- onError: (error) async {
592
- final errorMessage = state.streamingMessage?.copyWith(
593
- status: MessageStatus.error,
594
- errorMessage: error.toString(),
595
- );
596
- if (errorMessage != null) {
597
- await boxes.messages.put(errorMessage.id, errorMessage);
598
- final messageIndex = state.messages.indexWhere(
599
- (m) => m.id == assistantMessageId,
683
+ },
684
+ onError: (error) async {
685
+ final errorMessage = state.streamingMessage?.copyWith(
686
+ status: MessageStatus.error,
687
+ errorMessage: error.toString(),
688
);
601
- if (messageIndex != -1) {
602
- final updatedMessages = List<Message>.from(state.messages);
603
- updatedMessages[messageIndex] = errorMessage;
604
- state = state.copyWith(
605
- messages: updatedMessages,
606
- isStreaming: false,
607
- errorMessage: error.toString(),
608
- clearStreaming: true,
609
- );
689
+ if (errorMessage != null) {
690
+ await _saveMessage(errorMessage);
691
+
692
+ final streamConvId = assistantMessage.conversationId;
693
+ if (_currentConversationId == streamConvId) {
694
+ final messageIndex = state.messages.indexWhere(
695
+ (m) => m.id == assistantMessageId,
696
+ );
697
+ if (messageIndex != -1) {
698
+ final updatedMessages = List<Message>.from(
699
+ state.messages,
700
+ );
701
+ updatedMessages[messageIndex] = errorMessage;
702
+ state = state.copyWith(
703
+ messages: updatedMessages,
704
+ isStreaming: false,
705
+ errorMessage: error.toString(),
706
+ clearStreaming: true,
707
+ );
708
+ }
709
+ }
710
}
611
- }
612
- },
613
- );
711
+ },
712
+ );
713
+ }
714
} catch (e) {
715
+ final errorMsg = assistantMessage.copyWith(
716
+ status: MessageStatus.error,
717
+ errorMessage: e.toString(),
718
+ isProcessing: false,
719
+ );
720
+ await _saveMessage(errorMsg);
721
+
722
state = state.copyWith(
723
isStreaming: false,
724
errorMessage: e.toString(),
@@ -671,14 +778,13 @@ class ChatNotifier extends Notifier<ChatState> {
778
}
779
780
try {
674
- final boxes = ref.read(hiveBoxesProvider);
675
- final persona = boxes.personas.values.cast<dynamic>().firstWhere(
781
+ final personasAsync = ref.read(personasNotifierProvider);
782
+ final personas = personasAsync.value ?? [];
783
+ final persona = personas.firstWhere(
784
(p) => p.id == personaId,
677
- orElse: () => null,
785
+ orElse: () => throw Exception('Persona not found'),
786
);
679
- if (persona != null &&
680
- persona.systemPrompt != null &&
681
- persona.systemPrompt.isNotEmpty) {
787
+ if (persona.systemPrompt.isNotEmpty) {
788
return persona.systemPrompt;
789
}
790
} catch (_) {}
@@ -730,16 +836,15 @@ class ChatNotifier extends Notifier<ChatState> {
836
Future<void> cancelStream() async {
837
_streamSubscription?.cancel();
838
_streamSubscription = null;
733
- ref.read(chatServiceProvider).cancelStream();
839
+ ref.read(chatServiceProvider)?.cancelStream();
840
841
final streamingMessage = state.streamingMessage;
842
if (streamingMessage != null) {
737
- final boxes = ref.read(hiveBoxesProvider);
843
final finalMessage = streamingMessage.copyWith(
844
status: MessageStatus.complete,
845
isProcessing: false,
846
);
742
- await boxes.messages.put(finalMessage.id, finalMessage);
847
+ await _saveMessage(finalMessage);
848
}
849
850
state = state.copyWith(isStreaming: false, clearStreaming: true);
@@ -756,10 +861,14 @@ class ChatNotifier extends Notifier<ChatState> {
861
if (lastAssistantIndex > 0) {
862
final userMessage = messages[lastAssistantIndex - 1];
863
final messagesToRemove = messages.sublist(lastAssistantIndex);
759
- final boxes = ref.read(hiveBoxesProvider);
864
+ final db = ref.read(databaseProvider);
865
866
for (final msg in messagesToRemove) {
762
- await boxes.messages.delete(msg.id);
867
+ final query = db.messageBox
868
+ .query(MessageEntity_.id.equals(msg.id))
869
+ .build();
870
+ db.messageBox.removeMany(query.findIds());
871
+ query.close();
872
}
873
874
state = state.copyWith(
@@ -767,7 +876,12 @@ class ChatNotifier extends Notifier<ChatState> {
876
clearStreaming: true,
877
);
878
770
- await sendMessage(userMessage.content, attachments: userMessage.attachmentPaths?.map((p) => File(p)).toList());
879
+ await sendMessage(
880
+ userMessage.content,
881
+ attachments: userMessage.attachmentPaths
882
+ ?.map((p) => File(p))
883
+ .toList(),
884
+ );
885
}
886
}
887
}
@@ -786,10 +900,14 @@ class ChatNotifier extends Notifier<ChatState> {
900
901
// Remove this assistant message and all subsequent messages
902
final messagesToRemove = state.messages.sublist(messageIndex);
789
- final boxes = ref.read(hiveBoxesProvider);
903
+ final db = ref.read(databaseProvider);
904
905
for (final msg in messagesToRemove) {
792
- await boxes.messages.delete(msg.id);
906
+ final query = db.messageBox
907
+ .query(MessageEntity_.id.equals(msg.id))
908
+ .build();
909
+ db.messageBox.removeMany(query.findIds());
910
+ query.close();
911
}
912
913
state = state.copyWith(
@@ -797,22 +915,66 @@ class ChatNotifier extends Notifier<ChatState> {
915
clearStreaming: true,
916
);
917
800
- await sendMessage(userMessage.content, attachments: userMessage.attachmentPaths?.map((p) => File(p)).toList());
918
+ await sendMessage(
919
+ userMessage.content,
920
+ attachments: userMessage.attachmentPaths?.map((p) => File(p)).toList(),
921
+ );
922
}
923
}
924
925
Future<void> deleteMessage(String messageId) async {
805
- final boxes = ref.read(hiveBoxesProvider);
806
- await boxes.messages.delete(messageId);
926
+ final db = ref.read(databaseProvider);
927
+ final query = db.messageBox
928
+ .query(MessageEntity_.id.equals(messageId))
929
+ .build();
930
+ db.messageBox.removeMany(query.findIds());
931
+ query.close();
932
+
933
state = state.copyWith(
934
messages: state.messages.where((m) => m.id != messageId).toList(),
935
);
936
}
937
938
+ Future<void> _saveMessage(Message message) async {
939
+ final db = ref.read(databaseProvider);
940
+ final query = db.messageBox
941
+ .query(MessageEntity_.id.equals(message.id))
942
+ .build();
943
+ final existing = query.findFirst();
944
+ query.close();
945
+
946
+ final entity = MessageEntity.fromDomain(message);
947
+ if (existing != null) {
948
+ entity.internalId = existing.internalId;
949
+ }
950
+
951
+ // Set relation
952
+ final convQuery = db.conversationBox
953
+ .query(ConversationEntity_.id.equals(message.conversationId))
954
+ .build();
955
+ final convEntity = convQuery.findFirst();
956
+ convQuery.close();
957
+
958
+ if (convEntity != null) {
959
+ entity.conversation.target = convEntity;
960
+ // Also ensure the string UID is set correctly
961
+ entity.conversationUid = convEntity.id;
962
+ } else {
963
+ // Fallback: search by UID if the entity was just created but not found by ID (unlikely but safe)
964
+ entity.conversationUid = message.conversationId;
965
+ }
966
+
967
+ db.messageBox.put(entity);
968
+ }
969
+
970
Future<void> clearConversation() async {
813
- final boxes = ref.read(hiveBoxesProvider);
814
- for (final message in state.messages) {
815
- await boxes.messages.delete(message.id);
971
+ final db = ref.read(databaseProvider);
972
+ if (_currentConversationId != null) {
973
+ final query = db.messageBox
974
+ .query(MessageEntity_.conversationUid.equals(_currentConversationId!))
975
+ .build();
976
+ db.messageBox.removeMany(query.findIds());
977
+ query.close();
978
}
979
_currentConversationId = null;
980
state = const ChatState();
lib/features/chat/views/chat_screen.dart
+119
-51
@@ -3,18 +3,18 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
3
import 'package:go_router/go_router.dart';
4
import 'package:shadcn_ui/shadcn_ui.dart';
5
import 'package:hugeicons/hugeicons.dart';
6
-import 'package:localmind/features/servers/views/components/server_icon_picker.dart';
7
-import 'package:localmind/features/servers/data/models/server.dart';
8
-import 'package:localmind/core/models/enums.dart';
9
-import 'package:localmind/core/providers/app_providers.dart';
10
-import 'package:localmind/core/providers/storage_providers.dart';
11
-import 'package:localmind/core/routes/app_routes.dart';
12
-import 'package:localmind/features/chat/data/models/message.dart';
13
-import 'package:localmind/features/chat/providers/chat_providers.dart';
14
-import 'package:localmind/features/chat/views/components/chat_bubble.dart';
15
-import 'package:localmind/features/chat/views/components/chat_input_bar.dart';
16
-import 'package:localmind/features/chat/views/components/mcp_config_sheet.dart';
17
-import 'package:localmind/features/conversations/data/models/conversation.dart';
6
+import '../../servers/views/components/server_icon_picker.dart';
7
+import '../../servers/data/models/server.dart';
8
+import '../../../core/models/enums.dart';
9
+import '../../../core/providers/app_providers.dart';
10
+
11
+import '../../../core/routes/app_routes.dart';
12
+import '../data/models/message.dart';
13
+import '../providers/chat_providers.dart';
14
+import 'components/chat_bubble.dart';
15
+import 'components/chat_input_bar.dart';
16
+import 'components/mcp_config_sheet.dart';
17
+import '../../conversations/data/models/conversation.dart';
18
import 'package:localmind/features/conversations/providers/conversation_providers.dart'
19
as conv;
20
import 'package:localmind/features/models/screens/model_picker_sheet.dart';
@@ -212,42 +212,48 @@ class _ChatScreenState extends ConsumerState<ChatScreen> {
212
Expanded(
213
child: Stack(
214
children: [
215
- chatState.messages.isEmpty
216
- ? _EmptyState(
217
- onQuickPrompt: (prompt) =>
218
- ref.read(chatProvider.notifier).sendMessage(prompt),
219
- quickPrompts: _quickPrompts,
220
- recentConversations: ref.watch(
221
- conv.recentConversationsProvider,
222
- ),
223
- onSeeAll: () {
224
- if (Scaffold.maybeOf(context)?.hasDrawer ?? false) {
225
- Scaffold.of(context).openDrawer();
226
- }
227
- },
228
- selectedModel: selectedModel,
229
- onModelTap: () => _showModelPicker(context),
230
- selectedPersona: ref.watch(selectedPersonaProvider),
231
- onPersonaTap: () =>
232
- _showPersonaPickerForPreselection(context),
233
- )
234
- : _MessageList(
235
- scrollController: _scrollController,
236
- messages: chatState.messages,
237
- streamingMessage: chatState.streamingMessage,
238
- isStreaming: chatState.isStreaming,
239
- onRetry: (messageId) {
240
- ref.read(chatProvider.notifier).retryMessage(messageId);
241
- },
242
- onDelete: (messageId) {
243
- ref
244
- .read(chatProvider.notifier)
245
- .deleteMessage(messageId);
246
- },
247
- hasSmartReplies:
248
- !chatState.isStreaming &&
249
- ref.read(smartRepliesProvider).isNotEmpty,
250
- ),
215
+ if (chatState.isLoading)
216
+ const Center(child: CircularProgressIndicator(strokeWidth: 2))
217
+ else if (chatState.messages.isEmpty && activeConversation != null)
218
+ _CorruptedChatState(
219
+ onStartNewChat: () =>
220
+ ref.read(chatProvider.notifier).startNewConversation(),
221
+ )
222
+ else if (chatState.messages.isEmpty)
223
+ _EmptyState(
224
+ onQuickPrompt: (prompt) =>
225
+ ref.read(chatProvider.notifier).sendMessage(prompt),
226
+ quickPrompts: _quickPrompts,
227
+ recentConversations: ref.watch(
228
+ conv.recentConversationsProvider,
229
+ ),
230
+ onSeeAll: () {
231
+ if (Scaffold.maybeOf(context)?.hasDrawer ?? false) {
232
+ Scaffold.of(context).openDrawer();
233
+ }
234
+ },
235
+ selectedModel: selectedModel,
236
+ onModelTap: () => _showModelPicker(context),
237
+ selectedPersona: ref.watch(selectedPersonaProvider),
238
+ onPersonaTap: () =>
239
+ _showPersonaPickerForPreselection(context),
240
+ )
241
+ else
242
+ _MessageList(
243
+ scrollController: _scrollController,
244
+ messages: chatState.messages,
245
+ streamingMessage: chatState.streamingMessage,
246
+ isStreaming: chatState.isStreaming,
247
+ onRetry: (messageId) {
248
+ ref.read(chatProvider.notifier).retryMessage(messageId);
249
+ },
250
+ onDelete: (messageId) {
251
+ ref.read(chatProvider.notifier).deleteMessage(messageId);
252
+ },
253
+ hasSmartReplies:
254
+ !chatState.isStreaming &&
255
+ ref.read(smartRepliesProvider).isNotEmpty,
256
+ ),
257
if (!chatState.isStreaming)
258
Positioned(
259
bottom: 90,
@@ -335,7 +341,8 @@ class _ChatScreenState extends ConsumerState<ChatScreen> {
341
}
342
343
void _showPersonaPicker(BuildContext context) {
338
- final personas = ref.read(personasNotifierProvider);
344
+ final personasAsync = ref.read(personasNotifierProvider);
345
+ final personas = personasAsync.value ?? [];
346
final isDark = Theme.of(context).brightness == Brightness.dark;
347
final activeConv = ref.read(conv.activeConversationProvider);
348
final currentPersonaId = activeConv?.personaId;
@@ -435,7 +442,8 @@ class _ChatScreenState extends ConsumerState<ChatScreen> {
442
}
443
444
void _showPersonaPickerForPreselection(BuildContext context) {
438
- final personas = ref.read(personasNotifierProvider);
445
+ final personasAsync = ref.read(personasNotifierProvider);
446
+ final personas = personasAsync.value ?? [];
447
final isDark = Theme.of(context).brightness == Brightness.dark;
448
final currentPersona = ref.watch(selectedPersonaProvider);
449
@@ -778,7 +786,8 @@ class _EmptyStateState extends State<_EmptyState>
786
const SizedBox(width: 8),
787
Flexible(
788
child: Text(
781
- widget.selectedModel?.displayName ?? 'Select Model',
789
+ widget.selectedModel?.displayName ??
790
+ 'Select Model',
791
style: TextStyle(
792
fontSize: 15,
793
fontWeight: FontWeight.w600,
@@ -1358,3 +1367,62 @@ class _PersonaIndicator extends StatelessWidget {
1367
);
1368
}
1369
}
1370
+
1371
+class _CorruptedChatState extends StatelessWidget {
1372
+ const _CorruptedChatState({required this.onStartNewChat});
1373
+
1374
+ final VoidCallback onStartNewChat;
1375
+
1376
+ @override
1377
+ Widget build(BuildContext context) {
1378
+ final theme = Theme.of(context);
1379
+ final isDark = theme.brightness == Brightness.dark;
1380
+
1381
+ return Center(
1382
+ child: Padding(
1383
+ padding: const EdgeInsets.all(32.0),
1384
+ child: Column(
1385
+ mainAxisAlignment: MainAxisAlignment.center,
1386
+ children: [
1387
+ Container(
1388
+ padding: const EdgeInsets.all(20),
1389
+ decoration: BoxDecoration(
1390
+ color: Colors.red.withValues(alpha: 0.1),
1391
+ shape: BoxShape.circle,
1392
+ ),
1393
+ child: const Icon(
1394
+ Icons.history_toggle_off_rounded,
1395
+ size: 48,
1396
+ color: Colors.red,
1397
+ ),
1398
+ ),
1399
+ const SizedBox(height: 24),
1400
+ Text(
1401
+ 'History Missing',
1402
+ style: TextStyle(
1403
+ fontSize: 20,
1404
+ fontWeight: FontWeight.bold,
1405
+ color: isDark ? Colors.white : Colors.black,
1406
+ ),
1407
+ ),
1408
+ const SizedBox(height: 12),
1409
+ Text(
1410
+ 'Either the messages in this chat were deleted or the history record is corrupted.',
1411
+ textAlign: TextAlign.center,
1412
+ style: TextStyle(
1413
+ fontSize: 15,
1414
+ color: isDark ? Colors.white60 : Colors.black54,
1415
+ ),
1416
+ ),
1417
+ const SizedBox(height: 32),
1418
+ ShadButton(
1419
+ onPressed: onStartNewChat,
1420
+ leading: const Icon(Icons.add_rounded, size: 20),
1421
+ child: const Text('Start New Chat'),
1422
+ ),
1423
+ ],
1424
+ ),
1425
+ ),
1426
+ );
1427
+ }
1428
+}
lib/features/chat/views/components/chat_bubble.dart
+10
-11
@@ -1,13 +1,13 @@
1
import 'dart:io';
2
import 'package:flutter/material.dart';
3
import 'package:flutter_markdown_plus/flutter_markdown_plus.dart';
4
-import 'package:localmind/core/models/enums.dart';
5
-import 'package:localmind/features/chat/data/models/message.dart';
6
-import 'package:localmind/features/chat/views/components/code_block.dart';
7
-import 'package:localmind/features/chat/views/components/message_action_bar.dart';
8
-import 'package:localmind/features/chat/views/components/processing_indicator.dart';
9
-import 'package:localmind/features/chat/views/components/typing_indicator.dart';
10
-import 'package:localmind/features/chat/views/components/reasoning_widget.dart';
4
+import '../../../../core/models/enums.dart';
5
+import '../../data/models/message.dart';
6
+import 'code_block.dart';
7
+import 'message_action_bar.dart';
8
+import 'processing_indicator.dart';
9
+import 'typing_indicator.dart';
10
+import 'reasoning_widget.dart';
11
12
class ChatBubble extends StatelessWidget {
13
const ChatBubble({
@@ -27,9 +27,7 @@ class ChatBubble extends StatelessWidget {
27
28
@override
29
Widget build(BuildContext context) {
30
- return RepaintBoundary(
31
- child: _buildBubble(context),
32
- );
30
+ return RepaintBoundary(child: _buildBubble(context));
31
}
32
33
Widget _buildBubble(BuildContext context) {
@@ -526,7 +524,8 @@ class _AttachmentItem extends StatelessWidget {
524
width: 120,
525
height: 120,
526
fit: BoxFit.cover,
529
- errorBuilder: (context, error, stackTrace) => _FilePlaceholder(fileName: fileName),
527
+ errorBuilder: (context, error, stackTrace) =>
528
+ _FilePlaceholder(fileName: fileName),
529
),
530
),
531
);
lib/features/chat/views/components/chat_input_bar.dart
+2
-2
@@ -4,8 +4,8 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
4
import 'package:haptic_feedback/haptic_feedback.dart';
5
import 'package:file_picker/file_picker.dart';
6
import 'package:hugeicons/hugeicons.dart';
7
-import 'package:localmind/core/models/enums.dart';
8
-import 'package:localmind/features/servers/providers/server_providers.dart';
7
+import '../../../../core/models/enums.dart';
8
+import '../../../servers/providers/server_providers.dart';
9
10
class ChatInputBar extends ConsumerStatefulWidget {
11
const ChatInputBar({
lib/features/chat/views/components/code_block.dart
+2
-2
@@ -2,8 +2,8 @@ import 'package:flutter/material.dart';
2
import 'package:flutter/services.dart';
3
import 'package:flutter_riverpod/flutter_riverpod.dart';
4
import 'package:flutter_syntax_view/flutter_syntax_view.dart';
5
-import 'package:localmind/core/providers/app_providers.dart';
6
-import 'package:localmind/features/settings/data/models/app_settings.dart';
5
+import '../../../../core/providers/app_providers.dart';
6
+import '../../../settings/data/models/app_settings.dart';
7
8
class CodeBlock extends ConsumerWidget {
9
const CodeBlock({super.key, required this.code, this.language});
lib/features/chat/views/components/mcp_config_sheet.dart
+2
-2
@@ -1,7 +1,7 @@
1
import 'package:flutter/material.dart';
2
import 'package:flutter_riverpod/flutter_riverpod.dart';
3
-import 'package:localmind/core/providers/app_providers.dart';
4
-import 'package:localmind/features/chat/data/models/mcp_integration.dart';
3
+import '../../../../core/providers/app_providers.dart';
4
+import '../../data/models/mcp_integration.dart';
5
6
class ChatMcpConfigNotifier extends Notifier<ChatMcpConfig> {
7
@override
lib/features/chat/views/components/message_action_bar.dart
+1
-1
@@ -1,7 +1,7 @@
1
import 'package:flutter/material.dart';
2
import 'package:flutter/services.dart';
3
import 'package:shadcn_ui/shadcn_ui.dart';
4
-import 'package:localmind/features/chat/data/tts_service.dart';
4
+import '../../data/tts_service.dart';
5
6
class MessageActionBar extends StatefulWidget {
7
const MessageActionBar({
lib/features/conversations/data/models/conversation.dart
+1
-27
@@ -1,40 +1,14 @@
1
-import 'package:hive_ce/hive.dart';
2
-
3
-part 'conversation.g.dart';
4
-
5
-@HiveType(typeId: 2)
6
-class Conversation extends HiveObject {
7
- @HiveField(0)
1
+class Conversation {
2
final String id;
9
-
10
- @HiveField(1)
3
final String title;
12
-
13
- @HiveField(2)
4
final DateTime createdAt;
15
-
16
- @HiveField(3)
5
final DateTime updatedAt;
18
-
19
- @HiveField(4)
6
final bool isPinned;
21
-
22
- @HiveField(5)
7
final String? personaId;
24
-
25
- @HiveField(6)
8
final String? serverId;
27
-
28
- @HiveField(7)
9
final String? modelId;
30
-
31
- @HiveField(8)
10
final int messageCount;
33
-
34
- @HiveField(9)
11
final String? lastMessagePreview;
36
-
37
- @HiveField(10)
12
final String? systemPrompt;
13
14
Conversation({
lib/features/conversations/data/models/conversation.g.dart
deleted
-71
@@ -1,71 +0,0 @@
1
-// GENERATED CODE - DO NOT MODIFY BY HAND
2
-
3
-part of 'conversation.dart';
4
-
5
-// **************************************************************************
6
-// TypeAdapterGenerator
7
-// **************************************************************************
8
-
9
-class ConversationAdapter extends TypeAdapter<Conversation> {
10
- @override
11
- final typeId = 2;
12
-
13
- @override
14
- Conversation read(BinaryReader reader) {
15
- final numOfFields = reader.readByte();
16
- final fields = <int, dynamic>{
17
- for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
18
- };
19
- return Conversation(
20
- id: fields[0] as String,
21
- title: fields[1] as String,
22
- createdAt: fields[2] as DateTime,
23
- updatedAt: fields[3] as DateTime,
24
- isPinned: fields[4] == null ? false : fields[4] as bool,
25
- personaId: fields[5] as String?,
26
- serverId: fields[6] as String?,
27
- modelId: fields[7] as String?,
28
- messageCount: fields[8] == null ? 0 : (fields[8] as num).toInt(),
29
- lastMessagePreview: fields[9] as String?,
30
- systemPrompt: fields[10] as String?,
31
- );
32
- }
33
-
34
- @override
35
- void write(BinaryWriter writer, Conversation obj) {
36
- writer
37
- ..writeByte(11)
38
- ..writeByte(0)
39
- ..write(obj.id)
40
- ..writeByte(1)
41
- ..write(obj.title)
42
- ..writeByte(2)
43
- ..write(obj.createdAt)
44
- ..writeByte(3)
45
- ..write(obj.updatedAt)
46
- ..writeByte(4)
47
- ..write(obj.isPinned)
48
- ..writeByte(5)
49
- ..write(obj.personaId)
50
- ..writeByte(6)
51
- ..write(obj.serverId)
52
- ..writeByte(7)
53
- ..write(obj.modelId)
54
- ..writeByte(8)
55
- ..write(obj.messageCount)
56
- ..writeByte(9)
57
- ..write(obj.lastMessagePreview)
58
- ..writeByte(10)
59
- ..write(obj.systemPrompt);
60
- }
61
-
62
- @override
63
- int get hashCode => typeId.hashCode;
64
-
65
- @override
66
- bool operator ==(Object other) =>
67
- identical(this, other) ||
68
- other is ConversationAdapter &&
69
- runtimeType == other.runtimeType &&
70
- typeId == other.typeId;
71
-}
lib/features/conversations/providers/conversation_providers.dart
+183
-131
@@ -1,17 +1,24 @@
1
import 'package:flutter_riverpod/flutter_riverpod.dart';
2
-import 'package:localmind/core/providers/storage_providers.dart';
3
-import 'package:localmind/features/conversations/data/models/conversation.dart';
2
+import '../../../core/providers/storage_providers.dart';
3
+import '../../../core/storage/entities.dart';
4
+import '../data/models/conversation.dart';
5
+import '../../../objectbox.g.dart';
6
7
final conversationsProvider =
6
- NotifierProvider<ConversationsNotifier, List<Conversation>>(() {
8
+ AsyncNotifierProvider<ConversationsNotifier, List<Conversation>>(() {
9
return ConversationsNotifier();
10
});
11
10
-class ConversationsNotifier extends Notifier<List<Conversation>> {
12
+class ConversationsNotifier extends AsyncNotifier<List<Conversation>> {
13
@override
12
- List<Conversation> build() {
13
- final box = ref.watch(conversationsBoxProvider);
14
- final conversations = box.values.toList();
14
+ Future<List<Conversation>> build() async {
15
+ return _loadAll();
16
+ }
17
+
18
+ Future<List<Conversation>> _loadAll() async {
19
+ final db = ref.read(databaseProvider);
20
+ final entities = db.conversationBox.getAll();
21
+ final conversations = entities.map((e) => e.toDomain()).toList();
22
_sortConversations(conversations);
23
return conversations;
24
}
@@ -32,7 +39,7 @@ class ConversationsNotifier extends Notifier<List<Conversation>> {
39
String? serverId,
40
String? modelId,
41
}) async {
35
- final box = ref.read(conversationsBoxProvider);
42
+ final db = ref.read(databaseProvider);
43
final now = DateTime.now();
44
final id = _generateUuid();
45
@@ -50,45 +57,85 @@ class ConversationsNotifier extends Notifier<List<Conversation>> {
57
systemPrompt: systemPrompt,
58
);
59
53
- await box.put(id, conversation);
54
- state = [conversation, ...state];
60
+ db.conversationBox.put(ConversationEntity.fromDomain(conversation));
61
+ state = AsyncData(await _loadAll());
62
return conversation;
63
}
64
65
Future<void> renameConversation(String id, String newTitle) async {
59
- final box = ref.read(conversationsBoxProvider);
60
- final conversation = box.get(id);
61
- if (conversation != null) {
62
- final updated = conversation.copyWith(
63
- title: newTitle,
64
- updatedAt: DateTime.now(),
65
- );
66
- await box.put(id, updated);
67
- state = [
68
- for (final c in state)
69
- if (c.id == id) updated else c,
70
- ];
66
+ final db = ref.read(databaseProvider);
67
+ final conversations = state.value ?? [];
68
+ final existing = conversations.firstWhere(
69
+ (c) => c.id == id,
70
+ orElse: () => throw Exception('Conversation not found in state'),
71
+ );
72
+
73
+ final updated = existing.copyWith(
74
+ title: newTitle,
75
+ updatedAt: DateTime.now(),
76
+ );
77
+
78
+ final query = db.conversationBox
79
+ .query(ConversationEntity_.id.equals(id))
80
+ .build();
81
+ final existingEntity = query.findFirst();
82
+ query.close();
83
+
84
+ final entity = ConversationEntity.fromDomain(updated);
85
+ if (existingEntity != null) {
86
+ entity.internalId = existingEntity.internalId;
87
}
88
+ db.conversationBox.put(entity);
89
+
90
+ state = AsyncData(await _loadAll());
91
}
92
93
Future<void> deleteConversation(String id) async {
75
- final box = ref.read(conversationsBoxProvider);
76
- await box.delete(id);
77
- state = state.where((c) => c.id != id).toList();
94
+ final db = ref.read(databaseProvider);
95
+
96
+ // Delete messages first
97
+ final msgQuery = db.messageBox
98
+ .query(MessageEntity_.conversationUid.equals(id))
99
+ .build();
100
+ db.messageBox.removeMany(msgQuery.findIds());
101
+ msgQuery.close();
102
+
103
+ // Delete conversation
104
+ final convQuery = db.conversationBox
105
+ .query(ConversationEntity_.id.equals(id))
106
+ .build();
107
+ db.conversationBox.removeMany(convQuery.findIds());
108
+ convQuery.close();
109
+
110
+ state = AsyncData(await _loadAll());
111
}
112
113
Future<void> togglePin(String id) async {
81
- final box = ref.read(conversationsBoxProvider);
82
- final conversation = box.get(id);
83
- if (conversation != null) {
84
- final updated = conversation.copyWith(
85
- isPinned: !conversation.isPinned,
86
- updatedAt: DateTime.now(),
87
- );
88
- await box.put(id, updated);
89
- _sortConversations(state);
90
- state = [for (final c in state) c.id == id ? updated : c];
114
+ final db = ref.read(databaseProvider);
115
+ final conversations = state.value ?? [];
116
+ final existing = conversations.firstWhere(
117
+ (c) => c.id == id,
118
+ orElse: () => throw Exception('Conversation not found in state'),
119
+ );
120
+
121
+ final updated = existing.copyWith(
122
+ isPinned: !existing.isPinned,
123
+ updatedAt: DateTime.now(),
124
+ );
125
+
126
+ final query = db.conversationBox
127
+ .query(ConversationEntity_.id.equals(id))
128
+ .build();
129
+ final existingEntity = query.findFirst();
130
+ query.close();
131
+
132
+ final entity = ConversationEntity.fromDomain(updated);
133
+ if (existingEntity != null) {
134
+ entity.internalId = existingEntity.internalId;
135
}
136
+ db.conversationBox.put(entity);
137
+
138
+ state = AsyncData(await _loadAll());
139
}
140
141
Future<void> updatePreview(
@@ -97,21 +144,30 @@ class ConversationsNotifier extends Notifier<List<Conversation>> {
144
DateTime updatedAt, {
145
int? messageCount,
146
}) async {
100
- final box = ref.read(conversationsBoxProvider);
101
- final conversation = box.get(id);
102
- if (conversation != null) {
103
- final updated = conversation.copyWith(
147
+ final db = ref.read(databaseProvider);
148
+ final conversations = state.value ?? [];
149
+ final existingIndex = conversations.indexWhere((c) => c.id == id);
150
+ if (existingIndex != -1) {
151
+ final existing = conversations[existingIndex];
152
+ final updated = existing.copyWith(
153
lastMessagePreview: preview,
154
updatedAt: updatedAt,
106
- messageCount: messageCount ?? conversation.messageCount + 1,
155
+ messageCount: messageCount ?? existing.messageCount + 1,
156
);
108
- await box.put(id, updated);
109
- state = [
110
- for (final c in state)
111
- if (c.id == id) updated else c,
112
- ];
113
- _sortConversations(state);
114
- state = List.from(state);
157
+
158
+ final query = db.conversationBox
159
+ .query(ConversationEntity_.id.equals(id))
160
+ .build();
161
+ final existingEntity = query.findFirst();
162
+ query.close();
163
+
164
+ final entity = ConversationEntity.fromDomain(updated);
165
+ if (existingEntity != null) {
166
+ entity.internalId = existingEntity.internalId;
167
+ }
168
+ db.conversationBox.put(entity);
169
+
170
+ state = AsyncData(await _loadAll());
171
}
172
}
173
@@ -120,28 +176,40 @@ class ConversationsNotifier extends Notifier<List<Conversation>> {
176
String? personaId,
177
String? systemPrompt,
178
) async {
123
- final box = ref.read(conversationsBoxProvider);
124
- final conversation = box.get(id);
125
- if (conversation != null) {
126
- final updated = conversation.copyWith(
179
+ final db = ref.read(databaseProvider);
180
+ final conversations = state.value ?? [];
181
+ final existingIndex = conversations.indexWhere((c) => c.id == id);
182
+ if (existingIndex != -1) {
183
+ final existing = conversations[existingIndex];
184
+ final updated = existing.copyWith(
185
personaId: personaId,
186
clearPersona: personaId == null,
187
systemPrompt: systemPrompt,
188
clearSystemPrompt: systemPrompt == null,
189
updatedAt: DateTime.now(),
190
);
133
- await box.put(id, updated);
134
- state = [
135
- for (final c in state)
136
- if (c.id == id) updated else c,
137
- ];
191
+
192
+ final query = db.conversationBox
193
+ .query(ConversationEntity_.id.equals(id))
194
+ .build();
195
+ final existingEntity = query.findFirst();
196
+ query.close();
197
+
198
+ final entity = ConversationEntity.fromDomain(updated);
199
+ if (existingEntity != null) {
200
+ entity.internalId = existingEntity.internalId;
201
+ }
202
+ db.conversationBox.put(entity);
203
+
204
+ state = AsyncData(await _loadAll());
205
}
206
}
207
208
Future<void> deleteAll() async {
142
- final box = ref.read(conversationsBoxProvider);
143
- await box.clear();
144
- state = [];
209
+ final db = ref.read(databaseProvider);
210
+ db.messageBox.removeAll();
211
+ db.conversationBox.removeAll();
212
+ state = AsyncData(await _loadAll());
213
}
214
215
String _generateUuid() {
@@ -160,43 +228,19 @@ class ActiveConversationNotifier extends Notifier<Conversation?> {
228
229
@override
230
Conversation? build() {
163
- final conversations = ref.watch(conversationsProvider);
164
- if (conversations.isEmpty) {
165
- _activeConversationId = null;
166
- return null;
167
- }
231
+ final conversationsAsync = ref.watch(conversationsProvider);
232
+ final conversations = conversationsAsync.value ?? [];
233
169
- // Attempt to maintain the currently active conversation
234
if (_activeConversationId != null) {
171
- final conversation = conversations
235
+ return conversations
236
.where((c) => c.id == _activeConversationId)
237
.firstOrNull;
174
- if (conversation != null) {
175
- return conversation;
176
- }
238
}
178
-
179
- // Default to the first available conversation if none active or active one was deleted
180
- final first = conversations.first;
181
- _activeConversationId = first.id;
182
- return first;
239
+ return null;
240
}
241
242
void setActiveConversation(Conversation? conversation) {
243
_activeConversationId = conversation?.id;
187
- if (conversation != null) {
188
- final conversations = ref.read(conversationsProvider);
189
- final index = conversations.indexWhere((c) => c.id == conversation.id);
190
- if (index > 0) {
191
- ref
192
- .read(conversationsProvider.notifier)
193
- .updatePreview(
194
- conversation.id,
195
- conversation.lastMessagePreview ?? '',
196
- DateTime.now(),
197
- );
198
- }
199
- }
244
state = conversation;
245
}
246
}
@@ -219,57 +263,65 @@ class ConversationSearchNotifier extends Notifier<String> {
263
}
264
}
265
222
-final filteredConversationsProvider = Provider<List<Conversation>>((ref) {
223
- final conversations = ref.watch(conversationsProvider);
266
+final filteredConversationsProvider = Provider<AsyncValue<List<Conversation>>>((
267
+ ref,
268
+) {
269
+ final conversationsAsync = ref.watch(conversationsProvider);
270
final query = ref.watch(conversationSearchProvider).toLowerCase();
225
- if (query.isEmpty) return conversations;
226
- return conversations.where((c) {
227
- return c.title.toLowerCase().contains(query) ||
228
- (c.lastMessagePreview?.toLowerCase().contains(query) ?? false);
229
- }).toList();
271
+
272
+ return conversationsAsync.whenData((conversations) {
273
+ if (query.isEmpty) return conversations;
274
+ return conversations.where((c) {
275
+ return c.title.toLowerCase().contains(query) ||
276
+ (c.lastMessagePreview?.toLowerCase().contains(query) ?? false);
277
+ }).toList();
278
+ });
279
});
280
281
final recentConversationsProvider = Provider<List<Conversation>>((ref) {
233
- final all = ref.watch(conversationsProvider);
282
+ final allAsync = ref.watch(conversationsProvider);
283
+ final all = allAsync.value ?? [];
284
return all.take(3).toList();
285
});
286
237
-final groupedConversationsProvider = Provider<Map<String, List<Conversation>>>((
238
- ref,
239
-) {
240
- final conversations = ref.watch(filteredConversationsProvider);
241
- final now = DateTime.now();
242
- final today = DateTime(now.year, now.month, now.day);
243
- final yesterday = today.subtract(const Duration(days: 1));
244
- final sevenDaysAgo = today.subtract(const Duration(days: 7));
245
- final thirtyDaysAgo = today.subtract(const Duration(days: 30));
246
-
247
- final grouped = <String, List<Conversation>>{};
248
-
249
- for (final conversation in conversations) {
250
- final convDate = DateTime(
251
- conversation.updatedAt.year,
252
- conversation.updatedAt.month,
253
- conversation.updatedAt.day,
254
- );
287
+final groupedConversationsProvider =
288
+ Provider<AsyncValue<Map<String, List<Conversation>>>>((ref) {
289
+ final filteredAsync = ref.watch(filteredConversationsProvider);
290
256
- String section;
257
- if (conversation.isPinned) {
258
- section = 'PINNED';
259
- } else if (convDate.isAtSameMomentAs(today)) {
260
- section = 'TODAY';
261
- } else if (convDate.isAtSameMomentAs(yesterday)) {
262
- section = 'YESTERDAY';
263
- } else if (convDate.isAfter(sevenDaysAgo)) {
264
- section = 'PREVIOUS 7 DAYS';
265
- } else if (convDate.isAfter(thirtyDaysAgo)) {
266
- section = 'PREVIOUS 30 DAYS';
267
- } else {
268
- section = 'OLDER';
269
- }
291
+ return filteredAsync.whenData((conversations) {
292
+ final now = DateTime.now();
293
+ final today = DateTime(now.year, now.month, now.day);
294
+ final yesterday = today.subtract(const Duration(days: 1));
295
+ final sevenDaysAgo = today.subtract(const Duration(days: 7));
296
+ final thirtyDaysAgo = today.subtract(const Duration(days: 30));
297
271
- grouped.putIfAbsent(section, () => []).add(conversation);
272
- }
298
+ final grouped = <String, List<Conversation>>{};
299
274
- return grouped;
275
-});
300
+ for (final conversation in conversations) {
301
+ final convDate = DateTime(
302
+ conversation.updatedAt.year,
303
+ conversation.updatedAt.month,
304
+ conversation.updatedAt.day,
305
+ );
306
+
307
+ String section;
308
+ if (conversation.isPinned) {
309
+ section = 'PINNED';
310
+ } else if (convDate.isAtSameMomentAs(today)) {
311
+ section = 'TODAY';
312
+ } else if (convDate.isAtSameMomentAs(yesterday)) {
313
+ section = 'YESTERDAY';
314
+ } else if (convDate.isAfter(sevenDaysAgo)) {
315
+ section = 'PREVIOUS 7 DAYS';
316
+ } else if (convDate.isAfter(thirtyDaysAgo)) {
317
+ section = 'PREVIOUS 30 DAYS';
318
+ } else {
319
+ section = 'OLDER';
320
+ }
321
+
322
+ grouped.putIfAbsent(section, () => []).add(conversation);
323
+ }
324
+
325
+ return grouped;
326
+ });
327
+ });
lib/features/conversations/views/chat_history_screen.dart
+20
-8
@@ -1,8 +1,9 @@
1
import 'package:flutter/material.dart';
2
import 'package:flutter_riverpod/flutter_riverpod.dart';
3
-import 'package:localmind/features/conversations/providers/conversation_providers.dart';
4
-import 'components/conversation_list.dart';
3
+
4
+import '../providers/conversation_providers.dart';
5
import 'components/conversation_empty_state.dart';
6
+import 'components/conversation_list.dart';
7
import 'components/conversation_search_bar.dart';
8
9
class ChatHistoryScreen extends ConsumerWidget {
@@ -12,7 +13,7 @@ class ChatHistoryScreen extends ConsumerWidget {
13
Widget build(BuildContext context, WidgetRef ref) {
14
final theme = Theme.of(context);
15
final isDark = theme.brightness == Brightness.dark;
15
-
16
+
17
final groupedConversations = ref.watch(groupedConversationsProvider);
18
final activeConversation = ref.watch(activeConversationProvider);
19
final searchQuery = ref.watch(conversationSearchProvider);
@@ -60,12 +61,23 @@ class ChatHistoryScreen extends ConsumerWidget {
61
62
// List
63
Expanded(
63
- child: groupedConversations.isEmpty
64
- ? ConversationEmptyState(isSearching: searchQuery.isNotEmpty)
65
- : ConversationList(
66
- groupedConversations: groupedConversations,
67
- activeConversation: activeConversation,
64
+ child: groupedConversations.when(
65
+ data: (grouped) => grouped.isEmpty
66
+ ? ConversationEmptyState(isSearching: searchQuery.isNotEmpty)
67
+ : ConversationList(
68
+ groupedConversations: grouped,
69
+ activeConversation: activeConversation,
70
+ ),
71
+ loading: () => const Center(
72
+ child: CircularProgressIndicator(strokeWidth: 2),
73
+ ),
74
+ error: (err, stack) => Center(
75
+ child: Text(
76
+ 'Error: $err',
77
+ style: TextStyle(color: theme.colorScheme.error),
78
),
79
+ ),
80
+ ),
81
),
82
],
83
),
lib/features/conversations/views/components/conversation_list.dart
+5
-5
@@ -1,10 +1,10 @@
1
import 'package:flutter/material.dart';
2
import 'package:flutter_riverpod/flutter_riverpod.dart';
3
import 'package:go_router/go_router.dart';
4
-import 'package:localmind/core/routes/app_routes.dart';
5
-import 'package:localmind/features/chat/providers/chat_providers.dart';
6
-import 'package:localmind/features/conversations/data/models/conversation.dart';
7
-import 'package:localmind/features/conversations/providers/conversation_providers.dart';
4
+import '../../../../core/routes/app_routes.dart';
5
+import '../../../chat/providers/chat_providers.dart';
6
+import '../../data/models/conversation.dart';
7
+import '../../providers/conversation_providers.dart';
8
import 'conversation_tile.dart';
9
import 'date_section_header.dart';
10
@@ -58,10 +58,10 @@ class ConversationList extends ConsumerWidget {
58
ref
59
.read(chatProvider.notifier)
60
.loadConversation(conversation);
61
- context.go(AppRoutes.home);
61
if (Scaffold.maybeOf(context)?.isDrawerOpen ?? false) {
62
Navigator.pop(context); // Close drawer if it was a drawer
63
}
64
+ context.go(AppRoutes.home);
65
},
66
onRename: () {
67
_showRenameDialog(context, ref, conversation);
lib/features/conversations/views/components/conversation_search_bar.dart
+1
-1
@@ -1,6 +1,6 @@
1
import 'package:flutter/material.dart';
2
import 'package:flutter_riverpod/flutter_riverpod.dart';
3
-import 'package:localmind/features/conversations/providers/conversation_providers.dart';
3
+import '../../providers/conversation_providers.dart';
4
5
class ConversationSearchBar extends ConsumerStatefulWidget {
6
const ConversationSearchBar({super.key});
lib/features/conversations/views/components/conversation_tile.dart
+1
-1
@@ -1,5 +1,5 @@
1
import 'package:flutter/material.dart';
2
-import 'package:localmind/features/conversations/data/models/conversation.dart';
2
+import '../../data/models/conversation.dart';
3
4
class ConversationTile extends StatelessWidget {
5
const ConversationTile({
lib/features/conversations/views/components/new_chat_button.dart
+2
-2
@@ -1,8 +1,8 @@
1
import 'package:flutter/material.dart';
2
import 'package:flutter_riverpod/flutter_riverpod.dart';
3
import 'package:go_router/go_router.dart';
4
-import 'package:localmind/core/routes/app_routes.dart';
5
-import 'package:localmind/features/chat/providers/chat_providers.dart';
4
+import '../../../../core/routes/app_routes.dart';
5
+import '../../../chat/providers/chat_providers.dart';
6
7
class NewChatButton extends ConsumerWidget {
8
const NewChatButton({super.key});
lib/features/models/data/model_cache.dart
+1
-1
@@ -1,4 +1,4 @@
1
-import 'package:localmind/features/models/data/models/model_info.dart';
1
+import 'models/model_info.dart';
2
3
class ModelCache {
4
final _cache = <String, _CacheEntry>{};
lib/features/models/data/models/model_info.dart
+1
-1
@@ -1,4 +1,4 @@
1
-import 'package:localmind/core/models/enums.dart';
1
+import '../../../../core/models/enums.dart';
2
3
class ModelInfo {
4
final String id;
lib/features/models/screens/model_picker_sheet.dart
+10
-11
@@ -1,10 +1,10 @@
1
import 'package:flutter/material.dart';
2
import 'package:flutter_riverpod/flutter_riverpod.dart';
3
-import 'package:localmind/features/chat/providers/chat_providers.dart';
4
-import 'package:localmind/features/models/data/models/model_info.dart';
5
-import 'package:localmind/features/servers/providers/server_providers.dart';
6
-import 'package:localmind/core/providers/service_providers.dart';
7
-import 'package:localmind/core/models/enums.dart';
3
+import '../../chat/providers/chat_providers.dart';
4
+import '../data/models/model_info.dart';
5
+import '../../servers/providers/server_providers.dart';
6
+import '../../../core/providers/service_providers.dart';
7
+import '../../../core/models/enums.dart';
8
9
final modelSearchQueryProvider = NotifierProvider<_ModelSearchNotifier, String>(
10
_ModelSearchNotifier.new,
@@ -260,7 +260,8 @@ class _ModelList extends ConsumerWidget {
260
),
261
),
262
data: (models) {
263
- final servers = ref.watch(serversProvider);
263
+ final serversAsync = ref.watch(serversProvider);
264
+ final servers = serversAsync.value ?? [];
265
final activeServer = servers.where((s) => s.id == serverId).firstOrNull;
266
final loadedModelsAsync = activeServer != null
267
? ref.watch(loadedModelsProvider(activeServer))
@@ -366,11 +367,9 @@ class _ModelList extends ConsumerWidget {
367
final message = activeServer.type == ServerType.ollama
368
? '${model.name} will be unloaded once the keep-alive time passes'
369
: '${model.name} unloaded successfully';
369
- ScaffoldMessenger.of(context).showSnackBar(
370
- SnackBar(
371
- content: Text(message),
372
- ),
373
- );
370
+ ScaffoldMessenger.of(
371
+ context,
372
+ ).showSnackBar(SnackBar(content: Text(message)));
373
}
374
} catch (e) {
375
if (context.mounted) {
lib/features/onboarding/screens/onboarding_server_setup_screen.dart
+1
-1
@@ -7,7 +7,7 @@ import 'package:shadcn_ui/shadcn_ui.dart';
7
8
import '../../../core/constants/app_constants.dart';
9
import '../../../core/models/enums.dart';
10
-import 'package:localmind/features/servers/data/models/server.dart';
10
+import '../../servers/data/models/server.dart';
11
import '../../../core/providers/service_providers.dart';
12
import '../../servers/providers/server_providers.dart';
13
import '../../../core/routes/app_routes.dart';
lib/features/personas/data/models/persona.dart
+1
-25
@@ -1,37 +1,13 @@
1
-import 'package:hive_ce/hive.dart';
2
-
3
-part 'persona.g.dart';
4
-
5
-@HiveType(typeId: 3)
6
-class Persona extends HiveObject {
7
- @HiveField(0)
1
+class Persona {
2
final String id;
9
-
10
- @HiveField(1)
3
final String name;
12
-
13
- @HiveField(2)
4
final String emoji;
15
-
16
- @HiveField(3)
5
final String systemPrompt;
18
-
19
- @HiveField(4)
6
final String? description;
21
-
22
- @HiveField(5)
7
final bool isBuiltIn;
24
-
25
- @HiveField(6)
8
final DateTime createdAt;
27
-
28
- @HiveField(7)
9
final DateTime updatedAt;
30
-
31
- @HiveField(8)
10
final String? category;
33
-
34
- @HiveField(9)
11
final Map<String, dynamic>? preferredParams;
12
13
Persona({
lib/features/personas/data/models/persona.g.dart
deleted
-68
@@ -1,68 +0,0 @@
1
-// GENERATED CODE - DO NOT MODIFY BY HAND
2
-
3
-part of 'persona.dart';
4
-
5
-// **************************************************************************
6
-// TypeAdapterGenerator
7
-// **************************************************************************
8
-
9
-class PersonaAdapter extends TypeAdapter<Persona> {
10
- @override
11
- final typeId = 3;
12
-
13
- @override
14
- Persona read(BinaryReader reader) {
15
- final numOfFields = reader.readByte();
16
- final fields = <int, dynamic>{
17
- for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
18
- };
19
- return Persona(
20
- id: fields[0] as String,
21
- name: fields[1] as String,
22
- emoji: fields[2] as String,
23
- systemPrompt: fields[3] as String,
24
- description: fields[4] as String?,
25
- isBuiltIn: fields[5] == null ? false : fields[5] as bool,
26
- createdAt: fields[6] as DateTime,
27
- updatedAt: fields[7] as DateTime,
28
- category: fields[8] as String?,
29
- preferredParams: (fields[9] as Map?)?.cast<String, dynamic>(),
30
- );
31
- }
32
-
33
- @override
34
- void write(BinaryWriter writer, Persona obj) {
35
- writer
36
- ..writeByte(10)
37
- ..writeByte(0)
38
- ..write(obj.id)
39
- ..writeByte(1)
40
- ..write(obj.name)
41
- ..writeByte(2)
42
- ..write(obj.emoji)
43
- ..writeByte(3)
44
- ..write(obj.systemPrompt)
45
- ..writeByte(4)
46
- ..write(obj.description)
47
- ..writeByte(5)
48
- ..write(obj.isBuiltIn)
49
- ..writeByte(6)
50
- ..write(obj.createdAt)
51
- ..writeByte(7)
52
- ..write(obj.updatedAt)
53
- ..writeByte(8)
54
- ..write(obj.category)
55
- ..writeByte(9)
56
- ..write(obj.preferredParams);
57
- }
58
-
59
- @override
60
- int get hashCode => typeId.hashCode;
61
-
62
- @override
63
- bool operator ==(Object other) =>
64
- identical(this, other) ||
65
- other is PersonaAdapter &&
66
- runtimeType == other.runtimeType &&
67
- typeId == other.typeId;
68
-}
lib/features/personas/providers/personas_providers.dart
+71
-56
@@ -1,7 +1,8 @@
1
import 'package:flutter_riverpod/flutter_riverpod.dart';
2
-import 'package:hive_ce/hive.dart';
3
-import 'package:localmind/core/providers/storage_providers.dart';
4
-import 'package:localmind/features/personas/data/models/persona.dart';
2
+import '../../../core/providers/storage_providers.dart';
3
+import '../../../core/storage/entities.dart';
4
+import '../data/models/persona.dart';
5
+import '../../../objectbox.g.dart';
6
7
final personaSearchQueryProvider =
8
NotifierProvider<_PersonaSearchNotifier, String>(
@@ -47,31 +48,33 @@ class _CategoryFilterNotifier extends Notifier<String?> {
48
}
49
50
final personasNotifierProvider =
50
- NotifierProvider<PersonasNotifier, List<Persona>>(PersonasNotifier.new);
51
-
52
-class PersonasNotifier extends Notifier<List<Persona>> {
53
- Box<Persona> get _box => ref.read(personasBoxProvider);
51
+ AsyncNotifierProvider<PersonasNotifier, List<Persona>>(
52
+ PersonasNotifier.new,
53
+ );
54
55
+class PersonasNotifier extends AsyncNotifier<List<Persona>> {
56
@override
56
- List<Persona> build() {
57
- _seedIfEmpty();
58
- return _box.values.toList()..sort((a, b) {
59
- if (a.isBuiltIn != b.isBuiltIn) return a.isBuiltIn ? -1 : 1;
60
- return a.name.compareTo(b.name);
61
- });
57
+ Future<List<Persona>> build() async {
58
+ return _loadAndSeed();
59
}
60
64
- void _seedIfEmpty() {
65
- if (_box.isEmpty) {
61
+ Future<List<Persona>> _loadAndSeed() async {
62
+ final db = ref.read(databaseProvider);
63
+ var entities = db.personaBox.getAll();
64
+
65
+ if (entities.isEmpty) {
66
for (final preset in _builtInPersonas) {
67
- _box.put(preset.id, preset);
67
+ db.personaBox.put(PersonaEntity.fromDomain(preset));
68
}
69
- state = _box.values.toList()
70
- ..sort((a, b) {
71
- if (a.isBuiltIn != b.isBuiltIn) return a.isBuiltIn ? -1 : 1;
72
- return a.name.compareTo(b.name);
73
- });
69
+ entities = db.personaBox.getAll();
70
}
71
+
72
+ final personas = entities.map((e) => e.toDomain()).toList();
73
+ personas.sort((a, b) {
74
+ if (a.isBuiltIn != b.isBuiltIn) return a.isBuiltIn ? -1 : 1;
75
+ return a.name.compareTo(b.name);
76
+ });
77
+ return personas;
78
}
79
80
Future<Persona> createPersona({
@@ -82,6 +85,7 @@ class PersonasNotifier extends Notifier<List<Persona>> {
85
String? category,
86
Map<String, dynamic>? preferredParams,
87
}) async {
88
+ final db = ref.read(databaseProvider);
89
final now = DateTime.now();
90
final persona = Persona(
91
id: now.millisecondsSinceEpoch.toString(),
@@ -95,39 +99,52 @@ class PersonasNotifier extends Notifier<List<Persona>> {
99
category: category,
100
preferredParams: preferredParams,
101
);
98
- await _box.put(persona.id, persona);
99
- state = _box.values.toList()
100
- ..sort((a, b) {
101
- if (a.isBuiltIn != b.isBuiltIn) return a.isBuiltIn ? -1 : 1;
102
- return a.name.compareTo(b.name);
103
- });
102
+ db.personaBox.put(PersonaEntity.fromDomain(persona));
103
+ state = AsyncData(await _loadAndSeed());
104
return persona;
105
}
106
107
Future<void> updatePersona(Persona updated) async {
108
+ final db = ref.read(databaseProvider);
109
final persona = updated.copyWith(updatedAt: DateTime.now());
109
- await _box.put(persona.id, persona);
110
- state = _box.values.toList()
111
- ..sort((a, b) {
112
- if (a.isBuiltIn != b.isBuiltIn) return a.isBuiltIn ? -1 : 1;
113
- return a.name.compareTo(b.name);
114
- });
110
+
111
+ final query = db.personaBox
112
+ .query(PersonaEntity_.id.equals(persona.id))
113
+ .build();
114
+ final existing = query.findFirst();
115
+ query.close();
116
+
117
+ final entity = PersonaEntity.fromDomain(persona);
118
+ if (existing != null) {
119
+ entity.internalId = existing.internalId;
120
+ }
121
+ db.personaBox.put(entity);
122
+
123
+ state = AsyncData(await _loadAndSeed());
124
}
125
126
Future<void> deletePersona(String id) async {
118
- final persona = _box.get(id);
119
- if (persona != null && persona.isBuiltIn) return;
120
- await _box.delete(id);
121
- state = _box.values.toList()
122
- ..sort((a, b) {
123
- if (a.isBuiltIn != b.isBuiltIn) return a.isBuiltIn ? -1 : 1;
124
- return a.name.compareTo(b.name);
125
- });
127
+ final personas = state.value ?? [];
128
+ final persona = personas.firstWhere(
129
+ (p) => p.id == id,
130
+ orElse: () => throw Exception('Persona not found'),
131
+ );
132
+ if (persona.isBuiltIn) return;
133
+
134
+ final db = ref.read(databaseProvider);
135
+ final query = db.personaBox.query(PersonaEntity_.id.equals(id)).build();
136
+ db.personaBox.removeMany(query.findIds());
137
+ query.close();
138
+
139
+ state = AsyncData(await _loadAndSeed());
140
}
141
142
Future<Persona> clonePersona(String id) async {
129
- final original = _box.get(id);
130
- if (original == null) throw Exception('Persona not found');
143
+ final personas = state.value ?? [];
144
+ final original = personas.firstWhere(
145
+ (p) => p.id == id,
146
+ orElse: () => throw Exception('Persona not found'),
147
+ );
148
149
final now = DateTime.now();
150
final clone = Persona(
@@ -144,12 +161,9 @@ class PersonasNotifier extends Notifier<List<Persona>> {
161
? Map<String, dynamic>.from(original.preferredParams!)
162
: null,
163
);
147
- await _box.put(clone.id, clone);
148
- state = _box.values.toList()
149
- ..sort((a, b) {
150
- if (a.isBuiltIn != b.isBuiltIn) return a.isBuiltIn ? -1 : 1;
151
- return a.name.compareTo(b.name);
152
- });
164
+ final db = ref.read(databaseProvider);
165
+ db.personaBox.put(PersonaEntity.fromDomain(clone));
166
+ state = AsyncData(await _loadAndSeed());
167
return clone;
168
}
169
@@ -242,7 +256,8 @@ class PersonasNotifier extends Notifier<List<Persona>> {
256
}
257
258
final filteredPersonasProvider = Provider<List<Persona>>((ref) {
245
- final personas = ref.watch(personasNotifierProvider);
259
+ final personasAsync = ref.watch(personasNotifierProvider);
260
+ final personas = personasAsync.value ?? [];
261
final category = ref.watch(personaCategoryFilterProvider);
262
final query = ref.watch(personaSearchQueryProvider).toLowerCase();
263
@@ -264,19 +279,19 @@ final filteredPersonasProvider = Provider<List<Persona>>((ref) {
279
});
280
281
final builtInPersonasProvider = Provider<List<Persona>>((ref) {
267
- return ref.watch(personasNotifierProvider).where((p) => p.isBuiltIn).toList();
282
+ final personas = ref.watch(personasNotifierProvider).value ?? [];
283
+ return personas.where((p) => p.isBuiltIn).toList();
284
});
285
286
final userPersonasProvider = Provider<List<Persona>>((ref) {
271
- return ref
272
- .watch(personasNotifierProvider)
273
- .where((p) => !p.isBuiltIn)
274
- .toList();
287
+ final personas = ref.watch(personasNotifierProvider).value ?? [];
288
+ return personas.where((p) => !p.isBuiltIn).toList();
289
});
290
291
final personaByIdProvider = Provider.family<Persona?, String>((ref, id) {
292
try {
279
- return ref.watch(personasNotifierProvider).firstWhere((p) => p.id == id);
293
+ final personas = ref.watch(personasNotifierProvider).value ?? [];
294
+ return personas.firstWhere((p) => p.id == id);
295
} catch (_) {
296
return null;
297
}
lib/features/personas/views/create_persona_screen.dart
+2
-2
@@ -1,8 +1,8 @@
1
import 'package:flutter/material.dart';
2
import 'package:flutter_riverpod/flutter_riverpod.dart';
3
import 'package:go_router/go_router.dart';
4
-import 'package:localmind/features/personas/data/models/persona.dart';
5
-import 'package:localmind/features/personas/providers/personas_providers.dart';
4
+import '../data/models/persona.dart';
5
+import '../providers/personas_providers.dart';
6
7
class CreatePersonaScreen extends ConsumerStatefulWidget {
8
final Persona? editPersona;
lib/features/personas/views/persona_list_screen.dart
+3
-3
@@ -1,9 +1,9 @@
1
import 'package:flutter/material.dart';
2
import 'package:flutter_riverpod/flutter_riverpod.dart';
3
import 'package:go_router/go_router.dart';
4
-import 'package:localmind/core/routes/app_routes.dart';
5
-import 'package:localmind/features/personas/data/models/persona.dart';
6
-import 'package:localmind/features/personas/providers/personas_providers.dart';
4
+import '../../../core/routes/app_routes.dart';
5
+import '../data/models/persona.dart';
6
+import '../providers/personas_providers.dart';
7
8
class PersonaListScreen extends ConsumerWidget {
9
const PersonaListScreen({super.key});
lib/features/servers/data/models/server.dart
+2
-27
@@ -1,41 +1,16 @@
1
-import 'package:hive_ce/hive.dart';
2
-import 'package:localmind/core/models/enums.dart';
1
+import '../../../../core/models/enums.dart';
2
4
-part 'server.g.dart';
5
-
6
-@HiveType(typeId: 0)
7
-class Server extends HiveObject {
8
- @HiveField(0)
3
+class Server {
4
final String id;
10
-
11
- @HiveField(1)
5
final String name;
13
-
14
- @HiveField(2)
6
final ServerType type;
16
-
17
- @HiveField(3)
7
final String host;
19
-
20
- @HiveField(4)
8
final int port;
22
-
23
- @HiveField(5)
9
final String? apiKey;
25
-
26
- @HiveField(6)
10
final bool isDefault;
28
-
29
- @HiveField(7)
11
final DateTime createdAt;
31
-
32
- @HiveField(8)
12
final DateTime lastConnectedAt;
34
-
35
- @HiveField(9)
13
final ConnectionStatus status;
37
-
38
- @HiveField(10)
14
final String? iconName;
15
16
Server({
lib/features/servers/data/models/server.g.dart
deleted
-73
@@ -1,73 +0,0 @@
1
-// GENERATED CODE - DO NOT MODIFY BY HAND
2
-
3
-part of 'server.dart';
4
-
5
-// **************************************************************************
6
-// TypeAdapterGenerator
7
-// **************************************************************************
8
-
9
-class ServerAdapter extends TypeAdapter<Server> {
10
- @override
11
- final typeId = 0;
12
-
13
- @override
14
- Server read(BinaryReader reader) {
15
- final numOfFields = reader.readByte();
16
- final fields = <int, dynamic>{
17
- for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
18
- };
19
- return Server(
20
- id: fields[0] as String,
21
- name: fields[1] as String,
22
- type: fields[2] as ServerType,
23
- host: fields[3] as String,
24
- port: (fields[4] as num).toInt(),
25
- apiKey: fields[5] as String?,
26
- isDefault: fields[6] == null ? false : fields[6] as bool,
27
- createdAt: fields[7] as DateTime,
28
- lastConnectedAt: fields[8] as DateTime,
29
- status: fields[9] == null
30
- ? ConnectionStatus.disconnected
31
- : fields[9] as ConnectionStatus,
32
- iconName: fields[10] as String?,
33
- );
34
- }
35
-
36
- @override
37
- void write(BinaryWriter writer, Server obj) {
38
- writer
39
- ..writeByte(11)
40
- ..writeByte(0)
41
- ..write(obj.id)
42
- ..writeByte(1)
43
- ..write(obj.name)
44
- ..writeByte(2)
45
- ..write(obj.type)
46
- ..writeByte(3)
47
- ..write(obj.host)
48
- ..writeByte(4)
49
- ..write(obj.port)
50
- ..writeByte(5)
51
- ..write(obj.apiKey)
52
- ..writeByte(6)
53
- ..write(obj.isDefault)
54
- ..writeByte(7)
55
- ..write(obj.createdAt)
56
- ..writeByte(8)
57
- ..write(obj.lastConnectedAt)
58
- ..writeByte(9)
59
- ..write(obj.status)
60
- ..writeByte(10)
61
- ..write(obj.iconName);
62
- }
63
-
64
- @override
65
- int get hashCode => typeId.hashCode;
66
-
67
- @override
68
- bool operator ==(Object other) =>
69
- identical(this, other) ||
70
- other is ServerAdapter &&
71
- runtimeType == other.runtimeType &&
72
- typeId == other.typeId;
73
-}
lib/features/servers/data/server_api_service.dart
+4
-4
@@ -1,7 +1,8 @@
1
import 'package:dio/dio.dart';
2
-import 'package:localmind/features/servers/data/models/server.dart';
3
-import 'package:localmind/features/models/data/models/model_info.dart';
4
-import 'package:localmind/core/models/enums.dart';
2
+
3
+import '../../../core/models/enums.dart';
4
+import '../../models/data/models/model_info.dart';
5
+import 'models/server.dart';
6
7
class ServerApiService {
8
final Dio _dio;
@@ -267,7 +268,6 @@ class ServerApiService {
268
.join(' ');
269
}
270
270
-
271
int? _parseParameterSize(String size) {
272
final cleaned = size.replaceAll('B', '').replaceAll('b', '').trim();
273
return int.tryParse(cleaned);
lib/features/servers/providers/server_providers.dart
+88
-40
@@ -1,15 +1,21 @@
1
import 'package:flutter_riverpod/flutter_riverpod.dart';
2
-import 'package:localmind/features/servers/data/models/server.dart';
3
-import 'package:localmind/core/models/enums.dart';
4
-import 'package:localmind/core/providers/storage_providers.dart';
5
-import 'package:localmind/core/providers/service_providers.dart';
6
-import 'package:localmind/features/models/data/model_cache.dart';
2
+
3
+import '../data/models/server.dart';
4
+import '../../../core/models/enums.dart';
5
+import '../../../core/providers/storage_providers.dart';
6
+import '../../../core/providers/service_providers.dart';
7
+import '../../../core/storage/entities.dart';
8
+import '../../models/data/model_cache.dart';
9
+
10
+import '../../../objectbox.g.dart';
11
12
final _modelCache = ModelCache();
13
10
-final serversProvider = NotifierProvider<ServersNotifier, List<Server>>(() {
11
- return ServersNotifier();
12
-});
14
+final serversProvider = AsyncNotifierProvider<ServersNotifier, List<Server>>(
15
+ () {
16
+ return ServersNotifier();
17
+ },
18
+);
19
20
final activeServerProvider = NotifierProvider<ActiveServerNotifier, Server?>(
21
() {
@@ -56,7 +62,8 @@ final availableModelsProvider = FutureProvider.family<List<dynamic>, String>((
62
final cached = _modelCache.get(serverId);
63
if (cached != null) return cached;
64
59
- final servers = ref.watch(serversProvider);
65
+ final serversAsync = ref.watch(serversProvider);
66
+ final servers = serversAsync.value ?? [];
67
final server = servers.firstWhere(
68
(s) => s.id == serverId,
69
orElse: () => throw Exception('Server not found'),
@@ -67,48 +74,77 @@ final availableModelsProvider = FutureProvider.family<List<dynamic>, String>((
74
return models;
75
});
76
70
-class ServersNotifier extends Notifier<List<Server>> {
77
+class ServersNotifier extends AsyncNotifier<List<Server>> {
78
@override
72
- List<Server> build() {
73
- final boxes = ref.watch(hiveBoxesProvider);
74
- return boxes.servers.values.toList().cast<Server>();
79
+ Future<List<Server>> build() async {
80
+ return _loadAll();
81
+ }
82
+
83
+ Future<List<Server>> _loadAll() async {
84
+ final db = ref.read(databaseProvider);
85
+ final entities = db.serverBox.getAll();
86
+ return entities.map((e) => e.toDomain()).toList();
87
}
88
89
Future<void> addServer(Server server) async {
78
- final boxes = ref.read(hiveBoxesProvider);
79
- await boxes.servers.put(server.id, server);
80
- state = boxes.servers.values.toList().cast<Server>();
90
+ final db = ref.read(databaseProvider);
91
+ db.serverBox.put(ServerEntity.fromDomain(server));
92
+ state = AsyncData(await _loadAll());
93
}
94
95
Future<void> updateServer(Server server) async {
84
- final boxes = ref.read(hiveBoxesProvider);
85
- await boxes.servers.put(server.id, server);
86
- state = boxes.servers.values.toList().cast<Server>();
96
+ final db = ref.read(databaseProvider);
97
+ final query = db.serverBox
98
+ .query(ServerEntity_.id.equals(server.id))
99
+ .build();
100
+ final existing = query.findFirst();
101
+ query.close();
102
+
103
+ final entity = ServerEntity.fromDomain(server);
104
+ if (existing != null) {
105
+ entity.internalId = existing.internalId;
106
+ }
107
+ db.serverBox.put(entity);
108
+ state = AsyncData(await _loadAll());
109
}
110
111
Future<void> deleteServer(String serverId) async {
90
- final boxes = ref.read(hiveBoxesProvider);
91
- await boxes.servers.delete(serverId);
92
- state = boxes.servers.values.toList().cast<Server>();
112
+ final db = ref.read(databaseProvider);
113
+ final query = db.serverBox.query(ServerEntity_.id.equals(serverId)).build();
114
+ db.serverBox.removeMany(query.findIds());
115
+ query.close();
116
+ state = AsyncData(await _loadAll());
117
}
118
119
Future<void> setDefault(String serverId) async {
96
- final boxes = ref.read(hiveBoxesProvider);
97
- final updatedServers = state.map((s) {
120
+ final db = ref.read(databaseProvider);
121
+ final servers = state.value ?? [];
122
+ final updatedServers = servers.map((s) {
123
return s.copyWith(isDefault: s.id == serverId);
124
}).toList();
125
126
for (final server in updatedServers) {
102
- await boxes.servers.put(server.id, server);
127
+ final query = db.serverBox
128
+ .query(ServerEntity_.id.equals(server.id))
129
+ .build();
130
+ final existing = query.findFirst();
131
+ query.close();
132
+
133
+ final entity = ServerEntity.fromDomain(server);
134
+ if (existing != null) {
135
+ entity.internalId = existing.internalId;
136
+ }
137
+ db.serverBox.put(entity);
138
}
104
- state = boxes.servers.values.toList().cast<Server>();
139
+ state = AsyncData(await _loadAll());
140
}
141
142
Future<ConnectionStatus> testConnection(
143
String serverId,
144
dynamic apiService,
145
) async {
111
- final server = state.firstWhere((s) => s.id == serverId);
146
+ final servers = state.value ?? [];
147
+ final server = servers.firstWhere((s) => s.id == serverId);
148
final isConnected = await apiService.testConnection(server);
149
final status = isConnected
150
? ConnectionStatus.connected
@@ -118,9 +154,21 @@ class ServersNotifier extends Notifier<List<Server>> {
154
status: status,
155
lastConnectedAt: DateTime.now(),
156
);
121
- final boxes = ref.read(hiveBoxesProvider);
122
- await boxes.servers.put(serverId, updatedServer);
123
- state = boxes.servers.values.toList().cast<Server>();
157
+ final db = ref.read(databaseProvider);
158
+
159
+ final query = db.serverBox
160
+ .query(ServerEntity_.id.equals(updatedServer.id))
161
+ .build();
162
+ final existing = query.findFirst();
163
+ query.close();
164
+
165
+ final entity = ServerEntity.fromDomain(updatedServer);
166
+ if (existing != null) {
167
+ entity.internalId = existing.internalId;
168
+ }
169
+ db.serverBox.put(entity);
170
+
171
+ state = AsyncData(await _loadAll());
172
return status;
173
}
174
}
@@ -128,16 +176,14 @@ class ServersNotifier extends Notifier<List<Server>> {
176
class ActiveServerNotifier extends Notifier<Server?> {
177
@override
178
Server? build() {
131
- final servers = ref.watch(serversProvider);
132
- final boxes = ref.watch(hiveBoxesProvider);
133
- final defaultServerIdObj = boxes.settings.get('defaultServerId');
179
+ final serversAsync = ref.watch(serversProvider);
180
+ final servers = serversAsync.value ?? [];
181
+ if (servers.isEmpty) return null;
182
135
- String? defaultServerId;
136
- if (defaultServerIdObj is String && defaultServerIdObj.isNotEmpty) {
137
- defaultServerId = defaultServerIdObj;
138
- }
183
+ final prefs = ref.watch(sharedPreferencesProvider);
184
+ final defaultServerId = prefs.getString('defaultServerId');
185
140
- if (defaultServerId != null) {
186
+ if (defaultServerId != null && defaultServerId.isNotEmpty) {
187
final matchingServer = servers.where((s) => s.id == defaultServerId);
188
if (matchingServer.isNotEmpty) {
189
return matchingServer.first;
@@ -152,10 +198,12 @@ class ActiveServerNotifier extends Notifier<Server?> {
198
}
199
200
void setActiveServer(Server? server) {
155
- final boxes = ref.read(hiveBoxesProvider);
201
+ final prefs = ref.read(sharedPreferencesProvider);
202
state = server;
203
if (server != null) {
158
- boxes.settings.put('defaultServerId', server.id);
204
+ prefs.setString('defaultServerId', server.id);
205
+ } else {
206
+ prefs.remove('defaultServerId');
207
}
208
}
209
}
lib/features/servers/views/add_server_screen.dart
+1
-1
@@ -4,7 +4,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
4
import 'package:go_router/go_router.dart';
5
import 'package:shadcn_ui/shadcn_ui.dart';
6
import '../../../core/constants/app_constants.dart';
7
-import 'package:localmind/features/servers/data/models/server.dart';
7
+import '../data/models/server.dart';
8
import '../../../core/models/enums.dart';
9
import '../../../core/providers/service_providers.dart';
10
import '../providers/server_providers.dart';
lib/features/servers/views/components/connection_status_indicator.dart
+1
@@ -1,4 +1,5 @@
1
import 'package:flutter/material.dart';
2
+
3
import '../../../../core/models/enums.dart';
4
5
class ConnectionStatusIndicator extends StatefulWidget {
lib/features/servers/views/components/server_card.dart
+14
-11
@@ -1,6 +1,6 @@
1
import 'package:flutter/material.dart';
2
import 'package:hugeicons/hugeicons.dart';
3
-import 'package:localmind/features/servers/data/models/server.dart';
3
+import '../../data/models/server.dart';
4
import '../../../../core/models/enums.dart';
5
import 'connection_status_indicator.dart';
6
import 'server_icon_picker.dart';
@@ -60,9 +60,7 @@ class ServerCard extends StatelessWidget {
60
duration: const Duration(milliseconds: 300),
61
curve: Curves.easeInOut,
62
decoration: BoxDecoration(
63
- color: isActive
64
- ? activeGreen.withValues(alpha: 0.08)
65
- : theme.cardColor,
63
+ color: isActive ? activeGreen.withValues(alpha: 0.08) : theme.cardColor,
64
borderRadius: BorderRadius.circular(12),
65
border: Border.all(
66
color: isActive
@@ -104,7 +102,7 @@ class ServerCard extends StatelessWidget {
102
color: isActive
103
? activeGreen.withValues(alpha: 0.1)
104
: theme.colorScheme.surfaceContainerHighest
107
- .withValues(alpha: 0.3),
105
+ .withValues(alpha: 0.3),
106
borderRadius: BorderRadius.circular(12),
107
border: Border.all(
108
color: isActive
@@ -171,8 +169,9 @@ class ServerCard extends StatelessWidget {
169
decoration: BoxDecoration(
170
color: isActive
171
? activeGreen.withValues(alpha: 0.05)
174
- : theme.colorScheme.outline
175
- .withValues(alpha: 0.1),
172
+ : theme.colorScheme.outline.withValues(
173
+ alpha: 0.1,
174
+ ),
175
borderRadius: BorderRadius.circular(4),
176
border: Border.all(
177
color: isActive
@@ -194,13 +193,17 @@ class ServerCard extends StatelessWidget {
193
const SizedBox(height: 4),
194
Row(
195
children: [
197
- Text(_serverTypeName,
198
- style: theme.textTheme.bodySmall),
196
+ Text(
197
+ _serverTypeName,
198
+ style: theme.textTheme.bodySmall,
199
+ ),
200
const SizedBox(width: 8),
201
Text('•', style: theme.textTheme.bodySmall),
202
const SizedBox(width: 8),
202
- Text(_serverAddress,
203
- style: theme.textTheme.bodySmall),
203
+ Text(
204
+ _serverAddress,
205
+ style: theme.textTheme.bodySmall,
206
+ ),
207
],
208
),
209
],
lib/features/servers/views/components/server_type_selector.dart
+1
@@ -1,5 +1,6 @@
1
import 'package:flutter/material.dart';
2
import 'package:hugeicons/hugeicons.dart';
3
+
4
import '../../../../core/models/enums.dart';
5
6
class ServerTypeSelector extends StatelessWidget {
lib/features/servers/views/server_list_screen.dart
+74
-51
@@ -1,8 +1,9 @@
1
import 'package:flutter/material.dart';
2
import 'package:flutter_riverpod/flutter_riverpod.dart';
3
import 'package:go_router/go_router.dart';
4
-import '../../../core/routes/app_routes.dart';
4
+
5
import '../../../core/providers/service_providers.dart';
6
+import '../../../core/routes/app_routes.dart';
7
import '../providers/server_providers.dart';
8
import 'components/server_card.dart';
9
@@ -57,59 +58,81 @@ class ServerListScreen extends ConsumerWidget {
58
),
59
60
Expanded(
60
- child: servers.isEmpty
61
- ? _buildEmptyState(context, isDark, theme)
62
- : RefreshIndicator(
63
- onRefresh: () async {
64
- for (final server in servers) {
65
- await ref
66
- .read(serversProvider.notifier)
67
- .testConnection(
68
- server.id,
69
- ref.read(serverApiServiceProvider),
70
- );
71
- }
72
- },
73
- child: ListView.builder(
74
- padding: const EdgeInsets.all(16),
75
- itemCount: servers.length,
76
- itemBuilder: (context, index) {
77
- final server = servers[index];
78
- return Padding(
79
- padding: const EdgeInsets.only(bottom: 12),
80
- child: ServerCard(
81
- server: server,
82
- isActive: activeServer?.id == server.id,
83
- onTap: () {
84
- ref
85
- .read(activeServerProvider.notifier)
86
- .setActiveServer(server);
87
- ScaffoldMessenger.of(context).showSnackBar(
88
- SnackBar(
89
- content: Text(
90
- 'Switched to ${server.name}',
91
- ),
92
- duration: const Duration(seconds: 2),
93
- ),
61
+ child: servers.when(
62
+ data: (serverList) => serverList.isEmpty
63
+ ? _buildEmptyState(context, isDark, theme)
64
+ : RefreshIndicator(
65
+ onRefresh: () async {
66
+ for (final server in serverList) {
67
+ await ref
68
+ .read(serversProvider.notifier)
69
+ .testConnection(
70
+ server.id,
71
+ ref.read(serverApiServiceProvider),
72
);
95
- },
96
- onEdit: () =>
97
- _showEditDialog(context, ref, server),
98
- onDelete: () => _showDeleteConfirmation(
99
- context,
100
- ref,
101
- server,
102
- ),
103
- onSetDefault: () {
104
- ref
105
- .read(serversProvider.notifier)
106
- .setDefault(server.id);
107
- },
108
- ),
109
- );
73
+ }
74
},
75
+ child: ListView.builder(
76
+ padding: const EdgeInsets.all(16),
77
+ itemCount: serverList.length,
78
+ itemBuilder: (context, index) {
79
+ final server = serverList[index];
80
+ return Padding(
81
+ padding: const EdgeInsets.only(bottom: 12),
82
+ child: ServerCard(
83
+ server: server,
84
+ isActive: activeServer?.id == server.id,
85
+ onTap: () {
86
+ ref
87
+ .read(activeServerProvider.notifier)
88
+ .setActiveServer(server);
89
+ ScaffoldMessenger.of(context).showSnackBar(
90
+ SnackBar(
91
+ content: Text(
92
+ 'Switched to ${server.name}',
93
+ ),
94
+ duration: const Duration(seconds: 2),
95
+ ),
96
+ );
97
+ },
98
+ onEdit: () =>
99
+ _showEditDialog(context, ref, server),
100
+ onDelete: () => _showDeleteConfirmation(
101
+ context,
102
+ ref,
103
+ server,
104
+ ),
105
+ onSetDefault: () {
106
+ ref
107
+ .read(serversProvider.notifier)
108
+ .setDefault(server.id);
109
+ },
110
+ ),
111
+ );
112
+ },
113
+ ),
114
),
112
- ),
115
+ loading: () =>
116
+ const Center(child: CircularProgressIndicator()),
117
+ error: (err, stack) => Center(
118
+ child: Column(
119
+ mainAxisAlignment: MainAxisAlignment.center,
120
+ children: [
121
+ const Icon(
122
+ Icons.error_outline,
123
+ color: Colors.red,
124
+ size: 48,
125
+ ),
126
+ const SizedBox(height: 16),
127
+ Text('Error loading servers: $err'),
128
+ TextButton(
129
+ onPressed: () => ref.invalidate(serversProvider),
130
+ child: const Text('Retry'),
131
+ ),
132
+ ],
133
+ ),
134
+ ),
135
+ ),
136
),
137
],
138
),
lib/features/settings/data/models/app_settings.dart
+54
-52
@@ -1,88 +1,39 @@
1
-import 'package:flutter/material.dart';
2
-import 'package:hive_ce/hive.dart';
1
+import 'dart:convert';
2
4
-part 'app_settings.g.dart';
3
+import 'package:flutter/material.dart';
4
6
-@HiveType(typeId: 5)
5
enum SyntaxThemeName {
8
- @HiveField(0)
6
vscodeDark,
10
- @HiveField(1)
7
vscodeLight,
12
- @HiveField(2)
8
dracula,
14
- @HiveField(3)
9
monokaiSublime,
16
- @HiveField(4)
10
ayuLight,
18
- @HiveField(5)
11
ayuDark,
20
- @HiveField(6)
12
gravityLight,
22
- @HiveField(7)
13
gravityDark,
24
- @HiveField(8)
14
obsidian,
26
- @HiveField(9)
15
oceanSunset,
28
- @HiveField(10)
16
standard,
17
}
18
32
-@HiveType(typeId: 4)
33
-class AppSettings extends HiveObject {
34
- @HiveField(0)
19
+class AppSettings {
20
final double temperature;
36
-
37
- @HiveField(1)
21
final double topP;
39
-
40
- @HiveField(2)
22
final int maxTokens;
42
-
43
- @HiveField(3)
23
final int contextLength;
45
-
46
- @HiveField(4)
24
final ThemeMode themeMode;
48
-
49
- @HiveField(5)
25
final double fontSize;
51
-
52
- @HiveField(6)
26
final bool showSystemMessages;
54
-
55
- @HiveField(7)
27
final bool hapticFeedbackEnabled;
57
-
58
- @HiveField(8)
28
final bool sendOnEnter;
60
-
61
- @HiveField(9)
29
final String? defaultServerId;
63
-
64
- @HiveField(10)
30
final bool showDataIndicator;
66
-
67
- @HiveField(11)
31
final bool autoGenerateTitle;
69
-
70
- @HiveField(12)
32
final bool streamingEnabled;
72
-
73
- @HiveField(13)
33
final String? defaultPersonaId;
75
-
76
- @HiveField(14, defaultValue: false)
34
final bool hasCompletedOnboarding;
78
-
79
- @HiveField(15, defaultValue: true)
35
final bool mcpEnabled;
81
-
82
- @HiveField(16, defaultValue: SyntaxThemeName.vscodeDark)
36
final SyntaxThemeName codeThemeDark;
84
-
85
- @HiveField(17, defaultValue: SyntaxThemeName.vscodeLight)
37
final SyntaxThemeName codeThemeLight;
38
39
AppSettings({
@@ -149,4 +100,55 @@ class AppSettings extends HiveObject {
100
codeThemeLight: codeThemeLight ?? this.codeThemeLight,
101
);
102
}
103
+
104
+ Map<String, dynamic> toMap() {
105
+ return {
106
+ 'temperature': temperature,
107
+ 'topP': topP,
108
+ 'maxTokens': maxTokens,
109
+ 'contextLength': contextLength,
110
+ 'themeMode': themeMode.index,
111
+ 'fontSize': fontSize,
112
+ 'showSystemMessages': showSystemMessages,
113
+ 'hapticFeedbackEnabled': hapticFeedbackEnabled,
114
+ 'sendOnEnter': sendOnEnter,
115
+ 'defaultServerId': defaultServerId,
116
+ 'showDataIndicator': showDataIndicator,
117
+ 'autoGenerateTitle': autoGenerateTitle,
118
+ 'streamingEnabled': streamingEnabled,
119
+ 'defaultPersonaId': defaultPersonaId,
120
+ 'hasCompletedOnboarding': hasCompletedOnboarding,
121
+ 'mcpEnabled': mcpEnabled,
122
+ 'codeThemeDark': codeThemeDark.index,
123
+ 'codeThemeLight': codeThemeLight.index,
124
+ };
125
+ }
126
+
127
+ factory AppSettings.fromMap(Map<String, dynamic> map) {
128
+ return AppSettings(
129
+ temperature: map['temperature']?.toDouble() ?? 0.7,
130
+ topP: map['topP']?.toDouble() ?? 0.9,
131
+ maxTokens: map['maxTokens']?.toInt() ?? 2048,
132
+ contextLength: map['contextLength']?.toInt() ?? 4096,
133
+ themeMode: ThemeMode.values[map['themeMode'] ?? 2],
134
+ fontSize: map['fontSize']?.toDouble() ?? 16.0,
135
+ showSystemMessages: map['showSystemMessages'] ?? false,
136
+ hapticFeedbackEnabled: map['hapticFeedbackEnabled'] ?? true,
137
+ sendOnEnter: map['sendOnEnter'] ?? false,
138
+ defaultServerId: map['defaultServerId'],
139
+ showDataIndicator: map['showDataIndicator'] ?? true,
140
+ autoGenerateTitle: map['autoGenerateTitle'] ?? true,
141
+ streamingEnabled: map['streamingEnabled'] ?? true,
142
+ defaultPersonaId: map['defaultPersonaId'],
143
+ hasCompletedOnboarding: map['hasCompletedOnboarding'] ?? false,
144
+ mcpEnabled: map['mcpEnabled'] ?? true,
145
+ codeThemeDark: SyntaxThemeName.values[map['codeThemeDark'] ?? 0],
146
+ codeThemeLight: SyntaxThemeName.values[map['codeThemeLight'] ?? 1],
147
+ );
148
+ }
149
+
150
+ String toJson() => json.encode(toMap());
151
+
152
+ factory AppSettings.fromJson(String source) =>
153
+ AppSettings.fromMap(json.decode(source));
154
}
lib/features/settings/data/models/app_settings.g.dart
deleted
-169
@@ -1,169 +0,0 @@
1
-// GENERATED CODE - DO NOT MODIFY BY HAND
2
-
3
-part of 'app_settings.dart';
4
-
5
-// **************************************************************************
6
-// TypeAdapterGenerator
7
-// **************************************************************************
8
-
9
-class AppSettingsAdapter extends TypeAdapter<AppSettings> {
10
- @override
11
- final typeId = 4;
12
-
13
- @override
14
- AppSettings read(BinaryReader reader) {
15
- final numOfFields = reader.readByte();
16
- final fields = <int, dynamic>{
17
- for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
18
- };
19
- return AppSettings(
20
- temperature: fields[0] == null ? 0.7 : (fields[0] as num).toDouble(),
21
- topP: fields[1] == null ? 0.9 : (fields[1] as num).toDouble(),
22
- maxTokens: fields[2] == null ? 2048 : (fields[2] as num).toInt(),
23
- contextLength: fields[3] == null ? 4096 : (fields[3] as num).toInt(),
24
- themeMode: fields[4] == null ? ThemeMode.dark : fields[4] as ThemeMode,
25
- fontSize: fields[5] == null ? 16.0 : (fields[5] as num).toDouble(),
26
- showSystemMessages: fields[6] == null ? false : fields[6] as bool,
27
- hapticFeedbackEnabled: fields[7] == null ? true : fields[7] as bool,
28
- sendOnEnter: fields[8] == null ? false : fields[8] as bool,
29
- defaultServerId: fields[9] as String?,
30
- showDataIndicator: fields[10] == null ? true : fields[10] as bool,
31
- autoGenerateTitle: fields[11] == null ? true : fields[11] as bool,
32
- streamingEnabled: fields[12] == null ? true : fields[12] as bool,
33
- defaultPersonaId: fields[13] as String?,
34
- hasCompletedOnboarding: fields[14] == null ? false : fields[14] as bool,
35
- mcpEnabled: fields[15] == null ? true : fields[15] as bool,
36
- codeThemeDark: fields[16] == null
37
- ? SyntaxThemeName.vscodeDark
38
- : fields[16] as SyntaxThemeName,
39
- codeThemeLight: fields[17] == null
40
- ? SyntaxThemeName.vscodeLight
41
- : fields[17] as SyntaxThemeName,
42
- );
43
- }
44
-
45
- @override
46
- void write(BinaryWriter writer, AppSettings obj) {
47
- writer
48
- ..writeByte(18)
49
- ..writeByte(0)
50
- ..write(obj.temperature)
51
- ..writeByte(1)
52
- ..write(obj.topP)
53
- ..writeByte(2)
54
- ..write(obj.maxTokens)
55
- ..writeByte(3)
56
- ..write(obj.contextLength)
57
- ..writeByte(4)
58
- ..write(obj.themeMode)
59
- ..writeByte(5)
60
- ..write(obj.fontSize)
61
- ..writeByte(6)
62
- ..write(obj.showSystemMessages)
63
- ..writeByte(7)
64
- ..write(obj.hapticFeedbackEnabled)
65
- ..writeByte(8)
66
- ..write(obj.sendOnEnter)
67
- ..writeByte(9)
68
- ..write(obj.defaultServerId)
69
- ..writeByte(10)
70
- ..write(obj.showDataIndicator)
71
- ..writeByte(11)
72
- ..write(obj.autoGenerateTitle)
73
- ..writeByte(12)
74
- ..write(obj.streamingEnabled)
75
- ..writeByte(13)
76
- ..write(obj.defaultPersonaId)
77
- ..writeByte(14)
78
- ..write(obj.hasCompletedOnboarding)
79
- ..writeByte(15)
80
- ..write(obj.mcpEnabled)
81
- ..writeByte(16)
82
- ..write(obj.codeThemeDark)
83
- ..writeByte(17)
84
- ..write(obj.codeThemeLight);
85
- }
86
-
87
- @override
88
- int get hashCode => typeId.hashCode;
89
-
90
- @override
91
- bool operator ==(Object other) =>
92
- identical(this, other) ||
93
- other is AppSettingsAdapter &&
94
- runtimeType == other.runtimeType &&
95
- typeId == other.typeId;
96
-}
97
-
98
-class SyntaxThemeNameAdapter extends TypeAdapter<SyntaxThemeName> {
99
- @override
100
- final typeId = 5;
101
-
102
- @override
103
- SyntaxThemeName read(BinaryReader reader) {
104
- switch (reader.readByte()) {
105
- case 0:
106
- return SyntaxThemeName.vscodeDark;
107
- case 1:
108
- return SyntaxThemeName.vscodeLight;
109
- case 2:
110
- return SyntaxThemeName.dracula;
111
- case 3:
112
- return SyntaxThemeName.monokaiSublime;
113
- case 4:
114
- return SyntaxThemeName.ayuLight;
115
- case 5:
116
- return SyntaxThemeName.ayuDark;
117
- case 6:
118
- return SyntaxThemeName.gravityLight;
119
- case 7:
120
- return SyntaxThemeName.gravityDark;
121
- case 8:
122
- return SyntaxThemeName.obsidian;
123
- case 9:
124
- return SyntaxThemeName.oceanSunset;
125
- case 10:
126
- return SyntaxThemeName.standard;
127
- default:
128
- return SyntaxThemeName.vscodeDark;
129
- }
130
- }
131
-
132
- @override
133
- void write(BinaryWriter writer, SyntaxThemeName obj) {
134
- switch (obj) {
135
- case SyntaxThemeName.vscodeDark:
136
- writer.writeByte(0);
137
- case SyntaxThemeName.vscodeLight:
138
- writer.writeByte(1);
139
- case SyntaxThemeName.dracula:
140
- writer.writeByte(2);
141
- case SyntaxThemeName.monokaiSublime:
142
- writer.writeByte(3);
143
- case SyntaxThemeName.ayuLight:
144
- writer.writeByte(4);
145
- case SyntaxThemeName.ayuDark:
146
- writer.writeByte(5);
147
- case SyntaxThemeName.gravityLight:
148
- writer.writeByte(6);
149
- case SyntaxThemeName.gravityDark:
150
- writer.writeByte(7);
151
- case SyntaxThemeName.obsidian:
152
- writer.writeByte(8);
153
- case SyntaxThemeName.oceanSunset:
154
- writer.writeByte(9);
155
- case SyntaxThemeName.standard:
156
- writer.writeByte(10);
157
- }
158
- }
159
-
160
- @override
161
- int get hashCode => typeId.hashCode;
162
-
163
- @override
164
- bool operator ==(Object other) =>
165
- identical(this, other) ||
166
- other is SyntaxThemeNameAdapter &&
167
- runtimeType == other.runtimeType &&
168
- typeId == other.typeId;
169
-}
lib/features/settings/screens/settings_screen.dart
+9
-11
@@ -1,13 +1,13 @@
1
import 'package:flutter/material.dart';
2
import 'package:flutter_riverpod/flutter_riverpod.dart';
3
import 'package:go_router/go_router.dart';
4
-import 'package:localmind/core/providers/app_providers.dart';
5
-import 'package:localmind/core/providers/storage_providers.dart' as storage;
6
-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';
4
+import '../../../core/providers/app_providers.dart';
5
+
6
+import '../../../core/theme/app_theme.dart';
7
+import '../../conversations/providers/conversation_providers.dart';
8
+import '../../servers/providers/server_providers.dart';
9
+import '../data/models/app_settings.dart';
10
+import '../../personas/providers/personas_providers.dart';
11
12
class SettingsScreen extends ConsumerWidget {
13
const SettingsScreen({super.key});
@@ -147,8 +147,7 @@ class SettingsScreen extends ConsumerWidget {
147
_DropdownSetting(
148
label: 'Default Server',
149
currentValue: settings.defaultServerId,
150
- items: ref
151
- .watch(serversProvider)
150
+ items: (ref.watch(serversProvider).value ?? [])
151
.map((s) => (s.id, s.name))
152
.toList(),
153
onChanged: (v) =>
@@ -161,8 +160,7 @@ class SettingsScreen extends ConsumerWidget {
160
_DropdownSetting(
161
label: 'Default Persona',
162
currentValue: settings.defaultPersonaId,
164
- items: ref
165
- .watch(personasNotifierProvider)
163
+ items: (ref.watch(personasNotifierProvider).value ?? [])
164
.map((p) => (p.id, '${p.emoji} ${p.name}'))
165
.toList(),
166
onChanged: (v) =>
lib/features/sidebar/components/conversation_drawer_header.dart
+1
-1
@@ -1,6 +1,6 @@
1
import 'package:flutter/material.dart';
2
import 'package:go_router/go_router.dart';
3
-import 'package:localmind/core/routes/app_routes.dart';
3
+import '../../../core/routes/app_routes.dart';
4
5
class ConversationDrawerHeader extends StatelessWidget {
6
const ConversationDrawerHeader({super.key});
lib/features/sidebar/sidebar_widget.dart
+20
-9
@@ -47,12 +47,23 @@ class SidebarWidget extends ConsumerWidget {
47
const ConversationSearchBar(),
48
const Divider(height: 1),
49
Expanded(
50
- child: groupedConversations.isEmpty
51
- ? ConversationEmptyState(isSearching: searchQuery.isNotEmpty)
52
- : ConversationList(
53
- groupedConversations: groupedConversations,
54
- activeConversation: activeConversation,
55
- ),
50
+ child: groupedConversations.when(
51
+ data: (grouped) => grouped.isEmpty
52
+ ? ConversationEmptyState(isSearching: searchQuery.isNotEmpty)
53
+ : ConversationList(
54
+ groupedConversations: grouped,
55
+ activeConversation: activeConversation,
56
+ ),
57
+ loading: () => const Center(
58
+ child: CircularProgressIndicator(strokeWidth: 2),
59
+ ),
60
+ error: (err, stack) => Center(
61
+ child: Text(
62
+ 'Error: $err',
63
+ style: TextStyle(color: theme.colorScheme.error),
64
+ ),
65
+ ),
66
+ ),
67
),
68
const Divider(height: 1),
69
const ActiveServerIndicator(),
@@ -62,10 +73,10 @@ class SidebarWidget extends ConsumerWidget {
73
label: 'History',
74
isSelected: isHistory,
75
onTap: () {
65
- context.go(AppRoutes.chatHistory);
76
if (Scaffold.maybeOf(context)?.isDrawerOpen ?? false) {
77
Navigator.pop(context);
78
}
79
+ context.go(AppRoutes.chatHistory);
80
},
81
),
82
DrawerNavItem(
@@ -73,10 +84,10 @@ class SidebarWidget extends ConsumerWidget {
84
label: 'Servers',
85
isSelected: isServers,
86
onTap: () {
76
- context.go(AppRoutes.servers);
87
if (Scaffold.maybeOf(context)?.isDrawerOpen ?? false) {
88
Navigator.pop(context);
89
}
90
+ context.go(AppRoutes.servers);
91
},
92
),
93
DrawerNavItem(
@@ -84,10 +95,10 @@ class SidebarWidget extends ConsumerWidget {
95
label: 'Personas',
96
isSelected: isPersonas,
97
onTap: () {
87
- context.go(AppRoutes.personas);
98
if (Scaffold.maybeOf(context)?.isDrawerOpen ?? false) {
99
Navigator.pop(context);
100
}
101
+ context.go(AppRoutes.personas);
102
},
103
),
104
const SizedBox(height: 8),
lib/hive_registrar.g.dart
deleted
-32
@@ -1,32 +0,0 @@
1
-// Generated by Hive CE
2
-// Do not modify
3
-// Check in to version control
4
-
5
-import 'package:hive_ce/hive_ce.dart';
6
-import 'package:localmind/features/chat/data/models/message.dart';
7
-import 'package:localmind/features/conversations/data/models/conversation.dart';
8
-import 'package:localmind/features/personas/data/models/persona.dart';
9
-import 'package:localmind/features/servers/data/models/server.dart';
10
-import 'package:localmind/features/settings/data/models/app_settings.dart';
11
-
12
-extension HiveRegistrar on HiveInterface {
13
- void registerAdapters() {
14
- registerAdapter(AppSettingsAdapter());
15
- registerAdapter(ConversationAdapter());
16
- registerAdapter(MessageAdapter());
17
- registerAdapter(PersonaAdapter());
18
- registerAdapter(ServerAdapter());
19
- registerAdapter(SyntaxThemeNameAdapter());
20
- }
21
-}
22
-
23
-extension IsolatedHiveRegistrar on IsolatedHiveInterface {
24
- void registerAdapters() {
25
- registerAdapter(AppSettingsAdapter());
26
- registerAdapter(ConversationAdapter());
27
- registerAdapter(MessageAdapter());
28
- registerAdapter(PersonaAdapter());
29
- registerAdapter(ServerAdapter());
30
- registerAdapter(SyntaxThemeNameAdapter());
31
- }
32
-}
lib/main.dart
+9
-3
@@ -1,17 +1,23 @@
1
import 'package:flutter/material.dart';
2
import 'package:flutter_riverpod/flutter_riverpod.dart';
3
+import 'package:shared_preferences/shared_preferences.dart';
4
+
5
import 'app.dart';
4
-import 'core/storage/hive_initializer.dart';
6
import 'core/providers/storage_providers.dart';
7
+import 'core/storage/objectbox_store.dart';
8
9
Future<void> main() async {
10
WidgetsFlutterBinding.ensureInitialized();
11
10
- final hiveBoxes = await HiveInitializer.initialize();
12
+ final prefs = await SharedPreferences.getInstance();
13
+ final database = await ObjectBoxStore.create();
14
15
runApp(
16
ProviderScope(
14
- overrides: [hiveBoxesProvider.overrideWithValue(hiveBoxes)],
17
+ overrides: [
18
+ sharedPreferencesProvider.overrideWithValue(prefs),
19
+ databaseProvider.overrideWithValue(database),
20
+ ],
21
child: const App(),
22
),
23
);
lib/objectbox-model.json
new
+327
@@ -0,0 +1,327 @@
1
+{
2
+ "_note1": "KEEP THIS FILE! Check it into a version control system (VCS) like git.",
3
+ "_note2": "ObjectBox manages crucial IDs for your object model. See docs for details.",
4
+ "_note3": "If you have VCS merge conflicts, you must resolve them according to ObjectBox docs.",
5
+ "entities": [
6
+ {
7
+ "id": "1:6060443518060866028",
8
+ "lastPropertyId": "12:768672924450595967",
9
+ "name": "ConversationEntity",
10
+ "properties": [
11
+ {
12
+ "id": "1:7250988890405982940",
13
+ "name": "internalId",
14
+ "type": 6,
15
+ "flags": 1
16
+ },
17
+ {
18
+ "id": "2:6585352959757183870",
19
+ "name": "id",
20
+ "indexId": "1:4841541219215160349",
21
+ "type": 9,
22
+ "flags": 2048
23
+ },
24
+ {
25
+ "id": "3:4528360812098110067",
26
+ "name": "title",
27
+ "type": 9
28
+ },
29
+ {
30
+ "id": "4:6318468918758166025",
31
+ "name": "createdAt",
32
+ "type": 10
33
+ },
34
+ {
35
+ "id": "5:3060834012974707866",
36
+ "name": "updatedAt",
37
+ "type": 10
38
+ },
39
+ {
40
+ "id": "6:617250549526069739",
41
+ "name": "isPinned",
42
+ "type": 1
43
+ },
44
+ {
45
+ "id": "7:5786554614037038633",
46
+ "name": "personaId",
47
+ "type": 9
48
+ },
49
+ {
50
+ "id": "8:2861907472509737489",
51
+ "name": "serverId",
52
+ "type": 9
53
+ },
54
+ {
55
+ "id": "9:7707788543167527562",
56
+ "name": "modelId",
57
+ "type": 9
58
+ },
59
+ {
60
+ "id": "10:6820164520934907732",
61
+ "name": "messageCount",
62
+ "type": 6
63
+ },
64
+ {
65
+ "id": "11:3808591552035911328",
66
+ "name": "lastMessagePreview",
67
+ "type": 9
68
+ },
69
+ {
70
+ "id": "12:768672924450595967",
71
+ "name": "systemPrompt",
72
+ "type": 9
73
+ }
74
+ ],
75
+ "relations": []
76
+ },
77
+ {
78
+ "id": "2:196881068437544233",
79
+ "lastPropertyId": "17:6197342254666211356",
80
+ "name": "MessageEntity",
81
+ "properties": [
82
+ {
83
+ "id": "1:3923220810652427930",
84
+ "name": "internalId",
85
+ "type": 6,
86
+ "flags": 1
87
+ },
88
+ {
89
+ "id": "2:4538486775689812159",
90
+ "name": "id",
91
+ "indexId": "2:2170902273804247015",
92
+ "type": 9,
93
+ "flags": 2048
94
+ },
95
+ {
96
+ "id": "3:2455937359218896797",
97
+ "name": "conversationId",
98
+ "indexId": "3:169510492024268969",
99
+ "type": 11,
100
+ "flags": 520,
101
+ "relationTarget": "ConversationEntity"
102
+ },
103
+ {
104
+ "id": "4:9041846205772997953",
105
+ "name": "conversationUid",
106
+ "indexId": "4:7620462177544383533",
107
+ "type": 9,
108
+ "flags": 2048
109
+ },
110
+ {
111
+ "id": "5:3967309868246732396",
112
+ "name": "roleIndex",
113
+ "type": 5
114
+ },
115
+ {
116
+ "id": "6:5292209498807417151",
117
+ "name": "content",
118
+ "type": 9
119
+ },
120
+ {
121
+ "id": "7:4728980799803110489",
122
+ "name": "createdAt",
123
+ "type": 10
124
+ },
125
+ {
126
+ "id": "8:4723736841925529304",
127
+ "name": "statusIndex",
128
+ "type": 5
129
+ },
130
+ {
131
+ "id": "9:5749988360982169067",
132
+ "name": "modelId",
133
+ "type": 9
134
+ },
135
+ {
136
+ "id": "10:5208151897806410090",
137
+ "name": "tokenCount",
138
+ "type": 6
139
+ },
140
+ {
141
+ "id": "11:5046757142081916519",
142
+ "name": "errorMessage",
143
+ "type": 9
144
+ },
145
+ {
146
+ "id": "12:3014900088652314818",
147
+ "name": "attachmentPathsJson",
148
+ "type": 9
149
+ },
150
+ {
151
+ "id": "13:754466339638382368",
152
+ "name": "generationTimeMs",
153
+ "type": 6
154
+ },
155
+ {
156
+ "id": "14:6735567839050317594",
157
+ "name": "reasoningContent",
158
+ "type": 9
159
+ },
160
+ {
161
+ "id": "15:2001265758949765605",
162
+ "name": "toolCallsJson",
163
+ "type": 9
164
+ },
165
+ {
166
+ "id": "16:6649517364442324198",
167
+ "name": "toolCallId",
168
+ "type": 9
169
+ },
170
+ {
171
+ "id": "17:6197342254666211356",
172
+ "name": "isProcessing",
173
+ "type": 1
174
+ }
175
+ ],
176
+ "relations": []
177
+ },
178
+ {
179
+ "id": "3:2653818534060300073",
180
+ "lastPropertyId": "11:6109868781061218611",
181
+ "name": "PersonaEntity",
182
+ "properties": [
183
+ {
184
+ "id": "1:5387772201885529069",
185
+ "name": "internalId",
186
+ "type": 6,
187
+ "flags": 1
188
+ },
189
+ {
190
+ "id": "2:943547030981649537",
191
+ "name": "id",
192
+ "indexId": "5:271490381912735587",
193
+ "type": 9,
194
+ "flags": 2048
195
+ },
196
+ {
197
+ "id": "3:5296359738005988766",
198
+ "name": "name",
199
+ "type": 9
200
+ },
201
+ {
202
+ "id": "4:1099248784524684443",
203
+ "name": "emoji",
204
+ "type": 9
205
+ },
206
+ {
207
+ "id": "5:5744781536100424228",
208
+ "name": "systemPrompt",
209
+ "type": 9
210
+ },
211
+ {
212
+ "id": "6:7306493291640402510",
213
+ "name": "description",
214
+ "type": 9
215
+ },
216
+ {
217
+ "id": "7:3478346354108827230",
218
+ "name": "isBuiltIn",
219
+ "type": 1
220
+ },
221
+ {
222
+ "id": "8:2536114129168373905",
223
+ "name": "createdAt",
224
+ "type": 10
225
+ },
226
+ {
227
+ "id": "9:2004523499979292665",
228
+ "name": "updatedAt",
229
+ "type": 10
230
+ },
231
+ {
232
+ "id": "10:4803561724777818572",
233
+ "name": "category",
234
+ "type": 9
235
+ },
236
+ {
237
+ "id": "11:6109868781061218611",
238
+ "name": "preferredParamsJson",
239
+ "type": 9
240
+ }
241
+ ],
242
+ "relations": []
243
+ },
244
+ {
245
+ "id": "4:6971506781199819057",
246
+ "lastPropertyId": "12:4662365603702428254",
247
+ "name": "ServerEntity",
248
+ "properties": [
249
+ {
250
+ "id": "1:1193214592990508905",
251
+ "name": "internalId",
252
+ "type": 6,
253
+ "flags": 1
254
+ },
255
+ {
256
+ "id": "2:1949522104474427292",
257
+ "name": "id",
258
+ "indexId": "6:4529386251594949074",
259
+ "type": 9,
260
+ "flags": 2048
261
+ },
262
+ {
263
+ "id": "3:5435197030913688688",
264
+ "name": "name",
265
+ "type": 9
266
+ },
267
+ {
268
+ "id": "4:5859746794128897561",
269
+ "name": "typeIndex",
270
+ "type": 5
271
+ },
272
+ {
273
+ "id": "5:7884292605074234260",
274
+ "name": "host",
275
+ "type": 9
276
+ },
277
+ {
278
+ "id": "6:2516802883134110310",
279
+ "name": "port",
280
+ "type": 6
281
+ },
282
+ {
283
+ "id": "7:9034988768868935282",
284
+ "name": "apiKey",
285
+ "type": 9
286
+ },
287
+ {
288
+ "id": "8:7825400106697971993",
289
+ "name": "isDefault",
290
+ "type": 1
291
+ },
292
+ {
293
+ "id": "9:6761441426018420644",
294
+ "name": "createdAt",
295
+ "type": 10
296
+ },
297
+ {
298
+ "id": "10:9150237084363813165",
299
+ "name": "lastConnectedAt",
300
+ "type": 10
301
+ },
302
+ {
303
+ "id": "11:7342462043799432941",
304
+ "name": "statusIndex",
305
+ "type": 5
306
+ },
307
+ {
308
+ "id": "12:4662365603702428254",
309
+ "name": "iconName",
310
+ "type": 9
311
+ }
312
+ ],
313
+ "relations": []
314
+ }
315
+ ],
316
+ "lastEntityId": "4:6971506781199819057",
317
+ "lastIndexId": "6:4529386251594949074",
318
+ "lastRelationId": "0:0",
319
+ "lastSequenceId": "0:0",
320
+ "modelVersion": 5,
321
+ "modelVersionParserMinimum": 5,
322
+ "retiredEntityUids": [],
323
+ "retiredIndexUids": [],
324
+ "retiredPropertyUids": [],
325
+ "retiredRelationUids": [],
326
+ "version": 1
327
+}
\ No newline at end of file
lib/objectbox.g.dart
new
+1199
@@ -0,0 +1,1199 @@
1
+// GENERATED CODE - DO NOT MODIFY BY HAND
2
+// This code was generated by ObjectBox. To update it run the generator again
3
+// with `dart run build_runner build`.
4
+// See also https://docs.objectbox.io/getting-started#generate-objectbox-code
5
+
6
+// ignore_for_file: camel_case_types, depend_on_referenced_packages
7
+// coverage:ignore-file
8
+
9
+import 'dart:typed_data';
10
+
11
+import 'package:flat_buffers/flat_buffers.dart' as fb;
12
+import 'package:objectbox/internal.dart'
13
+ as obx_int; // generated code can access "internal" functionality
14
+import 'package:objectbox/objectbox.dart' as obx;
15
+import 'package:objectbox_flutter_libs/objectbox_flutter_libs.dart';
16
+
17
+import 'core/storage/entities.dart';
18
+
19
+export 'package:objectbox/objectbox.dart'; // so that callers only have to import this file
20
+
21
+final _entities = <obx_int.ModelEntity>[
22
+ obx_int.ModelEntity(
23
+ id: const obx_int.IdUid(1, 6060443518060866028),
24
+ name: 'ConversationEntity',
25
+ lastPropertyId: const obx_int.IdUid(12, 768672924450595967),
26
+ flags: 0,
27
+ properties: <obx_int.ModelProperty>[
28
+ obx_int.ModelProperty(
29
+ id: const obx_int.IdUid(1, 7250988890405982940),
30
+ name: 'internalId',
31
+ type: 6,
32
+ flags: 1,
33
+ ),
34
+ obx_int.ModelProperty(
35
+ id: const obx_int.IdUid(2, 6585352959757183870),
36
+ name: 'id',
37
+ type: 9,
38
+ flags: 2048,
39
+ indexId: const obx_int.IdUid(1, 4841541219215160349),
40
+ ),
41
+ obx_int.ModelProperty(
42
+ id: const obx_int.IdUid(3, 4528360812098110067),
43
+ name: 'title',
44
+ type: 9,
45
+ flags: 0,
46
+ ),
47
+ obx_int.ModelProperty(
48
+ id: const obx_int.IdUid(4, 6318468918758166025),
49
+ name: 'createdAt',
50
+ type: 10,
51
+ flags: 0,
52
+ ),
53
+ obx_int.ModelProperty(
54
+ id: const obx_int.IdUid(5, 3060834012974707866),
55
+ name: 'updatedAt',
56
+ type: 10,
57
+ flags: 0,
58
+ ),
59
+ obx_int.ModelProperty(
60
+ id: const obx_int.IdUid(6, 617250549526069739),
61
+ name: 'isPinned',
62
+ type: 1,
63
+ flags: 0,
64
+ ),
65
+ obx_int.ModelProperty(
66
+ id: const obx_int.IdUid(7, 5786554614037038633),
67
+ name: 'personaId',
68
+ type: 9,
69
+ flags: 0,
70
+ ),
71
+ obx_int.ModelProperty(
72
+ id: const obx_int.IdUid(8, 2861907472509737489),
73
+ name: 'serverId',
74
+ type: 9,
75
+ flags: 0,
76
+ ),
77
+ obx_int.ModelProperty(
78
+ id: const obx_int.IdUid(9, 7707788543167527562),
79
+ name: 'modelId',
80
+ type: 9,
81
+ flags: 0,
82
+ ),
83
+ obx_int.ModelProperty(
84
+ id: const obx_int.IdUid(10, 6820164520934907732),
85
+ name: 'messageCount',
86
+ type: 6,
87
+ flags: 0,
88
+ ),
89
+ obx_int.ModelProperty(
90
+ id: const obx_int.IdUid(11, 3808591552035911328),
91
+ name: 'lastMessagePreview',
92
+ type: 9,
93
+ flags: 0,
94
+ ),
95
+ obx_int.ModelProperty(
96
+ id: const obx_int.IdUid(12, 768672924450595967),
97
+ name: 'systemPrompt',
98
+ type: 9,
99
+ flags: 0,
100
+ ),
101
+ ],
102
+ relations: <obx_int.ModelRelation>[],
103
+ backlinks: <obx_int.ModelBacklink>[
104
+ obx_int.ModelBacklink(
105
+ name: 'messages',
106
+ srcEntity: 'MessageEntity',
107
+ srcField: '',
108
+ ),
109
+ ],
110
+ ),
111
+ obx_int.ModelEntity(
112
+ id: const obx_int.IdUid(2, 196881068437544233),
113
+ name: 'MessageEntity',
114
+ lastPropertyId: const obx_int.IdUid(17, 6197342254666211356),
115
+ flags: 0,
116
+ properties: <obx_int.ModelProperty>[
117
+ obx_int.ModelProperty(
118
+ id: const obx_int.IdUid(1, 3923220810652427930),
119
+ name: 'internalId',
120
+ type: 6,
121
+ flags: 1,
122
+ ),
123
+ obx_int.ModelProperty(
124
+ id: const obx_int.IdUid(2, 4538486775689812159),
125
+ name: 'id',
126
+ type: 9,
127
+ flags: 2048,
128
+ indexId: const obx_int.IdUid(2, 2170902273804247015),
129
+ ),
130
+ obx_int.ModelProperty(
131
+ id: const obx_int.IdUid(3, 2455937359218896797),
132
+ name: 'conversationId',
133
+ type: 11,
134
+ flags: 520,
135
+ indexId: const obx_int.IdUid(3, 169510492024268969),
136
+ relationField: 'conversation',
137
+ relationTarget: 'ConversationEntity',
138
+ ),
139
+ obx_int.ModelProperty(
140
+ id: const obx_int.IdUid(4, 9041846205772997953),
141
+ name: 'conversationUid',
142
+ type: 9,
143
+ flags: 2048,
144
+ indexId: const obx_int.IdUid(4, 7620462177544383533),
145
+ ),
146
+ obx_int.ModelProperty(
147
+ id: const obx_int.IdUid(5, 3967309868246732396),
148
+ name: 'roleIndex',
149
+ type: 5,
150
+ flags: 0,
151
+ ),
152
+ obx_int.ModelProperty(
153
+ id: const obx_int.IdUid(6, 5292209498807417151),
154
+ name: 'content',
155
+ type: 9,
156
+ flags: 0,
157
+ ),
158
+ obx_int.ModelProperty(
159
+ id: const obx_int.IdUid(7, 4728980799803110489),
160
+ name: 'createdAt',
161
+ type: 10,
162
+ flags: 0,
163
+ ),
164
+ obx_int.ModelProperty(
165
+ id: const obx_int.IdUid(8, 4723736841925529304),
166
+ name: 'statusIndex',
167
+ type: 5,
168
+ flags: 0,
169
+ ),
170
+ obx_int.ModelProperty(
171
+ id: const obx_int.IdUid(9, 5749988360982169067),
172
+ name: 'modelId',
173
+ type: 9,
174
+ flags: 0,
175
+ ),
176
+ obx_int.ModelProperty(
177
+ id: const obx_int.IdUid(10, 5208151897806410090),
178
+ name: 'tokenCount',
179
+ type: 6,
180
+ flags: 0,
181
+ ),
182
+ obx_int.ModelProperty(
183
+ id: const obx_int.IdUid(11, 5046757142081916519),
184
+ name: 'errorMessage',
185
+ type: 9,
186
+ flags: 0,
187
+ ),
188
+ obx_int.ModelProperty(
189
+ id: const obx_int.IdUid(12, 3014900088652314818),
190
+ name: 'attachmentPathsJson',
191
+ type: 9,
192
+ flags: 0,
193
+ ),
194
+ obx_int.ModelProperty(
195
+ id: const obx_int.IdUid(13, 754466339638382368),
196
+ name: 'generationTimeMs',
197
+ type: 6,
198
+ flags: 0,
199
+ ),
200
+ obx_int.ModelProperty(
201
+ id: const obx_int.IdUid(14, 6735567839050317594),
202
+ name: 'reasoningContent',
203
+ type: 9,
204
+ flags: 0,
205
+ ),
206
+ obx_int.ModelProperty(
207
+ id: const obx_int.IdUid(15, 2001265758949765605),
208
+ name: 'toolCallsJson',
209
+ type: 9,
210
+ flags: 0,
211
+ ),
212
+ obx_int.ModelProperty(
213
+ id: const obx_int.IdUid(16, 6649517364442324198),
214
+ name: 'toolCallId',
215
+ type: 9,
216
+ flags: 0,
217
+ ),
218
+ obx_int.ModelProperty(
219
+ id: const obx_int.IdUid(17, 6197342254666211356),
220
+ name: 'isProcessing',
221
+ type: 1,
222
+ flags: 0,
223
+ ),
224
+ ],
225
+ relations: <obx_int.ModelRelation>[],
226
+ backlinks: <obx_int.ModelBacklink>[],
227
+ ),
228
+ obx_int.ModelEntity(
229
+ id: const obx_int.IdUid(3, 2653818534060300073),
230
+ name: 'PersonaEntity',
231
+ lastPropertyId: const obx_int.IdUid(11, 6109868781061218611),
232
+ flags: 0,
233
+ properties: <obx_int.ModelProperty>[
234
+ obx_int.ModelProperty(
235
+ id: const obx_int.IdUid(1, 5387772201885529069),
236
+ name: 'internalId',
237
+ type: 6,
238
+ flags: 1,
239
+ ),
240
+ obx_int.ModelProperty(
241
+ id: const obx_int.IdUid(2, 943547030981649537),
242
+ name: 'id',
243
+ type: 9,
244
+ flags: 2048,
245
+ indexId: const obx_int.IdUid(5, 271490381912735587),
246
+ ),
247
+ obx_int.ModelProperty(
248
+ id: const obx_int.IdUid(3, 5296359738005988766),
249
+ name: 'name',
250
+ type: 9,
251
+ flags: 0,
252
+ ),
253
+ obx_int.ModelProperty(
254
+ id: const obx_int.IdUid(4, 1099248784524684443),
255
+ name: 'emoji',
256
+ type: 9,
257
+ flags: 0,
258
+ ),
259
+ obx_int.ModelProperty(
260
+ id: const obx_int.IdUid(5, 5744781536100424228),
261
+ name: 'systemPrompt',
262
+ type: 9,
263
+ flags: 0,
264
+ ),
265
+ obx_int.ModelProperty(
266
+ id: const obx_int.IdUid(6, 7306493291640402510),
267
+ name: 'description',
268
+ type: 9,
269
+ flags: 0,
270
+ ),
271
+ obx_int.ModelProperty(
272
+ id: const obx_int.IdUid(7, 3478346354108827230),
273
+ name: 'isBuiltIn',
274
+ type: 1,
275
+ flags: 0,
276
+ ),
277
+ obx_int.ModelProperty(
278
+ id: const obx_int.IdUid(8, 2536114129168373905),
279
+ name: 'createdAt',
280
+ type: 10,
281
+ flags: 0,
282
+ ),
283
+ obx_int.ModelProperty(
284
+ id: const obx_int.IdUid(9, 2004523499979292665),
285
+ name: 'updatedAt',
286
+ type: 10,
287
+ flags: 0,
288
+ ),
289
+ obx_int.ModelProperty(
290
+ id: const obx_int.IdUid(10, 4803561724777818572),
291
+ name: 'category',
292
+ type: 9,
293
+ flags: 0,
294
+ ),
295
+ obx_int.ModelProperty(
296
+ id: const obx_int.IdUid(11, 6109868781061218611),
297
+ name: 'preferredParamsJson',
298
+ type: 9,
299
+ flags: 0,
300
+ ),
301
+ ],
302
+ relations: <obx_int.ModelRelation>[],
303
+ backlinks: <obx_int.ModelBacklink>[],
304
+ ),
305
+ obx_int.ModelEntity(
306
+ id: const obx_int.IdUid(4, 6971506781199819057),
307
+ name: 'ServerEntity',
308
+ lastPropertyId: const obx_int.IdUid(12, 4662365603702428254),
309
+ flags: 0,
310
+ properties: <obx_int.ModelProperty>[
311
+ obx_int.ModelProperty(
312
+ id: const obx_int.IdUid(1, 1193214592990508905),
313
+ name: 'internalId',
314
+ type: 6,
315
+ flags: 1,
316
+ ),
317
+ obx_int.ModelProperty(
318
+ id: const obx_int.IdUid(2, 1949522104474427292),
319
+ name: 'id',
320
+ type: 9,
321
+ flags: 2048,
322
+ indexId: const obx_int.IdUid(6, 4529386251594949074),
323
+ ),
324
+ obx_int.ModelProperty(
325
+ id: const obx_int.IdUid(3, 5435197030913688688),
326
+ name: 'name',
327
+ type: 9,
328
+ flags: 0,
329
+ ),
330
+ obx_int.ModelProperty(
331
+ id: const obx_int.IdUid(4, 5859746794128897561),
332
+ name: 'typeIndex',
333
+ type: 5,
334
+ flags: 0,
335
+ ),
336
+ obx_int.ModelProperty(
337
+ id: const obx_int.IdUid(5, 7884292605074234260),
338
+ name: 'host',
339
+ type: 9,
340
+ flags: 0,
341
+ ),
342
+ obx_int.ModelProperty(
343
+ id: const obx_int.IdUid(6, 2516802883134110310),
344
+ name: 'port',
345
+ type: 6,
346
+ flags: 0,
347
+ ),
348
+ obx_int.ModelProperty(
349
+ id: const obx_int.IdUid(7, 9034988768868935282),
350
+ name: 'apiKey',
351
+ type: 9,
352
+ flags: 0,
353
+ ),
354
+ obx_int.ModelProperty(
355
+ id: const obx_int.IdUid(8, 7825400106697971993),
356
+ name: 'isDefault',
357
+ type: 1,
358
+ flags: 0,
359
+ ),
360
+ obx_int.ModelProperty(
361
+ id: const obx_int.IdUid(9, 6761441426018420644),
362
+ name: 'createdAt',
363
+ type: 10,
364
+ flags: 0,
365
+ ),
366
+ obx_int.ModelProperty(
367
+ id: const obx_int.IdUid(10, 9150237084363813165),
368
+ name: 'lastConnectedAt',
369
+ type: 10,
370
+ flags: 0,
371
+ ),
372
+ obx_int.ModelProperty(
373
+ id: const obx_int.IdUid(11, 7342462043799432941),
374
+ name: 'statusIndex',
375
+ type: 5,
376
+ flags: 0,
377
+ ),
378
+ obx_int.ModelProperty(
379
+ id: const obx_int.IdUid(12, 4662365603702428254),
380
+ name: 'iconName',
381
+ type: 9,
382
+ flags: 0,
383
+ ),
384
+ ],
385
+ relations: <obx_int.ModelRelation>[],
386
+ backlinks: <obx_int.ModelBacklink>[],
387
+ ),
388
+];
389
+
390
+/// Shortcut for [obx.Store.new] that passes [getObjectBoxModel] and for Flutter
391
+/// apps by default a [directory] using `defaultStoreDirectory()` from the
392
+/// ObjectBox Flutter library.
393
+///
394
+/// Note: for desktop apps it is recommended to specify a unique [directory].
395
+///
396
+/// See [obx.Store.new] for an explanation of all parameters.
397
+///
398
+/// For Flutter apps, also calls `loadObjectBoxLibraryAndroidCompat()` from
399
+/// the ObjectBox Flutter library to fix loading the native ObjectBox library
400
+/// on Android 6 and older.
401
+Future<obx.Store> openStore({
402
+ String? directory,
403
+ int? maxDBSizeInKB,
404
+ int? maxDataSizeInKB,
405
+ int? fileMode,
406
+ int? maxReaders,
407
+ bool queriesCaseSensitiveDefault = true,
408
+ String? macosApplicationGroup,
409
+}) async {
410
+ await loadObjectBoxLibraryAndroidCompat();
411
+ return obx.Store(
412
+ getObjectBoxModel(),
413
+ directory: directory ?? (await defaultStoreDirectory()).path,
414
+ maxDBSizeInKB: maxDBSizeInKB,
415
+ maxDataSizeInKB: maxDataSizeInKB,
416
+ fileMode: fileMode,
417
+ maxReaders: maxReaders,
418
+ queriesCaseSensitiveDefault: queriesCaseSensitiveDefault,
419
+ macosApplicationGroup: macosApplicationGroup,
420
+ );
421
+}
422
+
423
+/// Returns the ObjectBox model definition for this project for use with
424
+/// [obx.Store.new].
425
+obx_int.ModelDefinition getObjectBoxModel() {
426
+ final model = obx_int.ModelInfo(
427
+ // If this version is not found, it means that this file was generated
428
+ // with an older version of the ObjectBox Dart generator.
429
+ // Please regenerate this file with the current generator version.
430
+ // Typically, this is done with `dart run build_runner build`.
431
+ generatorVersion: obx_int.GeneratorVersion.v2025_12_16,
432
+ entities: _entities,
433
+ lastEntityId: const obx_int.IdUid(4, 6971506781199819057),
434
+ lastIndexId: const obx_int.IdUid(6, 4529386251594949074),
435
+ lastRelationId: const obx_int.IdUid(0, 0),
436
+ lastSequenceId: const obx_int.IdUid(0, 0),
437
+ retiredEntityUids: const [],
438
+ retiredIndexUids: const [],
439
+ retiredPropertyUids: const [],
440
+ retiredRelationUids: const [],
441
+ modelVersion: 5,
442
+ modelVersionParserMinimum: 5,
443
+ version: 1,
444
+ );
445
+
446
+ final bindings = <Type, obx_int.EntityDefinition>{
447
+ ConversationEntity: obx_int.EntityDefinition<ConversationEntity>(
448
+ model: _entities[0],
449
+ toOneRelations: (ConversationEntity object) => [],
450
+ toManyRelations: (ConversationEntity object) => {
451
+ obx_int.RelInfo<MessageEntity>.toOneBacklink(
452
+ 3,
453
+ object.internalId,
454
+ (MessageEntity srcObject) => srcObject.conversation,
455
+ ): object.messages,
456
+ },
457
+ getId: (ConversationEntity object) => object.internalId,
458
+ setId: (ConversationEntity object, int id) {
459
+ object.internalId = id;
460
+ },
461
+ objectToFB: (ConversationEntity object, fb.Builder fbb) {
462
+ final idOffset = fbb.writeString(object.id);
463
+ final titleOffset = fbb.writeString(object.title);
464
+ final personaIdOffset = object.personaId == null
465
+ ? null
466
+ : fbb.writeString(object.personaId!);
467
+ final serverIdOffset = object.serverId == null
468
+ ? null
469
+ : fbb.writeString(object.serverId!);
470
+ final modelIdOffset = object.modelId == null
471
+ ? null
472
+ : fbb.writeString(object.modelId!);
473
+ final lastMessagePreviewOffset = object.lastMessagePreview == null
474
+ ? null
475
+ : fbb.writeString(object.lastMessagePreview!);
476
+ final systemPromptOffset = object.systemPrompt == null
477
+ ? null
478
+ : fbb.writeString(object.systemPrompt!);
479
+ fbb.startTable(13);
480
+ fbb.addInt64(0, object.internalId);
481
+ fbb.addOffset(1, idOffset);
482
+ fbb.addOffset(2, titleOffset);
483
+ fbb.addInt64(3, object.createdAt.millisecondsSinceEpoch);
484
+ fbb.addInt64(4, object.updatedAt.millisecondsSinceEpoch);
485
+ fbb.addBool(5, object.isPinned);
486
+ fbb.addOffset(6, personaIdOffset);
487
+ fbb.addOffset(7, serverIdOffset);
488
+ fbb.addOffset(8, modelIdOffset);
489
+ fbb.addInt64(9, object.messageCount);
490
+ fbb.addOffset(10, lastMessagePreviewOffset);
491
+ fbb.addOffset(11, systemPromptOffset);
492
+ fbb.finish(fbb.endTable());
493
+ return object.internalId;
494
+ },
495
+ objectFromFB: (obx.Store store, ByteData fbData) {
496
+ final buffer = fb.BufferContext(fbData);
497
+ final rootOffset = buffer.derefObject(0);
498
+ final internalIdParam = const fb.Int64Reader().vTableGet(
499
+ buffer,
500
+ rootOffset,
501
+ 4,
502
+ 0,
503
+ );
504
+ final idParam = const fb.StringReader(
505
+ asciiOptimization: true,
506
+ ).vTableGet(buffer, rootOffset, 6, '');
507
+ final titleParam = const fb.StringReader(
508
+ asciiOptimization: true,
509
+ ).vTableGet(buffer, rootOffset, 8, '');
510
+ final createdAtParam = DateTime.fromMillisecondsSinceEpoch(
511
+ const fb.Int64Reader().vTableGet(buffer, rootOffset, 10, 0),
512
+ );
513
+ final updatedAtParam = DateTime.fromMillisecondsSinceEpoch(
514
+ const fb.Int64Reader().vTableGet(buffer, rootOffset, 12, 0),
515
+ );
516
+ final isPinnedParam = const fb.BoolReader().vTableGet(
517
+ buffer,
518
+ rootOffset,
519
+ 14,
520
+ false,
521
+ );
522
+ final personaIdParam = const fb.StringReader(
523
+ asciiOptimization: true,
524
+ ).vTableGetNullable(buffer, rootOffset, 16);
525
+ final serverIdParam = const fb.StringReader(
526
+ asciiOptimization: true,
527
+ ).vTableGetNullable(buffer, rootOffset, 18);
528
+ final modelIdParam = const fb.StringReader(
529
+ asciiOptimization: true,
530
+ ).vTableGetNullable(buffer, rootOffset, 20);
531
+ final messageCountParam = const fb.Int64Reader().vTableGet(
532
+ buffer,
533
+ rootOffset,
534
+ 22,
535
+ 0,
536
+ );
537
+ final lastMessagePreviewParam = const fb.StringReader(
538
+ asciiOptimization: true,
539
+ ).vTableGetNullable(buffer, rootOffset, 24);
540
+ final systemPromptParam = const fb.StringReader(
541
+ asciiOptimization: true,
542
+ ).vTableGetNullable(buffer, rootOffset, 26);
543
+ final object = ConversationEntity(
544
+ internalId: internalIdParam,
545
+ id: idParam,
546
+ title: titleParam,
547
+ createdAt: createdAtParam,
548
+ updatedAt: updatedAtParam,
549
+ isPinned: isPinnedParam,
550
+ personaId: personaIdParam,
551
+ serverId: serverIdParam,
552
+ modelId: modelIdParam,
553
+ messageCount: messageCountParam,
554
+ lastMessagePreview: lastMessagePreviewParam,
555
+ systemPrompt: systemPromptParam,
556
+ );
557
+ obx_int.InternalToManyAccess.setRelInfo<ConversationEntity>(
558
+ object.messages,
559
+ store,
560
+ obx_int.RelInfo<MessageEntity>.toOneBacklink(
561
+ 3,
562
+ object.internalId,
563
+ (MessageEntity srcObject) => srcObject.conversation,
564
+ ),
565
+ );
566
+ return object;
567
+ },
568
+ ),
569
+ MessageEntity: obx_int.EntityDefinition<MessageEntity>(
570
+ model: _entities[1],
571
+ toOneRelations: (MessageEntity object) => [object.conversation],
572
+ toManyRelations: (MessageEntity object) => {},
573
+ getId: (MessageEntity object) => object.internalId,
574
+ setId: (MessageEntity object, int id) {
575
+ object.internalId = id;
576
+ },
577
+ objectToFB: (MessageEntity object, fb.Builder fbb) {
578
+ final idOffset = fbb.writeString(object.id);
579
+ final conversationUidOffset = fbb.writeString(object.conversationUid);
580
+ final contentOffset = fbb.writeString(object.content);
581
+ final modelIdOffset = object.modelId == null
582
+ ? null
583
+ : fbb.writeString(object.modelId!);
584
+ final errorMessageOffset = object.errorMessage == null
585
+ ? null
586
+ : fbb.writeString(object.errorMessage!);
587
+ final attachmentPathsJsonOffset = object.attachmentPathsJson == null
588
+ ? null
589
+ : fbb.writeString(object.attachmentPathsJson!);
590
+ final reasoningContentOffset = object.reasoningContent == null
591
+ ? null
592
+ : fbb.writeString(object.reasoningContent!);
593
+ final toolCallsJsonOffset = object.toolCallsJson == null
594
+ ? null
595
+ : fbb.writeString(object.toolCallsJson!);
596
+ final toolCallIdOffset = object.toolCallId == null
597
+ ? null
598
+ : fbb.writeString(object.toolCallId!);
599
+ fbb.startTable(18);
600
+ fbb.addInt64(0, object.internalId);
601
+ fbb.addOffset(1, idOffset);
602
+ fbb.addInt64(2, object.conversation.targetId);
603
+ fbb.addOffset(3, conversationUidOffset);
604
+ fbb.addInt32(4, object.roleIndex);
605
+ fbb.addOffset(5, contentOffset);
606
+ fbb.addInt64(6, object.createdAt.millisecondsSinceEpoch);
607
+ fbb.addInt32(7, object.statusIndex);
608
+ fbb.addOffset(8, modelIdOffset);
609
+ fbb.addInt64(9, object.tokenCount);
610
+ fbb.addOffset(10, errorMessageOffset);
611
+ fbb.addOffset(11, attachmentPathsJsonOffset);
612
+ fbb.addInt64(12, object.generationTimeMs);
613
+ fbb.addOffset(13, reasoningContentOffset);
614
+ fbb.addOffset(14, toolCallsJsonOffset);
615
+ fbb.addOffset(15, toolCallIdOffset);
616
+ fbb.addBool(16, object.isProcessing);
617
+ fbb.finish(fbb.endTable());
618
+ return object.internalId;
619
+ },
620
+ objectFromFB: (obx.Store store, ByteData fbData) {
621
+ final buffer = fb.BufferContext(fbData);
622
+ final rootOffset = buffer.derefObject(0);
623
+ final internalIdParam = const fb.Int64Reader().vTableGet(
624
+ buffer,
625
+ rootOffset,
626
+ 4,
627
+ 0,
628
+ );
629
+ final idParam = const fb.StringReader(
630
+ asciiOptimization: true,
631
+ ).vTableGet(buffer, rootOffset, 6, '');
632
+ final conversationUidParam = const fb.StringReader(
633
+ asciiOptimization: true,
634
+ ).vTableGet(buffer, rootOffset, 10, '');
635
+ final roleIndexParam = const fb.Int32Reader().vTableGet(
636
+ buffer,
637
+ rootOffset,
638
+ 12,
639
+ 0,
640
+ );
641
+ final contentParam = const fb.StringReader(
642
+ asciiOptimization: true,
643
+ ).vTableGet(buffer, rootOffset, 14, '');
644
+ final createdAtParam = DateTime.fromMillisecondsSinceEpoch(
645
+ const fb.Int64Reader().vTableGet(buffer, rootOffset, 16, 0),
646
+ );
647
+ final statusIndexParam = const fb.Int32Reader().vTableGet(
648
+ buffer,
649
+ rootOffset,
650
+ 18,
651
+ 0,
652
+ );
653
+ final modelIdParam = const fb.StringReader(
654
+ asciiOptimization: true,
655
+ ).vTableGetNullable(buffer, rootOffset, 20);
656
+ final tokenCountParam = const fb.Int64Reader().vTableGetNullable(
657
+ buffer,
658
+ rootOffset,
659
+ 22,
660
+ );
661
+ final errorMessageParam = const fb.StringReader(
662
+ asciiOptimization: true,
663
+ ).vTableGetNullable(buffer, rootOffset, 24);
664
+ final attachmentPathsJsonParam = const fb.StringReader(
665
+ asciiOptimization: true,
666
+ ).vTableGetNullable(buffer, rootOffset, 26);
667
+ final generationTimeMsParam = const fb.Int64Reader().vTableGetNullable(
668
+ buffer,
669
+ rootOffset,
670
+ 28,
671
+ );
672
+ final reasoningContentParam = const fb.StringReader(
673
+ asciiOptimization: true,
674
+ ).vTableGetNullable(buffer, rootOffset, 30);
675
+ final toolCallsJsonParam = const fb.StringReader(
676
+ asciiOptimization: true,
677
+ ).vTableGetNullable(buffer, rootOffset, 32);
678
+ final toolCallIdParam = const fb.StringReader(
679
+ asciiOptimization: true,
680
+ ).vTableGetNullable(buffer, rootOffset, 34);
681
+ final isProcessingParam = const fb.BoolReader().vTableGet(
682
+ buffer,
683
+ rootOffset,
684
+ 36,
685
+ false,
686
+ );
687
+ final object = MessageEntity(
688
+ internalId: internalIdParam,
689
+ id: idParam,
690
+ conversationUid: conversationUidParam,
691
+ roleIndex: roleIndexParam,
692
+ content: contentParam,
693
+ createdAt: createdAtParam,
694
+ statusIndex: statusIndexParam,
695
+ modelId: modelIdParam,
696
+ tokenCount: tokenCountParam,
697
+ errorMessage: errorMessageParam,
698
+ attachmentPathsJson: attachmentPathsJsonParam,
699
+ generationTimeMs: generationTimeMsParam,
700
+ reasoningContent: reasoningContentParam,
701
+ toolCallsJson: toolCallsJsonParam,
702
+ toolCallId: toolCallIdParam,
703
+ isProcessing: isProcessingParam,
704
+ );
705
+ object.conversation.targetId = const fb.Int64Reader().vTableGet(
706
+ buffer,
707
+ rootOffset,
708
+ 8,
709
+ 0,
710
+ );
711
+ object.conversation.attach(store);
712
+ return object;
713
+ },
714
+ ),
715
+ PersonaEntity: obx_int.EntityDefinition<PersonaEntity>(
716
+ model: _entities[2],
717
+ toOneRelations: (PersonaEntity object) => [],
718
+ toManyRelations: (PersonaEntity object) => {},
719
+ getId: (PersonaEntity object) => object.internalId,
720
+ setId: (PersonaEntity object, int id) {
721
+ object.internalId = id;
722
+ },
723
+ objectToFB: (PersonaEntity object, fb.Builder fbb) {
724
+ final idOffset = fbb.writeString(object.id);
725
+ final nameOffset = fbb.writeString(object.name);
726
+ final emojiOffset = fbb.writeString(object.emoji);
727
+ final systemPromptOffset = fbb.writeString(object.systemPrompt);
728
+ final descriptionOffset = object.description == null
729
+ ? null
730
+ : fbb.writeString(object.description!);
731
+ final categoryOffset = object.category == null
732
+ ? null
733
+ : fbb.writeString(object.category!);
734
+ final preferredParamsJsonOffset = object.preferredParamsJson == null
735
+ ? null
736
+ : fbb.writeString(object.preferredParamsJson!);
737
+ fbb.startTable(12);
738
+ fbb.addInt64(0, object.internalId);
739
+ fbb.addOffset(1, idOffset);
740
+ fbb.addOffset(2, nameOffset);
741
+ fbb.addOffset(3, emojiOffset);
742
+ fbb.addOffset(4, systemPromptOffset);
743
+ fbb.addOffset(5, descriptionOffset);
744
+ fbb.addBool(6, object.isBuiltIn);
745
+ fbb.addInt64(7, object.createdAt.millisecondsSinceEpoch);
746
+ fbb.addInt64(8, object.updatedAt.millisecondsSinceEpoch);
747
+ fbb.addOffset(9, categoryOffset);
748
+ fbb.addOffset(10, preferredParamsJsonOffset);
749
+ fbb.finish(fbb.endTable());
750
+ return object.internalId;
751
+ },
752
+ objectFromFB: (obx.Store store, ByteData fbData) {
753
+ final buffer = fb.BufferContext(fbData);
754
+ final rootOffset = buffer.derefObject(0);
755
+ final internalIdParam = const fb.Int64Reader().vTableGet(
756
+ buffer,
757
+ rootOffset,
758
+ 4,
759
+ 0,
760
+ );
761
+ final idParam = const fb.StringReader(
762
+ asciiOptimization: true,
763
+ ).vTableGet(buffer, rootOffset, 6, '');
764
+ final nameParam = const fb.StringReader(
765
+ asciiOptimization: true,
766
+ ).vTableGet(buffer, rootOffset, 8, '');
767
+ final emojiParam = const fb.StringReader(
768
+ asciiOptimization: true,
769
+ ).vTableGet(buffer, rootOffset, 10, '');
770
+ final systemPromptParam = const fb.StringReader(
771
+ asciiOptimization: true,
772
+ ).vTableGet(buffer, rootOffset, 12, '');
773
+ final descriptionParam = const fb.StringReader(
774
+ asciiOptimization: true,
775
+ ).vTableGetNullable(buffer, rootOffset, 14);
776
+ final isBuiltInParam = const fb.BoolReader().vTableGet(
777
+ buffer,
778
+ rootOffset,
779
+ 16,
780
+ false,
781
+ );
782
+ final createdAtParam = DateTime.fromMillisecondsSinceEpoch(
783
+ const fb.Int64Reader().vTableGet(buffer, rootOffset, 18, 0),
784
+ );
785
+ final updatedAtParam = DateTime.fromMillisecondsSinceEpoch(
786
+ const fb.Int64Reader().vTableGet(buffer, rootOffset, 20, 0),
787
+ );
788
+ final categoryParam = const fb.StringReader(
789
+ asciiOptimization: true,
790
+ ).vTableGetNullable(buffer, rootOffset, 22);
791
+ final preferredParamsJsonParam = const fb.StringReader(
792
+ asciiOptimization: true,
793
+ ).vTableGetNullable(buffer, rootOffset, 24);
794
+ final object = PersonaEntity(
795
+ internalId: internalIdParam,
796
+ id: idParam,
797
+ name: nameParam,
798
+ emoji: emojiParam,
799
+ systemPrompt: systemPromptParam,
800
+ description: descriptionParam,
801
+ isBuiltIn: isBuiltInParam,
802
+ createdAt: createdAtParam,
803
+ updatedAt: updatedAtParam,
804
+ category: categoryParam,
805
+ preferredParamsJson: preferredParamsJsonParam,
806
+ );
807
+
808
+ return object;
809
+ },
810
+ ),
811
+ ServerEntity: obx_int.EntityDefinition<ServerEntity>(
812
+ model: _entities[3],
813
+ toOneRelations: (ServerEntity object) => [],
814
+ toManyRelations: (ServerEntity object) => {},
815
+ getId: (ServerEntity object) => object.internalId,
816
+ setId: (ServerEntity object, int id) {
817
+ object.internalId = id;
818
+ },
819
+ objectToFB: (ServerEntity object, fb.Builder fbb) {
820
+ final idOffset = fbb.writeString(object.id);
821
+ final nameOffset = fbb.writeString(object.name);
822
+ final hostOffset = fbb.writeString(object.host);
823
+ final apiKeyOffset = object.apiKey == null
824
+ ? null
825
+ : fbb.writeString(object.apiKey!);
826
+ final iconNameOffset = object.iconName == null
827
+ ? null
828
+ : fbb.writeString(object.iconName!);
829
+ fbb.startTable(13);
830
+ fbb.addInt64(0, object.internalId);
831
+ fbb.addOffset(1, idOffset);
832
+ fbb.addOffset(2, nameOffset);
833
+ fbb.addInt32(3, object.typeIndex);
834
+ fbb.addOffset(4, hostOffset);
835
+ fbb.addInt64(5, object.port);
836
+ fbb.addOffset(6, apiKeyOffset);
837
+ fbb.addBool(7, object.isDefault);
838
+ fbb.addInt64(8, object.createdAt.millisecondsSinceEpoch);
839
+ fbb.addInt64(9, object.lastConnectedAt.millisecondsSinceEpoch);
840
+ fbb.addInt32(10, object.statusIndex);
841
+ fbb.addOffset(11, iconNameOffset);
842
+ fbb.finish(fbb.endTable());
843
+ return object.internalId;
844
+ },
845
+ objectFromFB: (obx.Store store, ByteData fbData) {
846
+ final buffer = fb.BufferContext(fbData);
847
+ final rootOffset = buffer.derefObject(0);
848
+ final internalIdParam = const fb.Int64Reader().vTableGet(
849
+ buffer,
850
+ rootOffset,
851
+ 4,
852
+ 0,
853
+ );
854
+ final idParam = const fb.StringReader(
855
+ asciiOptimization: true,
856
+ ).vTableGet(buffer, rootOffset, 6, '');
857
+ final nameParam = const fb.StringReader(
858
+ asciiOptimization: true,
859
+ ).vTableGet(buffer, rootOffset, 8, '');
860
+ final typeIndexParam = const fb.Int32Reader().vTableGet(
861
+ buffer,
862
+ rootOffset,
863
+ 10,
864
+ 0,
865
+ );
866
+ final hostParam = const fb.StringReader(
867
+ asciiOptimization: true,
868
+ ).vTableGet(buffer, rootOffset, 12, '');
869
+ final portParam = const fb.Int64Reader().vTableGet(
870
+ buffer,
871
+ rootOffset,
872
+ 14,
873
+ 0,
874
+ );
875
+ final apiKeyParam = const fb.StringReader(
876
+ asciiOptimization: true,
877
+ ).vTableGetNullable(buffer, rootOffset, 16);
878
+ final isDefaultParam = const fb.BoolReader().vTableGet(
879
+ buffer,
880
+ rootOffset,
881
+ 18,
882
+ false,
883
+ );
884
+ final createdAtParam = DateTime.fromMillisecondsSinceEpoch(
885
+ const fb.Int64Reader().vTableGet(buffer, rootOffset, 20, 0),
886
+ );
887
+ final lastConnectedAtParam = DateTime.fromMillisecondsSinceEpoch(
888
+ const fb.Int64Reader().vTableGet(buffer, rootOffset, 22, 0),
889
+ );
890
+ final statusIndexParam = const fb.Int32Reader().vTableGet(
891
+ buffer,
892
+ rootOffset,
893
+ 24,
894
+ 0,
895
+ );
896
+ final iconNameParam = const fb.StringReader(
897
+ asciiOptimization: true,
898
+ ).vTableGetNullable(buffer, rootOffset, 26);
899
+ final object = ServerEntity(
900
+ internalId: internalIdParam,
901
+ id: idParam,
902
+ name: nameParam,
903
+ typeIndex: typeIndexParam,
904
+ host: hostParam,
905
+ port: portParam,
906
+ apiKey: apiKeyParam,
907
+ isDefault: isDefaultParam,
908
+ createdAt: createdAtParam,
909
+ lastConnectedAt: lastConnectedAtParam,
910
+ statusIndex: statusIndexParam,
911
+ iconName: iconNameParam,
912
+ );
913
+
914
+ return object;
915
+ },
916
+ ),
917
+ };
918
+
919
+ return obx_int.ModelDefinition(model, bindings);
920
+}
921
+
922
+/// [ConversationEntity] entity fields to define ObjectBox queries.
923
+class ConversationEntity_ {
924
+ /// See [ConversationEntity.internalId].
925
+ static final internalId = obx.QueryIntegerProperty<ConversationEntity>(
926
+ _entities[0].properties[0],
927
+ );
928
+
929
+ /// See [ConversationEntity.id].
930
+ static final id = obx.QueryStringProperty<ConversationEntity>(
931
+ _entities[0].properties[1],
932
+ );
933
+
934
+ /// See [ConversationEntity.title].
935
+ static final title = obx.QueryStringProperty<ConversationEntity>(
936
+ _entities[0].properties[2],
937
+ );
938
+
939
+ /// See [ConversationEntity.createdAt].
940
+ static final createdAt = obx.QueryDateProperty<ConversationEntity>(
941
+ _entities[0].properties[3],
942
+ );
943
+
944
+ /// See [ConversationEntity.updatedAt].
945
+ static final updatedAt = obx.QueryDateProperty<ConversationEntity>(
946
+ _entities[0].properties[4],
947
+ );
948
+
949
+ /// See [ConversationEntity.isPinned].
950
+ static final isPinned = obx.QueryBooleanProperty<ConversationEntity>(
951
+ _entities[0].properties[5],
952
+ );
953
+
954
+ /// See [ConversationEntity.personaId].
955
+ static final personaId = obx.QueryStringProperty<ConversationEntity>(
956
+ _entities[0].properties[6],
957
+ );
958
+
959
+ /// See [ConversationEntity.serverId].
960
+ static final serverId = obx.QueryStringProperty<ConversationEntity>(
961
+ _entities[0].properties[7],
962
+ );
963
+
964
+ /// See [ConversationEntity.modelId].
965
+ static final modelId = obx.QueryStringProperty<ConversationEntity>(
966
+ _entities[0].properties[8],
967
+ );
968
+
969
+ /// See [ConversationEntity.messageCount].
970
+ static final messageCount = obx.QueryIntegerProperty<ConversationEntity>(
971
+ _entities[0].properties[9],
972
+ );
973
+
974
+ /// See [ConversationEntity.lastMessagePreview].
975
+ static final lastMessagePreview = obx.QueryStringProperty<ConversationEntity>(
976
+ _entities[0].properties[10],
977
+ );
978
+
979
+ /// See [ConversationEntity.systemPrompt].
980
+ static final systemPrompt = obx.QueryStringProperty<ConversationEntity>(
981
+ _entities[0].properties[11],
982
+ );
983
+
984
+ /// see [ConversationEntity.messages]
985
+ static final messages =
986
+ obx.QueryBacklinkToMany<MessageEntity, ConversationEntity>(
987
+ MessageEntity_.conversation,
988
+ );
989
+}
990
+
991
+/// [MessageEntity] entity fields to define ObjectBox queries.
992
+class MessageEntity_ {
993
+ /// See [MessageEntity.internalId].
994
+ static final internalId = obx.QueryIntegerProperty<MessageEntity>(
995
+ _entities[1].properties[0],
996
+ );
997
+
998
+ /// See [MessageEntity.id].
999
+ static final id = obx.QueryStringProperty<MessageEntity>(
1000
+ _entities[1].properties[1],
1001
+ );
1002
+
1003
+ /// See [MessageEntity.conversation].
1004
+ static final conversation =
1005
+ obx.QueryRelationToOne<MessageEntity, ConversationEntity>(
1006
+ _entities[1].properties[2],
1007
+ );
1008
+
1009
+ /// See [MessageEntity.conversationUid].
1010
+ static final conversationUid = obx.QueryStringProperty<MessageEntity>(
1011
+ _entities[1].properties[3],
1012
+ );
1013
+
1014
+ /// See [MessageEntity.roleIndex].
1015
+ static final roleIndex = obx.QueryIntegerProperty<MessageEntity>(
1016
+ _entities[1].properties[4],
1017
+ );
1018
+
1019
+ /// See [MessageEntity.content].
1020
+ static final content = obx.QueryStringProperty<MessageEntity>(
1021
+ _entities[1].properties[5],
1022
+ );
1023
+
1024
+ /// See [MessageEntity.createdAt].
1025
+ static final createdAt = obx.QueryDateProperty<MessageEntity>(
1026
+ _entities[1].properties[6],
1027
+ );
1028
+
1029
+ /// See [MessageEntity.statusIndex].
1030
+ static final statusIndex = obx.QueryIntegerProperty<MessageEntity>(
1031
+ _entities[1].properties[7],
1032
+ );
1033
+
1034
+ /// See [MessageEntity.modelId].
1035
+ static final modelId = obx.QueryStringProperty<MessageEntity>(
1036
+ _entities[1].properties[8],
1037
+ );
1038
+
1039
+ /// See [MessageEntity.tokenCount].
1040
+ static final tokenCount = obx.QueryIntegerProperty<MessageEntity>(
1041
+ _entities[1].properties[9],
1042
+ );
1043
+
1044
+ /// See [MessageEntity.errorMessage].
1045
+ static final errorMessage = obx.QueryStringProperty<MessageEntity>(
1046
+ _entities[1].properties[10],
1047
+ );
1048
+
1049
+ /// See [MessageEntity.attachmentPathsJson].
1050
+ static final attachmentPathsJson = obx.QueryStringProperty<MessageEntity>(
1051
+ _entities[1].properties[11],
1052
+ );
1053
+
1054
+ /// See [MessageEntity.generationTimeMs].
1055
+ static final generationTimeMs = obx.QueryIntegerProperty<MessageEntity>(
1056
+ _entities[1].properties[12],
1057
+ );
1058
+
1059
+ /// See [MessageEntity.reasoningContent].
1060
+ static final reasoningContent = obx.QueryStringProperty<MessageEntity>(
1061
+ _entities[1].properties[13],
1062
+ );
1063
+
1064
+ /// See [MessageEntity.toolCallsJson].
1065
+ static final toolCallsJson = obx.QueryStringProperty<MessageEntity>(
1066
+ _entities[1].properties[14],
1067
+ );
1068
+
1069
+ /// See [MessageEntity.toolCallId].
1070
+ static final toolCallId = obx.QueryStringProperty<MessageEntity>(
1071
+ _entities[1].properties[15],
1072
+ );
1073
+
1074
+ /// See [MessageEntity.isProcessing].
1075
+ static final isProcessing = obx.QueryBooleanProperty<MessageEntity>(
1076
+ _entities[1].properties[16],
1077
+ );
1078
+}
1079
+
1080
+/// [PersonaEntity] entity fields to define ObjectBox queries.
1081
+class PersonaEntity_ {
1082
+ /// See [PersonaEntity.internalId].
1083
+ static final internalId = obx.QueryIntegerProperty<PersonaEntity>(
1084
+ _entities[2].properties[0],
1085
+ );
1086
+
1087
+ /// See [PersonaEntity.id].
1088
+ static final id = obx.QueryStringProperty<PersonaEntity>(
1089
+ _entities[2].properties[1],
1090
+ );
1091
+
1092
+ /// See [PersonaEntity.name].
1093
+ static final name = obx.QueryStringProperty<PersonaEntity>(
1094
+ _entities[2].properties[2],
1095
+ );
1096
+
1097
+ /// See [PersonaEntity.emoji].
1098
+ static final emoji = obx.QueryStringProperty<PersonaEntity>(
1099
+ _entities[2].properties[3],
1100
+ );
1101
+
1102
+ /// See [PersonaEntity.systemPrompt].
1103
+ static final systemPrompt = obx.QueryStringProperty<PersonaEntity>(
1104
+ _entities[2].properties[4],
1105
+ );
1106
+
1107
+ /// See [PersonaEntity.description].
1108
+ static final description = obx.QueryStringProperty<PersonaEntity>(
1109
+ _entities[2].properties[5],
1110
+ );
1111
+
1112
+ /// See [PersonaEntity.isBuiltIn].
1113
+ static final isBuiltIn = obx.QueryBooleanProperty<PersonaEntity>(
1114
+ _entities[2].properties[6],
1115
+ );
1116
+
1117
+ /// See [PersonaEntity.createdAt].
1118
+ static final createdAt = obx.QueryDateProperty<PersonaEntity>(
1119
+ _entities[2].properties[7],
1120
+ );
1121
+
1122
+ /// See [PersonaEntity.updatedAt].
1123
+ static final updatedAt = obx.QueryDateProperty<PersonaEntity>(
1124
+ _entities[2].properties[8],
1125
+ );
1126
+
1127
+ /// See [PersonaEntity.category].
1128
+ static final category = obx.QueryStringProperty<PersonaEntity>(
1129
+ _entities[2].properties[9],
1130
+ );
1131
+
1132
+ /// See [PersonaEntity.preferredParamsJson].
1133
+ static final preferredParamsJson = obx.QueryStringProperty<PersonaEntity>(
1134
+ _entities[2].properties[10],
1135
+ );
1136
+}
1137
+
1138
+/// [ServerEntity] entity fields to define ObjectBox queries.
1139
+class ServerEntity_ {
1140
+ /// See [ServerEntity.internalId].
1141
+ static final internalId = obx.QueryIntegerProperty<ServerEntity>(
1142
+ _entities[3].properties[0],
1143
+ );
1144
+
1145
+ /// See [ServerEntity.id].
1146
+ static final id = obx.QueryStringProperty<ServerEntity>(
1147
+ _entities[3].properties[1],
1148
+ );
1149
+
1150
+ /// See [ServerEntity.name].
1151
+ static final name = obx.QueryStringProperty<ServerEntity>(
1152
+ _entities[3].properties[2],
1153
+ );
1154
+
1155
+ /// See [ServerEntity.typeIndex].
1156
+ static final typeIndex = obx.QueryIntegerProperty<ServerEntity>(
1157
+ _entities[3].properties[3],
1158
+ );
1159
+
1160
+ /// See [ServerEntity.host].
1161
+ static final host = obx.QueryStringProperty<ServerEntity>(
1162
+ _entities[3].properties[4],
1163
+ );
1164
+
1165
+ /// See [ServerEntity.port].
1166
+ static final port = obx.QueryIntegerProperty<ServerEntity>(
1167
+ _entities[3].properties[5],
1168
+ );
1169
+
1170
+ /// See [ServerEntity.apiKey].
1171
+ static final apiKey = obx.QueryStringProperty<ServerEntity>(
1172
+ _entities[3].properties[6],
1173
+ );
1174
+
1175
+ /// See [ServerEntity.isDefault].
1176
+ static final isDefault = obx.QueryBooleanProperty<ServerEntity>(
1177
+ _entities[3].properties[7],
1178
+ );
1179
+
1180
+ /// See [ServerEntity.createdAt].
1181
+ static final createdAt = obx.QueryDateProperty<ServerEntity>(
1182
+ _entities[3].properties[8],
1183
+ );
1184
+
1185
+ /// See [ServerEntity.lastConnectedAt].
1186
+ static final lastConnectedAt = obx.QueryDateProperty<ServerEntity>(
1187
+ _entities[3].properties[9],
1188
+ );
1189
+
1190
+ /// See [ServerEntity.statusIndex].
1191
+ static final statusIndex = obx.QueryIntegerProperty<ServerEntity>(
1192
+ _entities[3].properties[10],
1193
+ );
1194
+
1195
+ /// See [ServerEntity.iconName].
1196
+ static final iconName = obx.QueryStringProperty<ServerEntity>(
1197
+ _entities[3].properties[11],
1198
+ );
1199
+}
pubspec.lock
+41
-129
@@ -121,14 +121,6 @@ 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"
124
checked_yaml:
125
dependency: transitive
126
description:
@@ -145,14 +137,6 @@ packages:
137
url: "https://pub.dev"
138
source: hosted
139
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"
140
clock:
141
dependency: transitive
142
description:
@@ -257,30 +241,6 @@ packages:
241
url: "https://pub.dev"
242
source: hosted
243
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"
244
extended_image:
245
dependency: transitive
246
description:
@@ -337,6 +297,14 @@ packages:
297
url: "https://pub.dev"
298
source: hosted
299
version: "1.1.1"
300
+ flat_buffers:
301
+ dependency: transitive
302
+ description:
303
+ name: flat_buffers
304
+ sha256: "7c1de2d6eb5f3e61e5c50040841109f509deaaf2b12ec0d57b92456d9ea50345"
305
+ url: "https://pub.dev"
306
+ source: hosted
307
+ version: "25.9.23"
308
flutter:
309
dependency: "direct main"
310
description: flutter
@@ -525,30 +493,6 @@ packages:
493
url: "https://pub.dev"
494
source: hosted
495
version: "0.6.4+3"
528
- hive_ce:
529
- dependency: "direct main"
530
- description:
531
- name: hive_ce
532
- sha256: "8e9980e68643afb1e765d3af32b47996552a64e190d03faf622cea07c1294418"
533
- url: "https://pub.dev"
534
- source: hosted
535
- version: "2.19.3"
536
- hive_ce_flutter:
537
- dependency: "direct main"
538
- description:
539
- name: hive_ce_flutter
540
- sha256: "2677e95a333ff15af43ccd06af7eb7abbf1a4f154ea071997f3de4346cae913a"
541
- url: "https://pub.dev"
542
- source: hosted
543
- version: "2.3.4"
544
- hive_ce_generator:
545
- dependency: "direct dev"
546
- description:
547
- name: hive_ce_generator
548
- sha256: c9a7cda57823ffec35846c051637ddcd105eafa253e7395d9c8c0ed5e87fbb72
549
- url: "https://pub.dev"
550
- source: hosted
551
- version: "1.11.1"
496
hooks:
497
dependency: transitive
498
description:
@@ -613,14 +557,6 @@ packages:
557
url: "https://pub.dev"
558
source: hosted
559
version: "1.0.5"
616
- isolate_channel:
617
- dependency: transitive
618
- description:
619
- name: isolate_channel
620
- sha256: a9d3d620695bc984244dafae00b95e4319d6974b2d77f4b9e1eb4f2efe099094
621
- url: "https://pub.dev"
622
- source: hosted
623
- version: "0.6.1"
560
js:
561
dependency: transitive
562
description:
@@ -749,6 +685,30 @@ packages:
685
url: "https://pub.dev"
686
source: hosted
687
version: "2.0.2"
688
+ objectbox:
689
+ dependency: "direct main"
690
+ description:
691
+ name: objectbox
692
+ sha256: "83d58e0ab5c4180a2f67086c449a4f2d1475932b31819271923dfc82a76f73c6"
693
+ url: "https://pub.dev"
694
+ source: hosted
695
+ version: "5.3.1"
696
+ objectbox_flutter_libs:
697
+ dependency: "direct main"
698
+ description:
699
+ name: objectbox_flutter_libs
700
+ sha256: fd4e8ed03e2b2bfeb8aa965435d7c5523ec7e962f1ffecf6e7da6c1f4d170419
701
+ url: "https://pub.dev"
702
+ source: hosted
703
+ version: "5.3.1"
704
+ objectbox_generator:
705
+ dependency: "direct dev"
706
+ description:
707
+ name: objectbox_generator
708
+ sha256: daa95f21c7140c619ffc1abee2465541d4f0317b8c3bbf2141be1a9e5c507a1d
709
+ url: "https://pub.dev"
710
+ source: hosted
711
+ version: "5.3.1"
712
objective_c:
713
dependency: transitive
714
description:
@@ -766,7 +726,7 @@ packages:
726
source: hosted
727
version: "2.2.0"
728
path:
769
- dependency: transitive
729
+ dependency: "direct main"
730
description:
731
name: path
732
sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
@@ -853,6 +813,14 @@ packages:
813
url: "https://pub.dev"
814
source: hosted
815
version: "2.1.8"
816
+ pointycastle:
817
+ dependency: transitive
818
+ description:
819
+ name: pointycastle
820
+ sha256: "92aa3841d083cc4b0f4709b5c74fd6409a3e6ba833ffc7dc6a8fee096366acf5"
821
+ url: "https://pub.dev"
822
+ source: hosted
823
+ version: "4.0.0"
824
pool:
825
dependency: transitive
826
description:
@@ -877,14 +845,6 @@ packages:
845
url: "https://pub.dev"
846
source: hosted
847
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"
848
riverpod:
849
dependency: transitive
850
description:
@@ -1026,14 +986,6 @@ packages:
986
url: "https://pub.dev"
987
source: hosted
988
version: "4.2.2"
1029
- source_helper:
1030
- dependency: transitive
1031
- description:
1032
- name: source_helper
1033
- sha256: "1d3b229b2934034fb2e691fbb3d53e0f75a4af7b1407f88425ed8f209bcb1b8f"
1034
- url: "https://pub.dev"
1035
- source: hosted
1036
- version: "1.3.11"
989
source_map_stack_trace:
990
dependency: transitive
991
description:
@@ -1058,38 +1010,6 @@ packages:
1010
url: "https://pub.dev"
1011
source: hosted
1012
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"
1013
stack_trace:
1014
dependency: transitive
1015
description:
@@ -1306,14 +1226,6 @@ packages:
1226
url: "https://pub.dev"
1227
source: hosted
1228
version: "3.1.3"
1309
- yaml_writer:
1310
- dependency: transitive
1311
- description:
1312
- name: yaml_writer
1313
- sha256: "69651cd7238411179ac32079937d4aa9a2970150d6b2ae2c6fe6de09402a5dc5"
1314
- url: "https://pub.dev"
1315
- source: hosted
1316
- version: "2.1.0"
1229
sdks:
1230
dart: ">=3.11.0 <4.0.0"
1231
flutter: ">=3.38.4"
pubspec.yaml
+5
-7
@@ -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+6
19
+version: 1.0.0+7
20
21
environment:
22
sdk: ^3.11.0
@@ -36,8 +36,6 @@ dependencies:
36
cupertino_icons: ^1.0.8
37
flutter_riverpod: ^3.3.1
38
go_router: ^17.1.0
39
- hive_ce: ^2.19.3
40
- hive_ce_flutter: ^2.3.4
39
shadcn_ui: ^0.53.3
40
dio: ^5.9.2
41
flutter_secure_storage: ^9.2.3
@@ -51,9 +49,10 @@ dependencies:
49
flutter_syntax_view: 4.1.7
50
animations: 2.1.2
51
logger: ^2.7.0
54
- drift: ^2.32.1
55
- drift_flutter: ^0.3.0
52
shared_preferences: ^2.5.5
53
+ objectbox: ^5.3.1
54
+ objectbox_flutter_libs: ^5.3.1
55
+ path: ^1.9.1
56
57
dev_dependencies:
58
flutter_test:
@@ -66,9 +65,8 @@ dev_dependencies:
65
# rules and activating additional ones.
66
flutter_lints: ^6.0.0
67
build_runner: ^2.13.1
69
- hive_ce_generator: ^1.11.1
68
change_app_package_name: ^1.4.0
71
- drift_dev: ^2.32.1
69
+ objectbox_generator: any
70
71
# For information on the generic Dart part of this file, see the
72
# following page: https://dart.dev/tools/pub/pubspec