20 files changed
+74
-118
android/app/build.gradle.kts
+2
-2
@@ -6,7 +6,7 @@ plugins {
6
}
7
8
android {
9
- namespace = "com.example.localmind"
9
+ namespace = "pro.momin.localmind"
10
compileSdk = flutter.compileSdkVersion
11
ndkVersion = flutter.ndkVersion
12
@@ -21,7 +21,7 @@ android {
21
22
defaultConfig {
23
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
24
- applicationId = "com.example.localmind"
24
+ applicationId = "pro.momin.localmind"
25
// You can update the following values to match your application needs.
26
// For more information, see: https://flutter.dev/to/review-gradle-config.
27
minSdk = flutter.minSdkVersion
android/app/src/main/AndroidManifest.xml
+1
-1
@@ -1,7 +1,7 @@
1
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
2
<uses-permission android:name="android.permission.INTERNET" />
3
<application
4
- android:label="localmind"
4
+ android:label="LocalMind"
5
android:name="${applicationName}"
6
android:icon="@mipmap/ic_launcher"
7
android:usesCleartextTraffic="true">
android/app/src/main/kotlin/pro/momin/localmind/MainActivity.kt
renamed
+1
-1
@@ -1,4 +1,4 @@
1
-package com.example.localmind
1
+package pro.momin.localmind
2
3
import io.flutter.embedding.android.FlutterActivity
4
ios/Runner.xcodeproj/project.pbxproj
+6
-6
@@ -482,7 +482,7 @@
482
"$(inherited)",
483
"@executable_path/Frameworks",
484
);
485
- PRODUCT_BUNDLE_IDENTIFIER = com.example.localmind;
485
+ PRODUCT_BUNDLE_IDENTIFIER = pro.momin.localmind;
486
PRODUCT_NAME = "$(TARGET_NAME)";
487
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
488
SWIFT_VERSION = 5.0;
@@ -499,7 +499,7 @@
499
CURRENT_PROJECT_VERSION = 1;
500
GENERATE_INFOPLIST_FILE = YES;
501
MARKETING_VERSION = 1.0;
502
- PRODUCT_BUNDLE_IDENTIFIER = com.example.localmind.RunnerTests;
502
+ PRODUCT_BUNDLE_IDENTIFIER = pro.momin.localmind.RunnerTests;
503
PRODUCT_NAME = "$(TARGET_NAME)";
504
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
505
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
@@ -517,7 +517,7 @@
517
CURRENT_PROJECT_VERSION = 1;
518
GENERATE_INFOPLIST_FILE = YES;
519
MARKETING_VERSION = 1.0;
520
- PRODUCT_BUNDLE_IDENTIFIER = com.example.localmind.RunnerTests;
520
+ PRODUCT_BUNDLE_IDENTIFIER = pro.momin.localmind.RunnerTests;
521
PRODUCT_NAME = "$(TARGET_NAME)";
522
SWIFT_VERSION = 5.0;
523
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
@@ -533,7 +533,7 @@
533
CURRENT_PROJECT_VERSION = 1;
534
GENERATE_INFOPLIST_FILE = YES;
535
MARKETING_VERSION = 1.0;
536
- PRODUCT_BUNDLE_IDENTIFIER = com.example.localmind.RunnerTests;
536
+ PRODUCT_BUNDLE_IDENTIFIER = pro.momin.localmind.RunnerTests;
537
PRODUCT_NAME = "$(TARGET_NAME)";
538
SWIFT_VERSION = 5.0;
539
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
@@ -665,7 +665,7 @@
665
"$(inherited)",
666
"@executable_path/Frameworks",
667
);
668
- PRODUCT_BUNDLE_IDENTIFIER = com.example.localmind;
668
+ PRODUCT_BUNDLE_IDENTIFIER = pro.momin.localmind;
669
PRODUCT_NAME = "$(TARGET_NAME)";
670
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
671
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
@@ -688,7 +688,7 @@
688
"$(inherited)",
689
"@executable_path/Frameworks",
690
);
691
- PRODUCT_BUNDLE_IDENTIFIER = com.example.localmind;
691
+ PRODUCT_BUNDLE_IDENTIFIER = pro.momin.localmind;
692
PRODUCT_NAME = "$(TARGET_NAME)";
693
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
694
SWIFT_VERSION = 5.0;
lib/core/repository/server/server_repository.dart
-1
@@ -1,4 +1,3 @@
1
-import 'package:localmind/core/models/enums.dart';
1
import 'package:localmind/features/models/data/models/model_info.dart';
2
import 'package:localmind/features/servers/data/models/server.dart';
3
lib/core/utils/date_utils.dart
+2
-1
@@ -27,8 +27,9 @@ class AppDateUtils {
27
28
static String formatDuration(Duration duration) {
29
if (duration.inSeconds < 60) return '${duration.inSeconds}s';
30
- if (duration.inMinutes < 60)
30
+ if (duration.inMinutes < 60) {
31
return '${duration.inMinutes}m ${duration.inSeconds % 60}s';
32
+ }
33
return '${duration.inHours}h ${duration.inMinutes % 60}m';
34
}
35
lib/features/chat/data/chat_service.dart
+15
-6
@@ -142,15 +142,18 @@ class LMStudioChatService implements ChatService {
142
}
143
if (params.topK != null) body['top_k'] = params.topK;
144
if (params.minP != null) body['min_p'] = params.minP;
145
- if (params.repeatPenalty != null)
145
+ if (params.repeatPenalty != null) {
146
body['repeat_penalty'] = params.repeatPenalty;
147
- if (params.reasoningLevel != null)
147
+ }
148
+ if (params.reasoningLevel != null) {
149
body['reasoning'] = params.reasoningLevel;
150
+ }
151
if (integrations != null && integrations.isNotEmpty) {
152
body['integrations'] = integrations.map((i) => i.toJson()).toList();
153
}
152
- if (previousResponseId != null)
154
+ if (previousResponseId != null) {
155
body['previous_response_id'] = previousResponseId;
156
+ }
157
158
final response = await _dio.post<ResponseBody>(
159
server.chatEndpoint,
@@ -436,7 +439,9 @@ class OpenAICompatibleChatService implements ChatService {
439
content: content,
440
);
441
}
439
- } catch (e) {}
442
+ } catch (e) {
443
+ // ignore: empty_catches
444
+ }
445
}
446
}
447
}
@@ -508,7 +513,9 @@ class OllamaChatService implements ChatService {
513
yield const ChatResponse(type: ChatResponseType.done);
514
return;
515
}
511
- } catch (e) {}
516
+ } catch (e) {
517
+ // ignore: empty_catches
518
+ }
519
}
520
}
521
}
@@ -587,7 +594,9 @@ class OpenRouterChatService implements ChatService {
594
content: content,
595
);
596
}
590
- } catch (e) {}
597
+ } catch (e) {
598
+ // ignore: empty_catches
599
+ }
600
}
601
}
602
}
lib/features/chat/providers/chat_providers.dart
+6
-4
@@ -141,11 +141,13 @@ final chatParamsProvider = Provider<ChatParameters>((ref) {
141
);
142
if (persona != null && persona.preferredParams != null) {
143
final params = persona.preferredParams as Map<String, dynamic>;
144
- if (params['temperature'] != null)
144
+ if (params['temperature'] != null) {
145
temperature = (params['temperature'] as num).toDouble();
146
+ }
147
if (params['topP'] != null) topP = (params['topP'] as num).toDouble();
147
- if (params['maxTokens'] != null)
148
+ if (params['maxTokens'] != null) {
149
maxTokens = (params['maxTokens'] as num).toInt();
150
+ }
151
}
152
} catch (_) {}
153
}
@@ -504,7 +506,7 @@ class ChatNotifier extends Notifier<ChatState> {
506
if (personaPrompt != null) {
507
messages.add(
508
Message(
507
- id: 'system-${_currentConversationId}',
509
+ id: 'system-$_currentConversationId',
510
conversationId: _currentConversationId ?? '',
511
role: MessageRole.system,
512
content: personaPrompt,
@@ -515,7 +517,7 @@ class ChatNotifier extends Notifier<ChatState> {
517
} else if (settings.showSystemMessages) {
518
messages.add(
519
Message(
518
- id: 'system-default-${_currentConversationId}',
520
+ id: 'system-default-$_currentConversationId',
521
conversationId: _currentConversationId ?? '',
522
role: MessageRole.system,
523
content:
lib/features/chat/screens/chat_screen.dart
+1
-1
@@ -815,7 +815,7 @@ class _SmartReplyChips extends ConsumerWidget {
815
child: ListView.separated(
816
scrollDirection: Axis.horizontal,
817
itemCount: suggestions.length,
818
- separatorBuilder: (_, __) => const SizedBox(width: 6),
818
+ separatorBuilder: (_, _) => const SizedBox(width: 6),
819
itemBuilder: (context, index) {
820
final label = suggestions[index];
821
return GestureDetector(
lib/features/chat/views/components/chat_input_bar.dart
+1
-1
@@ -107,7 +107,7 @@ class _ChatInputBarState extends ConsumerState<ChatInputBar> {
107
child: ListView.separated(
108
scrollDirection: Axis.horizontal,
109
itemCount: _attachedFiles.length,
110
- separatorBuilder: (_, __) => const SizedBox(width: 6),
110
+ separatorBuilder: (_, _) => const SizedBox(width: 6),
111
itemBuilder: (context, index) {
112
final file = _attachedFiles[index];
113
return Stack(
lib/features/conversations/views/components/conversation_drawer_header.dart
+5
-10
@@ -19,15 +19,12 @@ class ConversationDrawerHeader extends StatelessWidget {
19
Container(
20
padding: const EdgeInsets.all(8),
21
decoration: BoxDecoration(
22
- color: isDark
23
- ? const Color(0xFF3B82F6)
24
- : const Color(0xFF2563EB),
22
borderRadius: BorderRadius.circular(8),
23
),
27
- child: const Icon(
28
- Icons.psychology,
29
- color: Colors.white,
30
- size: 20,
24
+ child: Image.asset(
25
+ 'assets/images/logo.webp',
26
+ width: 24,
27
+ height: 24,
28
),
29
),
30
const SizedBox(width: 12),
@@ -44,9 +41,7 @@ class ConversationDrawerHeader extends StatelessWidget {
41
IconButton(
42
icon: Icon(
43
Icons.settings,
47
- color: isDark
48
- ? const Color(0xFF888888)
49
- : const Color(0xFF666666),
44
+ color: isDark ? const Color(0xFF888888) : const Color(0xFF666666),
45
),
46
onPressed: () {
47
Navigator.pop(context); // Close drawer
lib/features/conversations/views/components/conversation_list.dart
-51
@@ -82,57 +82,6 @@ class ConversationList extends ConsumerWidget {
82
);
83
}
84
85
- void _showConversationOptions(
86
- BuildContext context,
87
- WidgetRef ref,
88
- Conversation conversation,
89
- ) {
90
- showModalBottomSheet(
91
- context: context,
92
- builder: (context) {
93
- return SafeArea(
94
- child: Column(
95
- mainAxisSize: MainAxisSize.min,
96
- children: [
97
- ListTile(
98
- leading: Icon(
99
- conversation.isPinned
100
- ? Icons.push_pin
101
- : Icons.push_pin_outlined,
102
- ),
103
- title: Text(conversation.isPinned ? 'Unpin' : 'Pin'),
104
- onTap: () {
105
- Navigator.pop(context);
106
- ref
107
- .read(conversationsProvider.notifier)
108
- .togglePin(conversation.id);
109
- },
110
- ),
111
- ListTile(
112
- leading: const Icon(Icons.edit),
113
- title: const Text('Rename'),
114
- onTap: () {
115
- Navigator.pop(context);
116
- _showRenameDialog(context, ref, conversation);
117
- },
118
- ),
119
- ListTile(
120
- leading: const Icon(Icons.delete, color: Colors.red),
121
- title: const Text(
122
- 'Delete',
123
- style: TextStyle(color: Colors.red),
124
- ),
125
- onTap: () {
126
- Navigator.pop(context);
127
- _showDeleteConfirmation(context, ref, conversation);
128
- },
129
- ),
130
- ],
131
- ),
132
- );
133
- },
134
- );
135
- }
85
86
void _showRenameDialog(
87
BuildContext context,
lib/features/models/data/model_cache.dart
-1
@@ -1,4 +1,3 @@
1
-import 'dart:collection';
1
import 'package:localmind/features/models/data/models/model_info.dart';
2
3
class ModelCache {
lib/features/models/screens/model_picker_sheet.dart
+10
-12
@@ -396,19 +396,17 @@ class _ModelTile extends StatelessWidget {
396
children: [
397
Row(
398
children: [
399
- Expanded(
400
- child: Text(
401
- model.displayName,
402
- style: TextStyle(
403
- fontSize: 15,
404
- fontWeight: isSelected
405
- ? FontWeight.w600
406
- : FontWeight.w500,
407
- color: isDark ? Colors.white : Colors.black,
408
- ),
409
- maxLines: 1,
410
- overflow: TextOverflow.ellipsis,
399
+ Text(
400
+ model.displayName,
401
+ style: TextStyle(
402
+ fontSize: 15,
403
+ fontWeight: isSelected
404
+ ? FontWeight.w600
405
+ : FontWeight.w500,
406
+ color: isDark ? Colors.white : Colors.black,
407
),
408
+ maxLines: 1,
409
+ overflow: TextOverflow.ellipsis,
410
),
411
if (isLoaded)
412
Container(
lib/features/onboarding/screens/onboarding_server_type_screen.dart
+5
-2
@@ -27,10 +27,13 @@ class _OnboardingServerTypeScreenState
27
appBar: AppBar(
28
title: Row(
29
children: [
30
- /// TODO: Replace with actual app logo
30
Padding(
31
padding: const EdgeInsets.all(8),
33
- child: Placeholder(fallbackHeight: 24, fallbackWidth: 24),
32
+ child: Image.asset(
33
+ 'assets/images/logo.webp',
34
+ width: 24,
35
+ height: 24,
36
+ ),
37
),
38
const Text(
39
'LOCALMIND',
lib/features/personas/views/create_persona_screen.dart
+4
-3
@@ -201,7 +201,7 @@ class _CreatePersonaScreenState extends ConsumerState<CreatePersonaScreen> {
201
child: ListView.separated(
202
scrollDirection: Axis.horizontal,
203
itemCount: _emojis.length,
204
- separatorBuilder: (_, __) => const SizedBox(width: 4),
204
+ separatorBuilder: (_, _) => const SizedBox(width: 4),
205
itemBuilder: (context, index) {
206
final emoji = _emojis[index];
207
final isSelected = emoji == _selectedEmoji;
@@ -263,7 +263,7 @@ class _CreatePersonaScreenState extends ConsumerState<CreatePersonaScreen> {
263
const SizedBox(height: 12),
264
265
DropdownButtonFormField<String>(
266
- value: _selectedCategory,
266
+ initialValue: _selectedCategory,
267
decoration: const InputDecoration(labelText: 'Category'),
268
items: _categories
269
.map((c) => DropdownMenuItem(value: c, child: Text(c)))
@@ -362,8 +362,9 @@ class _CreatePersonaScreenState extends ConsumerState<CreatePersonaScreen> {
362
minLines: 4,
363
maxLength: 4000,
364
validator: (v) {
365
- if (v == null || v.trim().isEmpty)
365
+ if (v == null || v.trim().isEmpty) {
366
return 'System prompt is required';
367
+ }
368
if (v.trim().length > 4000) return 'Max 4000 characters';
369
return null;
370
},
lib/features/personas/views/persona_list_screen.dart
+4
-4
@@ -196,7 +196,6 @@ class PersonaListScreen extends ConsumerWidget {
196
showModalBottomSheet(
197
context: context,
198
builder: (ctx) {
199
- final isDark = Theme.of(ctx).brightness == Brightness.dark;
199
return SafeArea(
200
child: Column(
201
mainAxisSize: MainAxisSize.min,
@@ -254,9 +253,10 @@ class PersonaListScreen extends ConsumerWidget {
253
final clone = ref
254
.read(personasNotifierProvider.notifier)
255
.clonePersona(persona.id);
257
- clone.then(
258
- (c) => context.push(AppRoutes.createPersona, extra: c),
259
- );
256
+ clone.then((c) {
257
+ if (!context.mounted) return;
258
+ context.push(AppRoutes.createPersona, extra: c);
259
+ });
260
},
261
),
262
const SizedBox(height: 8),
lib/features/servers/data/server_api_service.dart
-9
@@ -267,15 +267,6 @@ class ServerApiService {
267
.join(' ');
268
}
269
270
- int? _extractParameterCount(String id) {
271
- final regex = RegExp(r'(\d+(?:\.\d+)?)\s*[Bb]');
272
- final match = regex.firstMatch(id);
273
- if (match != null) {
274
- final value = double.tryParse(match.group(1) ?? '');
275
- return value?.toInt();
276
- }
277
- return null;
278
- }
270
271
int? _parseParameterSize(String size) {
272
final cleaned = size.replaceAll('B', '').replaceAll('b', '').trim();
pubspec.lock
+8
@@ -97,6 +97,14 @@ packages:
97
url: "https://pub.dev"
98
source: hosted
99
version: "8.12.5"
100
+ change_app_package_name:
101
+ dependency: "direct dev"
102
+ description:
103
+ name: change_app_package_name
104
+ sha256: "8e43b754fe960426904d77ed4c62fa8c9834deaf6e293ae40963fa447482c4c5"
105
+ url: "https://pub.dev"
106
+ source: hosted
107
+ version: "1.5.0"
108
characters:
109
dependency: transitive
110
description:
pubspec.yaml
+3
-2
@@ -2,7 +2,7 @@ name: localmind
2
description: "A new Flutter project."
3
# The following line prevents the package from being accidentally published to
4
# pub.dev using `flutter pub publish`. This is preferred for private packages.
5
-publish_to: 'none' # Remove this line if you wish to publish to pub.dev
5
+publish_to: "none" # Remove this line if you wish to publish to pub.dev
6
7
# The following defines the version and build number for your application.
8
# A version number is three numbers separated by dots, like 1.2.43
@@ -61,13 +61,13 @@ dev_dependencies:
61
flutter_lints: ^6.0.0
62
build_runner: ^2.13.1
63
hive_ce_generator: ^1.11.1
64
+ change_app_package_name: ^1.4.0
65
66
# For information on the generic Dart part of this file, see the
67
# following page: https://dart.dev/tools/pub/pubspec
68
69
# The following section is specific to Flutter packages.
70
flutter:
70
-
71
# The following line ensures that the Material Icons font is
72
# included with your application, so that you can use the icons in
73
# the material Icons class.
@@ -76,6 +76,7 @@ flutter:
76
# To add assets to your application, add an assets section, like this:
77
assets:
78
- assets/google_fonts/
79
+ - assets/images/
80
81
# An image asset can refer to one or more resolution-specific "variants", see
82
# https://flutter.dev/to/resolution-aware-images