@tduncan / Cortana / commits / 377a999

feat: add GitHub repository card to sidebar, update conversation header UI, and increment app version.

abdulmominsakib committed Apr 16, 2026 at 16:14 UTC 377a999a0148418d179668aa5d8bc1e97e27d95b
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