22 files changed
+210
-34
lib/core/components/app_button.dart
+1
@@ -1,4 +1,5 @@
1
import 'package:flutter/material.dart';
2
+
3
import 'app_sizes.dart';
4
5
class AppButton extends StatelessWidget {
lib/core/components/app_text_field.dart
+1
@@ -1,4 +1,5 @@
1
import 'package:flutter/material.dart';
2
+
3
import 'app_sizes.dart';
4
5
class AppTextField extends StatelessWidget {
lib/core/storage/objectbox_store.dart
+3
-1
@@ -1,6 +1,8 @@
1
import 'dart:io';
2
-import 'package:path_provider/path_provider.dart';
2
+
3
import 'package:path/path.dart' as p;
4
+import 'package:path_provider/path_provider.dart';
5
+
6
import '../../objectbox.g.dart';
7
import 'entities.dart';
8
lib/features/chat/data/mcp_client.dart
+1
@@ -1,4 +1,5 @@
1
import 'dart:async';
2
+
3
import 'package:dio/dio.dart';
4
5
class McpClient {
lib/features/chat/views/components/chat_input_bar.dart
+3
-1
@@ -1,9 +1,11 @@
1
import 'dart:io';
2
+
3
+import 'package:file_picker/file_picker.dart';
4
import 'package:flutter/material.dart';
5
import 'package:flutter_riverpod/flutter_riverpod.dart';
6
import 'package:haptic_feedback/haptic_feedback.dart';
5
-import 'package:file_picker/file_picker.dart';
7
import 'package:hugeicons/hugeicons.dart';
8
+
9
import '../../../../core/models/enums.dart';
10
import '../../../servers/providers/server_providers.dart';
11
lib/features/conversations/providers/conversation_providers.dart
+2
-1
@@ -1,8 +1,9 @@
1
import 'package:flutter_riverpod/flutter_riverpod.dart';
2
+
3
import '../../../core/providers/storage_providers.dart';
4
import '../../../core/storage/entities.dart';
4
-import '../data/models/conversation.dart';
5
import '../../../objectbox.g.dart';
6
+import '../data/models/conversation.dart';
7
8
final conversationsProvider =
9
AsyncNotifierProvider<ConversationsNotifier, List<Conversation>>(() {
lib/features/conversations/views/components/new_chat_button.dart
+1
@@ -1,6 +1,7 @@
1
import 'package:flutter/material.dart';
2
import 'package:flutter_riverpod/flutter_riverpod.dart';
3
import 'package:go_router/go_router.dart';
4
+
5
import '../../../../core/routes/app_routes.dart';
6
import '../../../chat/providers/chat_providers.dart';
7
lib/features/on_device/data/on_device_chat_service.dart
+1
-1
@@ -6,8 +6,8 @@ import '../../../core/logger/app_logger.dart';
6
import '../../../core/models/enums.dart';
7
import '../../chat/data/chat_service.dart';
8
import '../../chat/data/models/chat_parameters.dart';
9
-import '../../chat/data/models/message.dart' hide ToolCallData;
9
import '../../chat/data/models/mcp_integration.dart';
10
+import '../../chat/data/models/message.dart' hide ToolCallData;
11
import '../../servers/data/models/server.dart';
12
import 'on_device_engine_service.dart';
13
lib/features/on_device/providers/on_device_providers.dart
+3
-3
@@ -151,9 +151,9 @@ class OnDeviceModelStateNotifier
151
EngineStatus? engineStatus,
152
}) {
153
final current =
154
- this.state[modelId] ?? OnDeviceModelStateInfo(modelId: modelId);
155
- this.state = {
156
- ...this.state,
154
+ state[modelId] ?? OnDeviceModelStateInfo(modelId: modelId);
155
+ state = {
156
+ ...state,
157
modelId: current.copyWith(
158
state: modelState ?? current.state,
159
downloadProgress: downloadProgress ?? current.downloadProgress,
lib/features/on_device/views/model_manager_screen.dart
+2
-2
@@ -2,7 +2,7 @@ import 'dart:io';
2
3
import 'package:flutter/material.dart';
4
import 'package:flutter_riverpod/flutter_riverpod.dart';
5
-import 'package:localmind/features/sidebar/sidebar_widget.dart';
5
+import '../../sidebar/sidebar_widget.dart';
6
import 'package:shadcn_ui/shadcn_ui.dart';
7
8
import '../../../core/models/enums.dart';
@@ -137,7 +137,7 @@ class _ModelCard extends ConsumerWidget {
137
final isDownloaded = downloadedAsync.when(
138
data: (set) => set.contains(model.id),
139
loading: () => false,
140
- error: (_, __) => false,
140
+ error: (_, _) => false,
141
);
142
143
final isLoading =
lib/features/onboarding/screens/onboarding_model_download_screen.dart
+1
-1
@@ -75,7 +75,7 @@ class _OnboardingModelDownloadScreenState
75
final isDownloaded = downloadedModelsAsync.when(
76
data: (set) => set.contains(model.id),
77
loading: () => false,
78
- error: (_, __) => false,
78
+ error: (_, _) => false,
79
);
80
final isDownloading = _downloadingModelId == model.id;
81
return Padding(
lib/features/onboarding/screens/onboarding_server_setup_screen.dart
+2
-2
@@ -7,10 +7,10 @@ import 'package:shadcn_ui/shadcn_ui.dart';
7
8
import '../../../core/constants/app_constants.dart';
9
import '../../../core/models/enums.dart';
10
-import '../../servers/data/models/server.dart';
10
import '../../../core/providers/service_providers.dart';
12
-import '../../servers/providers/server_providers.dart';
11
import '../../../core/routes/app_routes.dart';
12
+import '../../servers/data/models/server.dart';
13
+import '../../servers/providers/server_providers.dart';
14
15
class OnboardingServerSetupScreen extends ConsumerStatefulWidget {
16
final ServerType selectedType;
lib/features/personas/providers/personas_providers.dart
+2
-1
@@ -1,8 +1,9 @@
1
import 'package:flutter_riverpod/flutter_riverpod.dart';
2
+
3
import '../../../core/providers/storage_providers.dart';
4
import '../../../core/storage/entities.dart';
4
-import '../data/models/persona.dart';
5
import '../../../objectbox.g.dart';
6
+import '../data/models/persona.dart';
7
8
final personaSearchQueryProvider =
9
NotifierProvider<_PersonaSearchNotifier, String>(
lib/features/personas/views/persona_list_screen.dart
+1
@@ -1,6 +1,7 @@
1
import 'package:flutter/material.dart';
2
import 'package:flutter_riverpod/flutter_riverpod.dart';
3
import 'package:go_router/go_router.dart';
4
+
5
import '../../../core/routes/app_routes.dart';
6
import '../data/models/persona.dart';
7
import '../providers/personas_providers.dart';
lib/features/servers/data/server_api_service.dart
+6
-3
@@ -90,8 +90,9 @@ class ServerApiService {
90
91
Future<void> loadModel(Server server, String modelId) async {
92
if (server.type == ServerType.openRouter ||
93
- server.type == ServerType.onDevice)
93
+ server.type == ServerType.onDevice) {
94
return;
95
+ }
96
97
switch (server.type) {
98
case ServerType.lmStudio:
@@ -110,8 +111,9 @@ class ServerApiService {
111
112
Future<String?> loadModelWithInstanceId(Server server, String modelId) async {
113
if (server.type == ServerType.openRouter ||
113
- server.type == ServerType.onDevice)
114
+ server.type == ServerType.onDevice) {
115
return null;
116
+ }
117
118
switch (server.type) {
119
case ServerType.lmStudio:
@@ -141,8 +143,9 @@ class ServerApiService {
143
String? instanceId,
144
}) async {
145
if (server.type == ServerType.openRouter ||
144
- server.type == ServerType.onDevice)
146
+ server.type == ServerType.onDevice) {
147
return;
148
+ }
149
150
switch (server.type) {
151
case ServerType.lmStudio:
lib/features/servers/views/components/server_card.dart
+2
-1
@@ -1,7 +1,8 @@
1
import 'package:flutter/material.dart';
2
import 'package:hugeicons/hugeicons.dart';
3
-import '../../data/models/server.dart';
3
+
4
import '../../../../core/models/enums.dart';
5
+import '../../data/models/server.dart';
6
import 'connection_status_indicator.dart';
7
import 'server_icon_picker.dart';
8
lib/features/sidebar/components/conversation_drawer_header.dart
+14
-10
@@ -1,17 +1,21 @@
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:shadcn_ui/shadcn_ui.dart';
5
+
6
import '../../../core/routes/app_routes.dart';
7
+import '../../chat/providers/chat_providers.dart';
8
5
-class ConversationDrawerHeader extends StatelessWidget {
9
+class ConversationDrawerHeader extends ConsumerWidget {
10
const ConversationDrawerHeader({super.key});
11
12
@override
9
- Widget build(BuildContext context) {
13
+ Widget build(BuildContext context, WidgetRef ref) {
14
final theme = Theme.of(context);
15
final isDark = theme.brightness == Brightness.dark;
16
17
return Container(
14
- padding: const EdgeInsets.all(16),
18
+ padding: const EdgeInsets.only(left: 16, right: 16, top: 16, bottom: 8),
19
child: Row(
20
mainAxisAlignment: MainAxisAlignment.spaceBetween,
21
children: [
@@ -39,14 +43,14 @@ class ConversationDrawerHeader extends StatelessWidget {
43
),
44
],
45
),
42
- IconButton(
43
- icon: Icon(
44
- Icons.settings,
45
- color: isDark ? const Color(0xFF888888) : const Color(0xFF666666),
46
- ),
46
+ ShadIconButton.outline(
47
+ icon: const Icon(LucideIcons.plus, size: 20),
48
onPressed: () {
48
- Navigator.pop(context); // Close drawer
49
- context.push(AppRoutes.settings);
49
+ ref.read(chatProvider.notifier).startNewConversation();
50
+ context.go(AppRoutes.home);
51
+ if (Scaffold.maybeOf(context)?.isDrawerOpen ?? false) {
52
+ Navigator.pop(context);
53
+ }
54
},
55
),
56
],
lib/features/sidebar/components/github_repo_card.dart
new
+94
@@ -0,0 +1,94 @@
1
+import 'package:flutter/material.dart';
2
+import 'package:hugeicons/hugeicons.dart';
3
+import 'package:shadcn_ui/shadcn_ui.dart';
4
+import 'package:url_launcher/url_launcher.dart';
5
+
6
+class GitHubRepoCard extends StatelessWidget {
7
+ const GitHubRepoCard({super.key});
8
+
9
+ final String repoUrl = 'https://github.com/abdulmominsakib/localmind';
10
+
11
+ Future<void> _launchUrl() async {
12
+ final Uri url = Uri.parse(repoUrl);
13
+ if (!await launchUrl(url)) {
14
+ throw Exception('Could not launch $url');
15
+ }
16
+ }
17
+
18
+ @override
19
+ Widget build(BuildContext context) {
20
+ final theme = Theme.of(context);
21
+ final isDark = theme.brightness == Brightness.dark;
22
+
23
+ // Using standard colors from theme or defaults to ensure consistent premium look
24
+ final cardBg = isDark ? const Color(0xFF1F1F1F) : const Color(0xFFF9FAFB);
25
+ final borderColor = isDark ? const Color(0xFF374151) : const Color(0xFFE5E7EB);
26
+ final accentColor = isDark ? const Color(0xFF3B82F6) : const Color(0xFF2563EB);
27
+
28
+ return Padding(
29
+ padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
30
+ child: ShadCard(
31
+ padding: const EdgeInsets.all(16),
32
+ backgroundColor: cardBg,
33
+ border: ShadBorder.all(color: borderColor),
34
+ radius: BorderRadius.circular(12),
35
+ title: Row(
36
+ children: [
37
+ HugeIcon(
38
+ icon: HugeIcons.strokeRoundedGithub,
39
+ size: 20,
40
+ color: isDark ? Colors.white : Colors.black,
41
+ ),
42
+ const SizedBox(width: 8),
43
+ Text(
44
+ 'Open Source',
45
+ style: TextStyle(
46
+ fontSize: 14,
47
+ fontWeight: FontWeight.w600,
48
+ color: isDark ? Colors.white : Colors.black,
49
+ ),
50
+ ),
51
+ ],
52
+ ),
53
+ description: Padding(
54
+ padding: const EdgeInsets.only(top: 4),
55
+ child: Text(
56
+ 'LocalMind is open source. Follow our progress or contribute on GitHub.',
57
+ style: TextStyle(
58
+ fontSize: 12,
59
+ color: isDark ? const Color(0xFFA0A0A0) : const Color(0xFF666666),
60
+ height: 1.4,
61
+ ),
62
+ ),
63
+ ),
64
+ footer: Padding(
65
+ padding: const EdgeInsets.only(top: 12),
66
+ child: ShadButton.ghost(
67
+ width: double.infinity,
68
+ padding: EdgeInsets.zero,
69
+ onPressed: _launchUrl,
70
+ child: Row(
71
+ mainAxisAlignment: MainAxisAlignment.center,
72
+ children: [
73
+ Text(
74
+ 'Star on GitHub',
75
+ style: TextStyle(
76
+ fontSize: 12,
77
+ fontWeight: FontWeight.w600,
78
+ color: accentColor,
79
+ ),
80
+ ),
81
+ const SizedBox(width: 4),
82
+ Icon(
83
+ Icons.open_in_new,
84
+ size: 14,
85
+ color: accentColor,
86
+ ),
87
+ ],
88
+ ),
89
+ ),
90
+ ),
91
+ ),
92
+ );
93
+ }
94
+}
lib/features/sidebar/sidebar_widget.dart
+4
-5
@@ -5,13 +5,13 @@ import 'package:hugeicons/hugeicons.dart';
5
6
import '../../core/routes/app_routes.dart';
7
import '../conversations/providers/conversation_providers.dart';
8
-import 'components/active_server_indicator.dart';
9
-import 'components/conversation_drawer_header.dart';
8
import '../conversations/views/components/conversation_empty_state.dart';
9
import '../conversations/views/components/conversation_list.dart';
10
import '../conversations/views/components/conversation_search_bar.dart';
11
+import 'components/active_server_indicator.dart';
12
+import 'components/conversation_drawer_header.dart';
13
import 'components/drawer_nav_item.dart';
14
-import '../conversations/views/components/new_chat_button.dart';
14
+import 'components/github_repo_card.dart';
15
16
class SidebarWidget extends ConsumerWidget {
17
const SidebarWidget({super.key});
@@ -38,8 +38,6 @@ class SidebarWidget extends ConsumerWidget {
38
child: Column(
39
children: [
40
const ConversationDrawerHeader(),
41
- const NewChatButton(),
42
- const SizedBox(height: 8),
41
const ConversationSearchBar(),
42
const Divider(height: 1),
43
Expanded(
@@ -63,6 +61,7 @@ class SidebarWidget extends ConsumerWidget {
61
),
62
),
63
),
64
+ const GitHubRepoCard(),
65
const Divider(height: 1),
66
const ActiveServerIndicator(),
67
const SizedBox(height: 8),
lib/main.dart
-1
@@ -1,6 +1,5 @@
1
import 'package:flutter/material.dart';
2
import 'package:flutter_riverpod/flutter_riverpod.dart';
3
-
3
import 'package:shared_preferences/shared_preferences.dart';
4
5
import 'app.dart';
pubspec.lock
+64
@@ -1122,6 +1122,70 @@ packages:
1122
url: "https://pub.dev"
1123
source: hosted
1124
version: "1.0.11"
1125
+ url_launcher:
1126
+ dependency: "direct main"
1127
+ description:
1128
+ name: url_launcher
1129
+ sha256: f6a7e5c4835bb4e3026a04793a4199ca2d14c739ec378fdfe23fc8075d0439f8
1130
+ url: "https://pub.dev"
1131
+ source: hosted
1132
+ version: "6.3.2"
1133
+ url_launcher_android:
1134
+ dependency: transitive
1135
+ description:
1136
+ name: url_launcher_android
1137
+ sha256: "3bb000251e55d4a209aa0e2e563309dc9bb2befea2295fd0cec1f51760aac572"
1138
+ url: "https://pub.dev"
1139
+ source: hosted
1140
+ version: "6.3.29"
1141
+ url_launcher_ios:
1142
+ dependency: transitive
1143
+ description:
1144
+ name: url_launcher_ios
1145
+ sha256: "580fe5dfb51671ae38191d316e027f6b76272b026370708c2d898799750a02b0"
1146
+ url: "https://pub.dev"
1147
+ source: hosted
1148
+ version: "6.4.1"
1149
+ url_launcher_linux:
1150
+ dependency: transitive
1151
+ description:
1152
+ name: url_launcher_linux
1153
+ sha256: d5e14138b3bc193a0f63c10a53c94b91d399df0512b1f29b94a043db7482384a
1154
+ url: "https://pub.dev"
1155
+ source: hosted
1156
+ version: "3.2.2"
1157
+ url_launcher_macos:
1158
+ dependency: transitive
1159
+ description:
1160
+ name: url_launcher_macos
1161
+ sha256: "368adf46f71ad3c21b8f06614adb38346f193f3a59ba8fe9a2fd74133070ba18"
1162
+ url: "https://pub.dev"
1163
+ source: hosted
1164
+ version: "3.2.5"
1165
+ url_launcher_platform_interface:
1166
+ dependency: transitive
1167
+ description:
1168
+ name: url_launcher_platform_interface
1169
+ sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029"
1170
+ url: "https://pub.dev"
1171
+ source: hosted
1172
+ version: "2.3.2"
1173
+ url_launcher_web:
1174
+ dependency: transitive
1175
+ description:
1176
+ name: url_launcher_web
1177
+ sha256: d0412fcf4c6b31ecfdb7762359b7206ffba3bbffd396c6d9f9c4616ece476c1f
1178
+ url: "https://pub.dev"
1179
+ source: hosted
1180
+ version: "2.4.2"
1181
+ url_launcher_windows:
1182
+ dependency: transitive
1183
+ description:
1184
+ name: url_launcher_windows
1185
+ sha256: "712c70ab1b99744ff066053cbe3e80c73332b38d46e5e945c98689b2e66fc15f"
1186
+ url: "https://pub.dev"
1187
+ source: hosted
1188
+ version: "3.1.5"
1189
vector_graphics:
1190
dependency: transitive
1191
description:
pubspec.yaml
+2
-1
@@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
16
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
17
# In Windows, build-name is used as the major, minor, and patch parts
18
# of the product and file versions while build-number is used as the build suffix.
19
-version: 1.0.0+8
19
+version: 1.0.0+10
20
21
environment:
22
sdk: ^3.11.0
@@ -37,6 +37,7 @@ dependencies:
37
flutter_riverpod: ^3.3.1
38
go_router: ^17.1.0
39
shadcn_ui: ^0.53.3
40
+ url_launcher: ^6.3.1
41
dio: ^5.9.2
42
flutter_secure_storage: ^9.2.3
43
flutter_markdown_plus: ^1.0.7