@tduncan / Cortana / commits / a74eeb5

feat: add ServerLogo component and integrate server brand logos across views

abdulmominsakib committed Sep 30, 2026 at 09:14 UTC a74eeb5d4ab3fe4c1fdbfdcc811234407b05e000
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 +}