8 files changed
+120
-28
lib/core/providers/service_providers.dart
+83
-1
@@ -4,14 +4,96 @@ import '../constants/app_constants.dart';
4
import '../../features/servers/data/server_api_service.dart';
5
6
final dioProvider = Provider<Dio>((ref) {
7
- return Dio(
7
+ final dio = Dio(
8
BaseOptions(
9
connectTimeout: Duration(milliseconds: AppConstants.connectionTimeoutMs),
10
receiveTimeout: Duration(milliseconds: AppConstants.receiveTimeoutMs),
11
),
12
);
13
+ dio.interceptors.add(RedirectInterceptor(dio));
14
+ return dio;
15
});
16
17
final serverApiServiceProvider = Provider<ServerApiService>((ref) {
18
return ServerApiService(ref.read(dioProvider));
19
});
20
+
21
+class RedirectInterceptor extends Interceptor {
22
+ final Dio _dio;
23
+
24
+ RedirectInterceptor(this._dio);
25
+
26
+ @override
27
+ void onResponse(Response response, ResponseInterceptorHandler handler) async {
28
+ final statusCode = response.statusCode;
29
+ if (statusCode == 307 || statusCode == 308) {
30
+ final location = response.headers.value('location');
31
+ if (location != null) {
32
+ try {
33
+ final redirectedResponse = await _performRedirect(response.requestOptions, location);
34
+ return handler.resolve(redirectedResponse);
35
+ } catch (_) {
36
+ return handler.next(response);
37
+ }
38
+ }
39
+ }
40
+ super.onResponse(response, handler);
41
+ }
42
+
43
+ @override
44
+ void onError(DioException err, ErrorInterceptorHandler handler) async {
45
+ final response = err.response;
46
+ if (response != null && (response.statusCode == 307 || response.statusCode == 308)) {
47
+ final location = response.headers.value('location');
48
+ if (location != null) {
49
+ try {
50
+ final redirectedResponse = await _performRedirect(err.requestOptions, location);
51
+ return handler.resolve(redirectedResponse);
52
+ } catch (e) {
53
+ if (e is DioException) {
54
+ return handler.next(e);
55
+ }
56
+ return handler.next(DioException(
57
+ requestOptions: err.requestOptions,
58
+ error: e,
59
+ type: DioExceptionType.unknown,
60
+ ));
61
+ }
62
+ }
63
+ }
64
+ super.onError(err, handler);
65
+ }
66
+
67
+ Future<Response<dynamic>> _performRedirect(RequestOptions requestOptions, String location) async {
68
+ var redirectUrl = location;
69
+ if (!redirectUrl.startsWith('http://') && !redirectUrl.startsWith('https://')) {
70
+ final baseUri = Uri.parse(requestOptions.path);
71
+ redirectUrl = baseUri.resolve(redirectUrl).toString();
72
+ }
73
+
74
+ final extra = Map<String, dynamic>.from(requestOptions.extra);
75
+ final redirectCount = (extra['redirectCount'] as int? ?? 0) + 1;
76
+ if (redirectCount > 5) {
77
+ throw DioException(
78
+ requestOptions: requestOptions,
79
+ error: 'Too many redirects',
80
+ type: DioExceptionType.unknown,
81
+ );
82
+ }
83
+ extra['redirectCount'] = redirectCount;
84
+
85
+ return _dio.request(
86
+ redirectUrl,
87
+ data: requestOptions.data,
88
+ queryParameters: requestOptions.queryParameters,
89
+ options: Options(
90
+ method: requestOptions.method,
91
+ headers: requestOptions.headers,
92
+ contentType: requestOptions.contentType,
93
+ responseType: requestOptions.responseType,
94
+ extra: extra,
95
+ ),
96
+ );
97
+ }
98
+}
99
+
lib/features/models/data/models/model_info.dart
+10
-4
@@ -4,7 +4,7 @@ class ModelInfo {
4
final String id;
5
final String name;
6
final String? description;
7
- final int? parameterCount;
7
+ final double? parameterCount;
8
final int? contextLength;
9
final int? fileSize;
10
final String? quantization;
@@ -42,7 +42,7 @@ class ModelInfo {
42
}
43
44
String? get formattedSize {
45
- if (fileSize == null) return null;
45
+ if (fileSize == null || fileSize == 0) return null;
46
if (fileSize! < 1024 * 1024 * 1024) {
47
return '${(fileSize! / (1024 * 1024)).toStringAsFixed(1)} MB';
48
}
@@ -51,14 +51,20 @@ class ModelInfo {
51
52
String? get parameterCountDisplay {
53
if (parameterCount == null) return null;
54
- return '${parameterCount}B';
54
+ final formatted = parameterCount!.toStringAsFixed(2);
55
+ if (formatted.endsWith('.00')) {
56
+ return '${parameterCount!.toInt()}B';
57
+ } else if (formatted.endsWith('0')) {
58
+ return '${parameterCount!.toStringAsFixed(1)}B';
59
+ }
60
+ return '${parameterCount!.toStringAsFixed(2)}B';
61
}
62
63
ModelInfo copyWith({
64
String? id,
65
String? name,
66
String? description,
61
- int? parameterCount,
67
+ double? parameterCount,
68
int? contextLength,
69
int? fileSize,
70
String? quantization,
lib/features/models/screens/model_picker_sheet.dart
+7
-7
@@ -478,17 +478,17 @@ class _ModelTile extends StatelessWidget {
478
spacing: 6,
479
runSpacing: 4,
480
children: [
481
- if (model.parameterCountDisplay != null)
481
+ if (model.parameterCountDisplay != null && model.parameterCountDisplay!.isNotEmpty)
482
_MetadataChip(
483
label: model.parameterCountDisplay!,
484
isDark: isDark,
485
),
486
- if (model.quantization != null)
486
+ if (model.quantization != null && model.quantization!.isNotEmpty)
487
_MetadataChip(
488
label: model.quantization!,
489
isDark: isDark,
490
),
491
- if (model.formattedSize != null)
491
+ if (model.formattedSize != null && model.formattedSize!.isNotEmpty)
492
_MetadataChip(
493
label: model.formattedSize!,
494
isDark: isDark,
@@ -988,8 +988,8 @@ class _OnDeviceModelTile extends ConsumerWidget {
988
id: model.id,
989
name: model.name,
990
description: model.description,
991
- parameterCount: int.tryParse(
992
- model.parameterLabel.replaceAll(RegExp(r'[^0-9]'), ''),
991
+ parameterCount: double.tryParse(
992
+ model.parameterLabel.replaceAll(RegExp(r'[^0-9\.]'), ''),
993
),
994
serverType: ServerType.onDevice,
995
serverId: 'on-device',
@@ -1045,8 +1045,8 @@ class _OnDeviceModelTile extends ConsumerWidget {
1045
id: model.id,
1046
name: model.name,
1047
description: model.description,
1048
- parameterCount: int.tryParse(
1049
- model.parameterLabel.replaceAll(RegExp(r'[^0-9]'), ''),
1048
+ parameterCount: double.tryParse(
1049
+ model.parameterLabel.replaceAll(RegExp(r'[^0-9\.]'), ''),
1050
),
1051
serverType: ServerType.onDevice,
1052
serverId: 'on-device',
lib/features/servers/data/models/server.dart
+6
-1
@@ -34,7 +34,12 @@ class Server {
34
if (type == ServerType.onDevice) {
35
return 'on-device';
36
}
37
- return 'http://$host:$port';
37
+ final trimmedHost = host.trim();
38
+ if (trimmedHost.startsWith('http://') || trimmedHost.startsWith('https://')) {
39
+ return '$trimmedHost:$port';
40
+ }
41
+ final protocol = (port == 443) ? 'https' : 'http';
42
+ return '$protocol://$trimmedHost:$port';
43
}
44
45
String get chatEndpoint {
lib/features/servers/data/server_api_service.dart
+8
-11
@@ -286,15 +286,13 @@ class ServerApiService {
286
return models;
287
}
288
289
- int? _paramCountFromMeta(dynamic nParams) {
289
+ double? _paramCountFromMeta(dynamic nParams) {
290
if (nParams == null) return null;
291
- if (nParams is int) return nParams ~/ 1000000000;
292
- if (nParams is double) return (nParams / 1000000000).round();
291
+ if (nParams is int) return nParams / 1000000000;
292
+ if (nParams is double) return nParams / 1000000000;
293
if (nParams is String) {
294
- final parsedInt = int.tryParse(nParams);
295
- if (parsedInt != null) return parsedInt ~/ 1000000000;
294
final parsedDouble = double.tryParse(nParams);
297
- if (parsedDouble != null) return (parsedDouble / 1000000000).round();
295
+ if (parsedDouble != null) return parsedDouble / 1000000000;
296
}
297
return null;
298
}
@@ -358,18 +356,17 @@ class ServerApiService {
356
.join(' ');
357
}
358
361
- int? _parseParameterSize(String size) {
359
+ double? _parseParameterSize(String size) {
360
final cleaned = size.replaceAll('B', '').replaceAll('b', '').trim();
363
- return int.tryParse(cleaned);
361
+ return double.tryParse(cleaned);
362
}
363
366
- int? _parseParameterString(String? paramsString) {
364
+ double? _parseParameterString(String? paramsString) {
365
if (paramsString == null) return null;
366
final regex = RegExp(r'(\d+(?:\.\d+)?)\s*[Bb]');
367
final match = regex.firstMatch(paramsString);
368
if (match != null) {
371
- final value = double.tryParse(match.group(1) ?? '');
372
- return value?.toInt();
369
+ return double.tryParse(match.group(1) ?? '');
370
}
371
return null;
372
}
lib/features/servers/providers/server_providers.dart
+2
-2
@@ -120,8 +120,8 @@ final availableModelsProvider = FutureProvider.family<List<dynamic>, String>((
120
id: m.id,
121
name: m.name,
122
description: m.description,
123
- parameterCount: int.tryParse(
124
- m.parameterLabel.replaceAll(RegExp(r'[^0-9]'), ''),
123
+ parameterCount: double.tryParse(
124
+ m.parameterLabel.replaceAll(RegExp(r'[^0-9\.]'), ''),
125
),
126
serverType: ServerType.onDevice,
127
serverId: server.id,
lib/features/servers/views/add_server_screen.dart
+3
-1
@@ -193,8 +193,10 @@ class _AddServerScreenState extends ConsumerState<AddServerScreen> {
193
if (value == null || value.trim().isEmpty) {
194
return l10n.host_required;
195
}
196
+ // Clean any protocol prefixes (http:// or https://) for validation
197
+ final cleaned = value.trim().replaceFirst(RegExp(r'^https?://'), '');
198
final hostPattern = RegExp(r'^[a-zA-Z0-9]([a-zA-Z0-9\-\.]*[a-zA-Z0-9])?$');
197
- if (!hostPattern.hasMatch(value.trim())) {
199
+ if (!hostPattern.hasMatch(cleaned)) {
200
return l10n.host_valid;
201
}
202
return null;
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.2.8+27
19
+version: 1.2.9+28
20
21
environment:
22
sdk: ^3.11.0