17 files changed
+422
-99
.gitignore
+2
@@ -5,9 +5,11 @@
5
*.swp
6
.DS_Store
7
.atom/
8
+.build/
9
.buildlog/
10
.history
11
.svn/
12
+.swiftpm/
13
migrate_working_dir/
14
15
# IntelliJ related
assets/images/lmstudio.webp
Binary files /dev/null and b/assets/images/lmstudio.webp differ
assets/images/ollama.webp
Binary files /dev/null and b/assets/images/ollama.webp differ
assets/images/openrouter.webp
Binary files /dev/null and b/assets/images/openrouter.webp differ
assets/images/requesty.webp
Binary files /dev/null and b/assets/images/requesty.webp differ
lib/core/components/server_logo.dart
new
+75
@@ -0,0 +1,75 @@
1
+import 'package:flutter/material.dart';
2
+import 'package:hugeicons/hugeicons.dart';
3
+
4
+import '../models/enums.dart';
5
+
6
+class ServerLogo extends StatelessWidget {
7
+ final ServerType type;
8
+ final double size;
9
+ final double? borderRadius;
10
+ final Color? iconColor;
11
+ final List<List<dynamic>>? customIcon;
12
+
13
+ const ServerLogo({
14
+ super.key,
15
+ required this.type,
16
+ this.size = 24,
17
+ this.borderRadius,
18
+ this.iconColor,
19
+ this.customIcon,
20
+ });
21
+
22
+ static List<List<dynamic>> defaultHugeIconForType(ServerType type) {
23
+ switch (type) {
24
+ case ServerType.lmStudio:
25
+ return HugeIcons.strokeRoundedComputerTerminal01;
26
+ case ServerType.openAICompatible:
27
+ return HugeIcons.strokeRoundedApi;
28
+ case ServerType.ollama:
29
+ return HugeIcons.strokeRoundedRobot01;
30
+ case ServerType.ollamaCloud:
31
+ return HugeIcons.strokeRoundedAiCloud;
32
+ case ServerType.openRouter:
33
+ return HugeIcons.strokeRoundedCloud;
34
+ case ServerType.requesty:
35
+ return HugeIcons.strokeRoundedAiNetwork;
36
+ case ServerType.onDevice:
37
+ return HugeIcons.strokeRoundedSmartPhone01;
38
+ }
39
+ }
40
+
41
+ @override
42
+ Widget build(BuildContext context) {
43
+ final theme = Theme.of(context);
44
+ final fallbackColor = iconColor ?? theme.colorScheme.onSurface;
45
+
46
+ if (customIcon != null) {
47
+ return HugeIcon(icon: customIcon!, size: size, color: fallbackColor);
48
+ }
49
+
50
+ final asset = type.logoAsset;
51
+ if (asset != null) {
52
+ final radius = borderRadius ?? (size * 0.22);
53
+ return ClipRRect(
54
+ borderRadius: BorderRadius.circular(radius),
55
+ child: Image.asset(
56
+ asset,
57
+ width: size,
58
+ height: size,
59
+ fit: BoxFit.contain,
60
+ errorBuilder: (context, error, stackTrace) => HugeIcon(
61
+ icon: defaultHugeIconForType(type),
62
+ size: size,
63
+ color: fallbackColor,
64
+ ),
65
+ ),
66
+ );
67
+ }
68
+
69
+ return HugeIcon(
70
+ icon: defaultHugeIconForType(type),
71
+ size: size,
72
+ color: fallbackColor,
73
+ );
74
+ }
75
+}
lib/core/constants/app_constants.dart
+5
@@ -26,4 +26,9 @@ class AppConstants {
26
static const String requestyBaseUrl = 'https://router.requesty.ai/v1';
27
static const String ollamaCloudBaseUrl = 'https://ollama.com';
28
static const int ollamaCloudDefaultPort = 443;
29
+
30
+ static const String lmStudioLogoAsset = 'assets/images/lmstudio.webp';
31
+ static const String ollamaLogoAsset = 'assets/images/ollama.webp';
32
+ static const String openRouterLogoAsset = 'assets/images/openrouter.webp';
33
+ static const String requestyLogoAsset = 'assets/images/requesty.webp';
34
}
lib/core/models/enums.dart
+23
@@ -1,3 +1,5 @@
1
+import '../constants/app_constants.dart';
2
+
3
enum EngineId { system, kitten, piper }
4
5
class EngineMeta {
@@ -210,6 +212,27 @@ enum ServerType {
212
requesty,
213
}
214
215
+extension ServerTypeLogoExtension on ServerType {
216
+ String? get logoAsset {
217
+ switch (this) {
218
+ case ServerType.lmStudio:
219
+ return AppConstants.lmStudioLogoAsset;
220
+ case ServerType.ollama:
221
+ case ServerType.ollamaCloud:
222
+ return AppConstants.ollamaLogoAsset;
223
+ case ServerType.openRouter:
224
+ return AppConstants.openRouterLogoAsset;
225
+ case ServerType.requesty:
226
+ return AppConstants.requestyLogoAsset;
227
+ case ServerType.openAICompatible:
228
+ case ServerType.onDevice:
229
+ return null;
230
+ }
231
+ }
232
+
233
+ bool get hasLogo => logoAsset != null;
234
+}
235
+
236
enum ConnectionStatus { connected, disconnected, checking, error }
237
238
enum MessageRole { user, assistant, system, tool }
lib/features/onboarding/screens/onboarding_server_setup_screen.dart
+79
-4
@@ -7,6 +7,7 @@ import 'package:go_router/go_router.dart';
7
import 'package:shadcn_ui/shadcn_ui.dart';
8
import 'package:cue/cue.dart';
9
10
+import '../../../core/components/server_logo.dart';
11
import '../../../core/constants/app_constants.dart';
12
import '../../../core/models/enums.dart';
13
import '../../../core/providers/service_providers.dart';
@@ -306,6 +307,25 @@ class _OnboardingServerSetupScreenState
307
}
308
}
309
310
+ String _serverTypeName(ServerType type, AppLocalizations l10n) {
311
+ switch (type) {
312
+ case ServerType.lmStudio:
313
+ return l10n.server_type_lm_studio;
314
+ case ServerType.openAICompatible:
315
+ return l10n.server_type_openai_display;
316
+ case ServerType.ollama:
317
+ return l10n.server_type_ollama;
318
+ case ServerType.ollamaCloud:
319
+ return l10n.server_type_ollama_cloud;
320
+ case ServerType.openRouter:
321
+ return l10n.server_type_openrouter;
322
+ case ServerType.requesty:
323
+ return l10n.server_type_requesty;
324
+ case ServerType.onDevice:
325
+ return l10n.server_type_on_device;
326
+ }
327
+ }
328
+
329
@override
330
Widget build(BuildContext context) {
331
final l10n = AppLocalizations.of(context)!;
@@ -329,10 +349,65 @@ class _OnboardingServerSetupScreenState
349
children: [
350
Actor(
351
acts: [.fadeIn(), .slideY(from: 0.08)],
332
- child: Text(
333
- l10n.setup_connection_desc(widget.selectedType.name),
334
- style: theme.textTheme.titleMedium?.copyWith(
335
- color: theme.colorScheme.onSurface.withValues(alpha: 0.7),
352
+ child: Container(
353
+ padding: const EdgeInsets.all(16),
354
+ decoration: BoxDecoration(
355
+ color: theme.colorScheme.surfaceContainerHighest
356
+ .withValues(alpha: 0.35),
357
+ borderRadius: BorderRadius.circular(16),
358
+ border: Border.all(
359
+ color: theme.colorScheme.outline.withValues(alpha: 0.2),
360
+ ),
361
+ ),
362
+ child: Row(
363
+ children: [
364
+ Container(
365
+ width: 44,
366
+ height: 44,
367
+ padding: widget.selectedType.hasLogo
368
+ ? const EdgeInsets.all(5)
369
+ : const EdgeInsets.all(8),
370
+ decoration: BoxDecoration(
371
+ color: theme.colorScheme.surface,
372
+ borderRadius: BorderRadius.circular(12),
373
+ border: Border.all(
374
+ color: theme.colorScheme.outline.withValues(
375
+ alpha: 0.15,
376
+ ),
377
+ ),
378
+ ),
379
+ child: Center(
380
+ child: ServerLogo(
381
+ type: widget.selectedType,
382
+ size: widget.selectedType.hasLogo ? 30 : 22,
383
+ iconColor: theme.colorScheme.primary,
384
+ ),
385
+ ),
386
+ ),
387
+ const SizedBox(width: 14),
388
+ Expanded(
389
+ child: Column(
390
+ crossAxisAlignment: CrossAxisAlignment.start,
391
+ children: [
392
+ Text(
393
+ _serverTypeName(widget.selectedType, l10n),
394
+ style: theme.textTheme.titleMedium?.copyWith(
395
+ fontWeight: FontWeight.w700,
396
+ ),
397
+ ),
398
+ const SizedBox(height: 2),
399
+ Text(
400
+ l10n.setup_connection_desc(
401
+ widget.selectedType.name,
402
+ ),
403
+ style: theme.textTheme.bodySmall?.copyWith(
404
+ color: theme.colorScheme.onSurfaceVariant,
405
+ ),
406
+ ),
407
+ ],
408
+ ),
409
+ ),
410
+ ],
411
),
412
),
413
),
lib/features/onboarding/screens/onboarding_server_type_screen.dart
+30
-22
@@ -6,6 +6,7 @@ import 'package:shadcn_ui/shadcn_ui.dart';
6
import 'package:cue/cue.dart';
7
import 'package:url_launcher/url_launcher.dart';
8
9
+import '../../../core/components/server_logo.dart';
10
import '../../../core/models/enums.dart';
11
import '../../../core/providers/app_providers.dart';
12
import '../../../core/routes/app_routes.dart';
@@ -150,9 +151,10 @@ class _OnboardingServerTypeScreenState
151
type: ServerType.onDevice,
152
title: l10n.server_type_on_device,
153
subtitle: l10n.server_type_on_device_desc,
153
- iconWidget: HugeIcon(
154
- icon: HugeIcons.strokeRoundedSmartPhone01,
155
- color:
154
+ iconWidget: ServerLogo(
155
+ type: ServerType.onDevice,
156
+ size: 20,
157
+ iconColor:
158
_selectedType == ServerType.onDevice
159
? theme.colorScheme.primary
160
: theme.colorScheme.onSurface,
@@ -163,10 +165,10 @@ class _OnboardingServerTypeScreenState
165
type: ServerType.lmStudio,
166
title: l10n.server_type_lm_studio,
167
subtitle: l10n.server_type_lm_studio_desc,
166
- iconWidget: HugeIcon(
167
- icon: HugeIcons
168
- .strokeRoundedComputerTerminal01,
169
- color:
168
+ iconWidget: ServerLogo(
169
+ type: ServerType.lmStudio,
170
+ size: 20,
171
+ iconColor:
172
_selectedType == ServerType.lmStudio
173
? theme.colorScheme.primary
174
: theme.colorScheme.onSurface,
@@ -177,9 +179,10 @@ class _OnboardingServerTypeScreenState
179
type: ServerType.openAICompatible,
180
title: l10n.server_type_openai_display,
181
subtitle: l10n.openai_compatible_api,
180
- iconWidget: HugeIcon(
181
- icon: HugeIcons.strokeRoundedApi,
182
- color:
182
+ iconWidget: ServerLogo(
183
+ type: ServerType.openAICompatible,
184
+ size: 20,
185
+ iconColor:
186
_selectedType ==
187
ServerType.openAICompatible
188
? theme.colorScheme.primary
@@ -191,9 +194,11 @@ class _OnboardingServerTypeScreenState
194
type: ServerType.ollama,
195
title: l10n.server_type_ollama,
196
subtitle: l10n.server_type_ollama_desc,
194
- iconWidget: HugeIcon(
195
- icon: HugeIcons.strokeRoundedRobot01,
196
- color: _selectedType == ServerType.ollama
197
+ iconWidget: ServerLogo(
198
+ type: ServerType.ollama,
199
+ size: 20,
200
+ iconColor:
201
+ _selectedType == ServerType.ollama
202
? theme.colorScheme.primary
203
: theme.colorScheme.onSurface,
204
),
@@ -203,9 +208,10 @@ class _OnboardingServerTypeScreenState
208
type: ServerType.ollamaCloud,
209
title: l10n.server_type_ollama_cloud,
210
subtitle: l10n.server_type_ollama_cloud_sub,
206
- iconWidget: HugeIcon(
207
- icon: HugeIcons.strokeRoundedAiCloud,
208
- color:
211
+ iconWidget: ServerLogo(
212
+ type: ServerType.ollamaCloud,
213
+ size: 20,
214
+ iconColor:
215
_selectedType ==
216
ServerType.ollamaCloud
217
? theme.colorScheme.primary
@@ -217,9 +223,10 @@ class _OnboardingServerTypeScreenState
223
type: ServerType.openRouter,
224
title: l10n.server_type_openrouter,
225
subtitle: l10n.server_type_openrouter_sub,
220
- iconWidget: HugeIcon(
221
- icon: HugeIcons.strokeRoundedCloudServer,
222
- color:
226
+ iconWidget: ServerLogo(
227
+ type: ServerType.openRouter,
228
+ size: 20,
229
+ iconColor:
230
_selectedType == ServerType.openRouter
231
? theme.colorScheme.primary
232
: theme.colorScheme.onSurface,
@@ -230,9 +237,10 @@ class _OnboardingServerTypeScreenState
237
type: ServerType.requesty,
238
title: l10n.server_type_requesty,
239
subtitle: l10n.server_type_requesty_sub,
233
- iconWidget: HugeIcon(
234
- icon: HugeIcons.strokeRoundedAiNetwork,
235
- color:
240
+ iconWidget: ServerLogo(
241
+ type: ServerType.requesty,
242
+ size: 20,
243
+ iconColor:
244
_selectedType == ServerType.requesty
245
? theme.colorScheme.primary
246
: theme.colorScheme.onSurface,
lib/features/servers/views/add_server_screen.dart
+35
-17
@@ -4,6 +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 'package:localmind/l10n/app_localizations.dart';
7
+import '../../../core/components/server_logo.dart';
8
import '../../../core/constants/app_constants.dart';
9
import '../data/models/server.dart';
10
import '../../../core/models/enums.dart';
@@ -475,25 +476,36 @@ class _AddServerScreenState extends ConsumerState<AddServerScreen> {
476
Container(
477
width: 48,
478
height: 48,
479
+ padding:
480
+ _selectedIconName == null &&
481
+ _selectedType.hasLogo
482
+ ? const EdgeInsets.all(6)
483
+ : EdgeInsets.zero,
484
decoration: BoxDecoration(
485
color: colorScheme.primary.withValues(
486
alpha: 0.12,
487
),
488
borderRadius: BorderRadius.circular(14),
489
),
484
- child: HugeIcon(
485
- icon: _selectedIconName != null
486
- ? (getHugeIconByName(
487
- _selectedIconName,
488
- )?.icon ??
489
- getDefaultServerIcon(
490
- _selectedType.name,
491
- )!.icon)
492
- : getDefaultServerIcon(
493
- _selectedType.name,
494
- )!.icon,
495
- size: 24,
496
- color: colorScheme.primary,
490
+ child: Center(
491
+ child: _selectedIconName != null
492
+ ? HugeIcon(
493
+ icon:
494
+ (getHugeIconByName(
495
+ _selectedIconName,
496
+ )?.icon ??
497
+ getDefaultServerIcon(
498
+ _selectedType.name,
499
+ )!.icon),
500
+ size: 24,
501
+ color: colorScheme.primary,
502
+ )
503
+ : ServerLogo(
504
+ type: _selectedType,
505
+ size: _selectedType.hasLogo ? 32 : 24,
506
+ borderRadius: 7,
507
+ iconColor: colorScheme.primary,
508
+ ),
509
),
510
),
511
const SizedBox(width: 14),
@@ -711,14 +723,20 @@ class _AddServerScreenState extends ConsumerState<AddServerScreen> {
723
Container(
724
width: 52,
725
height: 52,
726
+ padding: _selectedType.hasLogo
727
+ ? const EdgeInsets.all(7)
728
+ : EdgeInsets.zero,
729
decoration: BoxDecoration(
730
color: colorScheme.primary.withValues(alpha: 0.14),
731
borderRadius: BorderRadius.circular(16),
732
),
718
- child: HugeIcon(
719
- icon: getDefaultServerIcon(_selectedType.name)!.icon,
720
- size: 28,
721
- color: colorScheme.primary,
733
+ child: Center(
734
+ child: ServerLogo(
735
+ type: _selectedType,
736
+ size: _selectedType.hasLogo ? 34 : 28,
737
+ borderRadius: 8,
738
+ iconColor: colorScheme.primary,
739
+ ),
740
),
741
),
742
const SizedBox(height: 12),
lib/features/servers/views/components/server_card.dart
+23
-19
@@ -1,6 +1,7 @@
1
import 'package:flutter/material.dart';
2
import 'package:hugeicons/hugeicons.dart';
3
import 'package:localmind/l10n/app_localizations.dart';
4
+import '../../../../core/components/server_logo.dart';
5
import '../../../../core/models/enums.dart';
6
import '../../data/models/server.dart';
7
import 'connection_status_indicator.dart';
@@ -29,13 +30,6 @@ class ServerCard extends StatelessWidget {
30
static const _error = Color(0xFFEF4444);
31
static const _neutral = Color(0xFF71717A);
32
32
- HugeIconData? get _serverIconData {
33
- if (server.iconName != null) {
34
- return getHugeIconByName(server.iconName);
35
- }
36
- return getDefaultServerIcon(server.type.name);
37
- }
38
-
33
String _serverTypeName(AppLocalizations l10n) {
34
switch (server.type) {
35
case ServerType.lmStudio:
@@ -221,16 +215,21 @@ class ServerCard extends StatelessWidget {
215
),
216
),
217
child: Center(
224
- child: _serverIconData != null
218
+ child: server.iconName != null
219
? HugeIcon(
226
- icon: _serverIconData!.icon,
220
+ icon:
221
+ (getHugeIconByName(server.iconName)?.icon ??
222
+ getDefaultServerIcon(server.type.name)!.icon),
223
size: 22,
224
color: isActive ? _accent : theme.colorScheme.onSurfaceVariant,
225
)
230
- : HugeIcon(
231
- icon: HugeIcons.strokeRoundedDatabase,
232
- size: 22,
233
- color: isActive ? _accent : theme.colorScheme.onSurfaceVariant,
226
+ : ServerLogo(
227
+ type: server.type,
228
+ size: server.type.hasLogo ? 26 : 22,
229
+ borderRadius: 6,
230
+ iconColor: isActive
231
+ ? _accent
232
+ : theme.colorScheme.onSurfaceVariant,
233
),
234
),
235
);
@@ -284,12 +283,17 @@ class ServerCard extends StatelessWidget {
283
child: Row(
284
mainAxisSize: MainAxisSize.min,
285
children: [
287
- HugeIcon(
288
- icon: _typeIcon(),
289
- size: 12,
290
- color: theme.colorScheme.onSurfaceVariant,
291
- ),
292
- const SizedBox(width: 5),
286
+ if (server.type.hasLogo) ...[
287
+ ServerLogo(type: server.type, size: 13, borderRadius: 2.5),
288
+ const SizedBox(width: 5),
289
+ ] else ...[
290
+ HugeIcon(
291
+ icon: _typeIcon(),
292
+ size: 12,
293
+ color: theme.colorScheme.onSurfaceVariant,
294
+ ),
295
+ const SizedBox(width: 5),
296
+ ],
297
Text(
298
_serverTypeName(l10n),
299
style: theme.textTheme.labelSmall?.copyWith(
lib/features/servers/views/components/server_picker_sheet.dart
+21
-13
@@ -3,6 +3,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
3
import 'package:go_router/go_router.dart';
4
import 'package:hugeicons/hugeicons.dart';
5
import 'package:shadcn_ui/shadcn_ui.dart';
6
+import '../../../../core/components/server_logo.dart';
7
import '../../../../core/routes/app_routes.dart';
8
import '../../../../l10n/app_localizations.dart';
9
import '../../providers/server_providers.dart';
@@ -100,10 +101,9 @@ void showServerPickerSheet(BuildContext context, WidgetRef ref) {
101
...servers.map((server) {
102
final isCurrentlyActive = server.id == activeServer?.id;
103
final serverIconName = server.iconName;
103
- final currentServerIcon = serverIconName != null
104
- ? (getHugeIconByName(serverIconName)?.icon ??
105
- getDefaultServerIcon(server.type.name)?.icon)
106
- : getDefaultServerIcon(server.type.name)?.icon;
104
+ final customHugeIcon = serverIconName != null
105
+ ? getHugeIconByName(serverIconName)
106
+ : null;
107
108
return Padding(
109
padding: const EdgeInsets.only(bottom: 8),
@@ -136,15 +136,23 @@ void showServerPickerSheet(BuildContext context, WidgetRef ref) {
136
),
137
child: Row(
138
children: [
139
- HugeIcon(
140
- icon:
141
- currentServerIcon ??
142
- HugeIcons.strokeRoundedServerStack01,
143
- size: 20,
144
- color: isCurrentlyActive
145
- ? colorScheme.primary
146
- : colorScheme.onSurface,
147
- ),
139
+ if (customHugeIcon != null)
140
+ HugeIcon(
141
+ icon: customHugeIcon.icon,
142
+ size: 20,
143
+ color: isCurrentlyActive
144
+ ? colorScheme.primary
145
+ : colorScheme.onSurface,
146
+ )
147
+ else
148
+ ServerLogo(
149
+ type: server.type,
150
+ size: 20,
151
+ borderRadius: 4,
152
+ iconColor: isCurrentlyActive
153
+ ? colorScheme.primary
154
+ : colorScheme.onSurface,
155
+ ),
156
const SizedBox(width: 12),
157
Expanded(
158
child: Column(
lib/features/servers/views/components/server_type_selector.dart
+13
-6
@@ -1,6 +1,7 @@
1
import 'package:flutter/material.dart';
2
import 'package:hugeicons/hugeicons.dart';
3
import 'package:localmind/l10n/app_localizations.dart';
4
+import '../../../../core/components/server_logo.dart';
5
import '../../../../core/models/enums.dart';
6
7
class ServerTypeSelector extends StatelessWidget {
@@ -113,18 +114,24 @@ class ServerTypeSelector extends StatelessWidget {
114
Container(
115
width: 32,
116
height: 32,
117
+ padding: type.hasLogo
118
+ ? const EdgeInsets.all(4)
119
+ : EdgeInsets.zero,
120
decoration: BoxDecoration(
121
color: isSelected
122
? accentColor.withValues(alpha: 0.20)
123
: colorScheme.surface,
124
borderRadius: BorderRadius.circular(10),
125
),
122
- child: HugeIcon(
123
- icon: icon,
124
- size: 18,
125
- color: isSelected
126
- ? accentColor
127
- : colorScheme.onSurface,
126
+ child: Center(
127
+ child: ServerLogo(
128
+ type: type,
129
+ customIcon: type.hasLogo ? null : icon,
130
+ size: type.hasLogo ? 22 : 18,
131
+ iconColor: isSelected
132
+ ? accentColor
133
+ : colorScheme.onSurface,
134
+ ),
135
),
136
),
137
const SizedBox(height: 6),
lib/features/sidebar/components/active_server_indicator.dart
+38
-17
@@ -5,6 +5,7 @@ import 'package:go_router/go_router.dart';
5
import 'package:shadcn_ui/shadcn_ui.dart';
6
import 'package:localmind/core/theme/colors.dart';
7
8
+import '../../../core/components/server_logo.dart';
9
import '../../../core/models/enums.dart';
10
import '../../../core/routes/app_routes.dart';
11
import '../../../l10n/app_localizations.dart';
@@ -244,16 +245,26 @@ class ActiveServerIndicator extends ConsumerWidget {
245
),
246
child: Row(
247
children: [
247
- HugeIcon(
248
- icon:
249
- currentServerIcon ??
250
- HugeIcons
251
- .strokeRoundedServerStack01,
252
- size: 20,
253
- color: isCurrentlyActive
254
- ? colorScheme.primary
255
- : colorScheme.onSurface,
256
- ),
248
+ if (serverIconName != null)
249
+ HugeIcon(
250
+ icon:
251
+ currentServerIcon ??
252
+ HugeIcons
253
+ .strokeRoundedServerStack01,
254
+ size: 20,
255
+ color: isCurrentlyActive
256
+ ? colorScheme.primary
257
+ : colorScheme.onSurface,
258
+ )
259
+ else
260
+ ServerLogo(
261
+ type: server.type,
262
+ size: 20,
263
+ borderRadius: 4,
264
+ iconColor: isCurrentlyActive
265
+ ? colorScheme.primary
266
+ : colorScheme.onSurface,
267
+ ),
268
const SizedBox(width: 12),
269
Expanded(
270
child: Column(
@@ -352,13 +363,23 @@ class ActiveServerIndicator extends ConsumerWidget {
363
),
364
child: Row(
365
children: [
355
- HugeIcon(
356
- icon: serverIcon ?? HugeIcons.strokeRoundedServerStack01,
357
- size: 18,
358
- color: isDark
359
- ? AppColors.darkPrimaryText
360
- : AppColors.lightPrimaryText,
361
- ),
366
+ if (iconName != null)
367
+ HugeIcon(
368
+ icon: serverIcon ?? HugeIcons.strokeRoundedServerStack01,
369
+ size: 18,
370
+ color: isDark
371
+ ? AppColors.darkPrimaryText
372
+ : AppColors.lightPrimaryText,
373
+ )
374
+ else
375
+ ServerLogo(
376
+ type: activeServer.type,
377
+ size: 18,
378
+ borderRadius: 3.5,
379
+ iconColor: isDark
380
+ ? AppColors.darkPrimaryText
381
+ : AppColors.lightPrimaryText,
382
+ ),
383
const SizedBox(width: 10),
384
Expanded(
385
child: Column(
pubspec.yaml
+1
-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.8.3+89
19
+version: 1.8.5+91
20
21
environment:
22
sdk: ">=3.12.0 <4.0.0"
test/core/components/server_logo_test.dart
new
+77
@@ -0,0 +1,77 @@
1
+import 'package:flutter/material.dart';
2
+import 'package:flutter_test/flutter_test.dart';
3
+import 'package:hugeicons/hugeicons.dart';
4
+import 'package:localmind/core/components/server_logo.dart';
5
+import 'package:localmind/core/models/enums.dart';
6
+
7
+void main() {
8
+ group('ServerTypeLogoExtension', () {
9
+ test('provides correct asset paths for brand logos', () {
10
+ expect(ServerType.lmStudio.logoAsset, 'assets/images/lmstudio.webp');
11
+ expect(ServerType.lmStudio.hasLogo, isTrue);
12
+
13
+ expect(ServerType.ollama.logoAsset, 'assets/images/ollama.webp');
14
+ expect(ServerType.ollama.hasLogo, isTrue);
15
+
16
+ expect(ServerType.ollamaCloud.logoAsset, 'assets/images/ollama.webp');
17
+ expect(ServerType.ollamaCloud.hasLogo, isTrue);
18
+
19
+ expect(ServerType.openRouter.logoAsset, 'assets/images/openrouter.webp');
20
+ expect(ServerType.openRouter.hasLogo, isTrue);
21
+
22
+ expect(ServerType.requesty.logoAsset, 'assets/images/requesty.webp');
23
+ expect(ServerType.requesty.hasLogo, isTrue);
24
+
25
+ expect(ServerType.openAICompatible.logoAsset, isNull);
26
+ expect(ServerType.openAICompatible.hasLogo, isFalse);
27
+
28
+ expect(ServerType.onDevice.logoAsset, isNull);
29
+ expect(ServerType.onDevice.hasLogo, isFalse);
30
+ });
31
+ });
32
+
33
+ group('ServerLogo widget', () {
34
+ testWidgets('renders Image for server types with brand logos', (tester) async {
35
+ await tester.pumpWidget(
36
+ const MaterialApp(
37
+ home: Scaffold(
38
+ body: ServerLogo(type: ServerType.lmStudio, size: 24),
39
+ ),
40
+ ),
41
+ );
42
+
43
+ expect(find.byType(Image), findsOneWidget);
44
+ expect(find.byType(HugeIcon), findsNothing);
45
+ });
46
+
47
+ testWidgets('renders HugeIcon for server types without brand logos', (tester) async {
48
+ await tester.pumpWidget(
49
+ const MaterialApp(
50
+ home: Scaffold(
51
+ body: ServerLogo(type: ServerType.openAICompatible, size: 24),
52
+ ),
53
+ ),
54
+ );
55
+
56
+ expect(find.byType(HugeIcon), findsOneWidget);
57
+ expect(find.byType(Image), findsNothing);
58
+ });
59
+
60
+ testWidgets('renders custom HugeIcon when customIcon is explicitly provided', (tester) async {
61
+ await tester.pumpWidget(
62
+ const MaterialApp(
63
+ home: Scaffold(
64
+ body: ServerLogo(
65
+ type: ServerType.lmStudio,
66
+ customIcon: HugeIcons.strokeRoundedStar,
67
+ size: 24,
68
+ ),
69
+ ),
70
+ ),
71
+ );
72
+
73
+ expect(find.byType(HugeIcon), findsOneWidget);
74
+ expect(find.byType(Image), findsNothing);
75
+ });
76
+ });
77
+}