@tduncan / Cortana / commits / ece66b0

refactor: migrate local storage from Hive to ObjectBox and update project dependencies

abdulmominsakib committed Apr 8, 2026 at 16:59 UTC ece66b0a82f36cb6458e78cf4a5a366d265504fe
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