feat: implement self-signed certificate support in API and UI
- Added support for self-signed TLS certificates in the ApiService, allowing configuration based on server settings. - Introduced a toggle in the ServerConnectionPage to enable or disable trusting self-signed certificates. - Updated localization files to include new strings for self-signed certificate settings in multiple languages. - Enhanced the OptimizedStorageService to manage trusted servers based on user preferences for self-signed certificates. - Improved error handling and logging throughout the affected services to ensure clarity and maintainability.
This commit is contained in:
@@ -224,8 +224,7 @@ flutter pub run build_runner build --delete-conflicting-outputs
|
|||||||
|
|
||||||
If you protect Open‑WebUI with SSO or a reverse proxy (Authlia, Authentik,
|
If you protect Open‑WebUI with SSO or a reverse proxy (Authlia, Authentik,
|
||||||
etc.), whitelist these path prefixes so Conduit can complete login, sync, and
|
etc.), whitelist these path prefixes so Conduit can complete login, sync, and
|
||||||
streaming flows. Paths are relative to your server base URL; replace tokens like
|
streaming flows. Paths are relative to your server base URL.
|
||||||
`{chatId}` with actual identifiers.
|
|
||||||
|
|
||||||
- `/health`
|
- `/health`
|
||||||
- `/api/*`
|
- `/api/*`
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
PODS:
|
PODS:
|
||||||
|
- connectivity_plus (0.0.1):
|
||||||
|
- Flutter
|
||||||
- DKImagePickerController/Core (4.3.9):
|
- DKImagePickerController/Core (4.3.9):
|
||||||
- DKImagePickerController/ImageDataManager
|
- DKImagePickerController/ImageDataManager
|
||||||
- DKImagePickerController/Resource
|
- DKImagePickerController/Resource
|
||||||
@@ -82,6 +84,7 @@ PODS:
|
|||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
|
|
||||||
DEPENDENCIES:
|
DEPENDENCIES:
|
||||||
|
- connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`)
|
||||||
- file_picker (from `.symlinks/plugins/file_picker/ios`)
|
- file_picker (from `.symlinks/plugins/file_picker/ios`)
|
||||||
- Flutter (from `Flutter`)
|
- Flutter (from `Flutter`)
|
||||||
- flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`)
|
- flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`)
|
||||||
@@ -110,6 +113,8 @@ SPEC REPOS:
|
|||||||
- SwiftyGif
|
- SwiftyGif
|
||||||
|
|
||||||
EXTERNAL SOURCES:
|
EXTERNAL SOURCES:
|
||||||
|
connectivity_plus:
|
||||||
|
:path: ".symlinks/plugins/connectivity_plus/ios"
|
||||||
file_picker:
|
file_picker:
|
||||||
:path: ".symlinks/plugins/file_picker/ios"
|
:path: ".symlinks/plugins/file_picker/ios"
|
||||||
Flutter:
|
Flutter:
|
||||||
@@ -150,6 +155,7 @@ EXTERNAL SOURCES:
|
|||||||
:path: ".symlinks/plugins/webview_flutter_wkwebview/darwin"
|
:path: ".symlinks/plugins/webview_flutter_wkwebview/darwin"
|
||||||
|
|
||||||
SPEC CHECKSUMS:
|
SPEC CHECKSUMS:
|
||||||
|
connectivity_plus: cb623214f4e1f6ef8fe7403d580fdad517d2f7dd
|
||||||
DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c
|
DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c
|
||||||
DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60
|
DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60
|
||||||
file_picker: a0560bc09d61de87f12d246fc47d2119e6ef37be
|
file_picker: a0560bc09d61de87f12d246fc47d2119e6ef37be
|
||||||
|
|||||||
@@ -13,6 +13,9 @@ sealed class ServerConfig with _$ServerConfig {
|
|||||||
@Default({}) Map<String, String> customHeaders,
|
@Default({}) Map<String, String> customHeaders,
|
||||||
DateTime? lastConnected,
|
DateTime? lastConnected,
|
||||||
@Default(false) bool isActive,
|
@Default(false) bool isActive,
|
||||||
|
|
||||||
|
/// Whether to trust self-signed TLS certificates for this server.
|
||||||
|
@Default(false) bool allowSelfSignedCertificates,
|
||||||
}) = _ServerConfig;
|
}) = _ServerConfig;
|
||||||
|
|
||||||
factory ServerConfig.fromJson(Map<String, dynamic> json) =>
|
factory ServerConfig.fromJson(Map<String, dynamic> json) =>
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'package:flutter/foundation.dart';
|
|
||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
|
import 'package:dio/io.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
// import 'package:http_parser/http_parser.dart';
|
// import 'package:http_parser/http_parser.dart';
|
||||||
// Removed legacy websocket/socket.io imports
|
// Removed legacy websocket/socket.io imports
|
||||||
import 'package:uuid/uuid.dart';
|
import 'package:uuid/uuid.dart';
|
||||||
@@ -62,6 +63,8 @@ class ApiService {
|
|||||||
: null,
|
: null,
|
||||||
),
|
),
|
||||||
) {
|
) {
|
||||||
|
_configureSelfSignedSupport();
|
||||||
|
|
||||||
// Use API key from server config if provided and no explicit auth token
|
// Use API key from server config if provided and no explicit auth token
|
||||||
final effectiveAuthToken = authToken ?? serverConfig.apiKey;
|
final effectiveAuthToken = authToken ?? serverConfig.apiKey;
|
||||||
|
|
||||||
@@ -125,6 +128,55 @@ class ApiService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _configureSelfSignedSupport() {
|
||||||
|
if (kIsWeb || !serverConfig.allowSelfSignedCertificates) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final baseUri = _parseBaseUri(serverConfig.url);
|
||||||
|
if (baseUri == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final adapter = _dio.httpClientAdapter;
|
||||||
|
if (adapter is! IOHttpClientAdapter) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
adapter.createHttpClient = () {
|
||||||
|
final client = HttpClient();
|
||||||
|
final host = baseUri.host.toLowerCase();
|
||||||
|
final port = baseUri.hasPort ? baseUri.port : null;
|
||||||
|
client.badCertificateCallback =
|
||||||
|
(X509Certificate cert, String requestHost, int requestPort) {
|
||||||
|
if (requestHost.toLowerCase() != host) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (port == null) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return requestPort == port;
|
||||||
|
};
|
||||||
|
return client;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
Uri? _parseBaseUri(String baseUrl) {
|
||||||
|
final trimmed = baseUrl.trim();
|
||||||
|
if (trimmed.isEmpty) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
Uri? parsed = Uri.tryParse(trimmed);
|
||||||
|
if (parsed == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (!parsed.hasScheme) {
|
||||||
|
parsed =
|
||||||
|
Uri.tryParse('https://$trimmed') ?? Uri.tryParse('http://$trimmed');
|
||||||
|
}
|
||||||
|
return parsed;
|
||||||
|
}
|
||||||
|
|
||||||
// Health check
|
// Health check
|
||||||
Future<bool> checkHealth() async {
|
Future<bool> checkHealth() async {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -217,7 +217,11 @@ class BackgroundStreamingHandler {
|
|||||||
DebugLogger.stream(
|
DebugLogger.stream(
|
||||||
'saveStreamStatesForRecovery called',
|
'saveStreamStatesForRecovery called',
|
||||||
scope: 'background',
|
scope: 'background',
|
||||||
data: {'streamIds': streamIds, 'reason': reason, 'statesCount': _streamStates.length},
|
data: {
|
||||||
|
'streamIds': streamIds,
|
||||||
|
'reason': reason,
|
||||||
|
'statesCount': _streamStates.length,
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
final statesToSave = streamIds
|
final statesToSave = streamIds
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import '../persistence/hive_boxes.dart';
|
|||||||
import '../persistence/persistence_keys.dart';
|
import '../persistence/persistence_keys.dart';
|
||||||
import '../utils/debug_logger.dart';
|
import '../utils/debug_logger.dart';
|
||||||
import 'secure_credential_storage.dart';
|
import 'secure_credential_storage.dart';
|
||||||
|
import 'self_signed_certificate_manager.dart';
|
||||||
|
|
||||||
/// Optimized storage service backed by Hive for non-sensitive data and
|
/// Optimized storage service backed by Hive for non-sensitive data and
|
||||||
/// FlutterSecureStorage for credentials.
|
/// FlutterSecureStorage for credentials.
|
||||||
@@ -196,6 +197,9 @@ class OptimizedStorageService {
|
|||||||
await _secureCredentialStorage.saveServerConfigs(jsonString);
|
await _secureCredentialStorage.saveServerConfigs(jsonString);
|
||||||
_cache['server_config_count'] = configs.length;
|
_cache['server_config_count'] = configs.length;
|
||||||
_cacheTimestamps['server_config_count'] = DateTime.now();
|
_cacheTimestamps['server_config_count'] = DateTime.now();
|
||||||
|
SelfSignedCertificateManager.instance
|
||||||
|
..ensureInitialized()
|
||||||
|
..updateTrustedServers(configs);
|
||||||
DebugLogger.log(
|
DebugLogger.log(
|
||||||
'Server configs saved (${configs.length} entries)',
|
'Server configs saved (${configs.length} entries)',
|
||||||
scope: 'storage/optimized',
|
scope: 'storage/optimized',
|
||||||
@@ -215,6 +219,9 @@ class OptimizedStorageService {
|
|||||||
if (jsonString == null || jsonString.isEmpty) {
|
if (jsonString == null || jsonString.isEmpty) {
|
||||||
_cache['server_config_count'] = 0;
|
_cache['server_config_count'] = 0;
|
||||||
_cacheTimestamps['server_config_count'] = DateTime.now();
|
_cacheTimestamps['server_config_count'] = DateTime.now();
|
||||||
|
SelfSignedCertificateManager.instance
|
||||||
|
..ensureInitialized()
|
||||||
|
..clearTrustedServers();
|
||||||
return const [];
|
return const [];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -224,12 +231,16 @@ class OptimizedStorageService {
|
|||||||
.toList();
|
.toList();
|
||||||
_cache['server_config_count'] = configs.length;
|
_cache['server_config_count'] = configs.length;
|
||||||
_cacheTimestamps['server_config_count'] = DateTime.now();
|
_cacheTimestamps['server_config_count'] = DateTime.now();
|
||||||
|
SelfSignedCertificateManager.instance
|
||||||
|
..ensureInitialized()
|
||||||
|
..updateTrustedServers(configs);
|
||||||
return configs;
|
return configs;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
DebugLogger.log(
|
DebugLogger.log(
|
||||||
'Failed to retrieve server configs: $error',
|
'Failed to retrieve server configs: $error',
|
||||||
scope: 'storage/optimized',
|
scope: 'storage/optimized',
|
||||||
);
|
);
|
||||||
|
SelfSignedCertificateManager.instance.clearTrustedServers();
|
||||||
return const [];
|
return const [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -234,7 +234,9 @@ class PersistentStreamingService with WidgetsBindingObserver {
|
|||||||
void unregisterStream(String streamId, {bool saveForRecovery = false}) {
|
void unregisterStream(String streamId, {bool saveForRecovery = false}) {
|
||||||
// If app is in background and stream is unregistering, it might be due to
|
// If app is in background and stream is unregistering, it might be due to
|
||||||
// network interruption - save state for recovery instead of just dropping it
|
// network interruption - save state for recovery instead of just dropping it
|
||||||
if (_isInBackground && !saveForRecovery && _streamMetadata.containsKey(streamId)) {
|
if (_isInBackground &&
|
||||||
|
!saveForRecovery &&
|
||||||
|
_streamMetadata.containsKey(streamId)) {
|
||||||
DebugLogger.stream(
|
DebugLogger.stream(
|
||||||
'PersistentStreamingService: Stream $streamId interrupted in background, saving for recovery',
|
'PersistentStreamingService: Stream $streamId interrupted in background, saving for recovery',
|
||||||
);
|
);
|
||||||
|
|||||||
23
lib/core/services/self_signed_certificate_manager.dart
Normal file
23
lib/core/services/self_signed_certificate_manager.dart
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import '../models/server_config.dart';
|
||||||
|
import 'self_signed_certificate_manager_io.dart'
|
||||||
|
if (dart.library.html) 'self_signed_certificate_manager_stub.dart'
|
||||||
|
as platform;
|
||||||
|
|
||||||
|
/// Coordinates opt-in trust for self-signed TLS certificates.
|
||||||
|
///
|
||||||
|
/// On IO platforms we install an [HttpOverrides] that whitelists the servers
|
||||||
|
/// flagged in [ServerConfig.allowSelfSignedCertificates]. On web platforms the
|
||||||
|
/// helpers are no-ops because browsers manage TLS validation themselves.
|
||||||
|
class SelfSignedCertificateManager {
|
||||||
|
const SelfSignedCertificateManager._();
|
||||||
|
|
||||||
|
static const SelfSignedCertificateManager instance =
|
||||||
|
SelfSignedCertificateManager._();
|
||||||
|
|
||||||
|
void ensureInitialized() => platform.ensureInitialized();
|
||||||
|
|
||||||
|
void updateTrustedServers(Iterable<ServerConfig> configs) =>
|
||||||
|
platform.updateTrustedServers(configs);
|
||||||
|
|
||||||
|
void clearTrustedServers() => platform.clearTrustedServers();
|
||||||
|
}
|
||||||
117
lib/core/services/self_signed_certificate_manager_io.dart
Normal file
117
lib/core/services/self_signed_certificate_manager_io.dart
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import '../models/server_config.dart';
|
||||||
|
|
||||||
|
final _IoSelfSignedCertificateManager _manager =
|
||||||
|
_IoSelfSignedCertificateManager();
|
||||||
|
|
||||||
|
void ensureInitialized() => _manager.ensureInitialized();
|
||||||
|
|
||||||
|
void updateTrustedServers(Iterable<ServerConfig> configs) =>
|
||||||
|
_manager.updateTrustedServers(configs);
|
||||||
|
|
||||||
|
void clearTrustedServers() => _manager.clearTrustedServers();
|
||||||
|
|
||||||
|
class _IoSelfSignedCertificateManager {
|
||||||
|
_IoSelfSignedCertificateManager();
|
||||||
|
|
||||||
|
_ConduitHttpOverrides? _overrides;
|
||||||
|
|
||||||
|
void ensureInitialized() {
|
||||||
|
if (_overrides != null) return;
|
||||||
|
|
||||||
|
final overrides = _ConduitHttpOverrides();
|
||||||
|
HttpOverrides.global = overrides;
|
||||||
|
_overrides = overrides;
|
||||||
|
}
|
||||||
|
|
||||||
|
void updateTrustedServers(Iterable<ServerConfig> configs) {
|
||||||
|
ensureInitialized();
|
||||||
|
_overrides?.updateTrustedServers(configs);
|
||||||
|
}
|
||||||
|
|
||||||
|
void clearTrustedServers() {
|
||||||
|
_overrides?.clearTrustedServers();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ConduitHttpOverrides extends HttpOverrides {
|
||||||
|
final Set<_TrustedEndpoint> _trustedEndpoints = {};
|
||||||
|
|
||||||
|
void updateTrustedServers(Iterable<ServerConfig> configs) {
|
||||||
|
_trustedEndpoints
|
||||||
|
..clear()
|
||||||
|
..addAll(
|
||||||
|
configs
|
||||||
|
.where((config) => config.allowSelfSignedCertificates)
|
||||||
|
.map((config) => _TrustedEndpoint.fromUrl(config.url))
|
||||||
|
.whereType<_TrustedEndpoint>(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void clearTrustedServers() {
|
||||||
|
_trustedEndpoints.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _shouldTrust(String host, int port) {
|
||||||
|
for (final endpoint in _trustedEndpoints) {
|
||||||
|
if (endpoint.matches(host, port)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
HttpClient createHttpClient(SecurityContext? context) {
|
||||||
|
final client = super.createHttpClient(context);
|
||||||
|
client.badCertificateCallback =
|
||||||
|
(X509Certificate cert, String host, int port) =>
|
||||||
|
_shouldTrust(host, port);
|
||||||
|
return client;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _TrustedEndpoint {
|
||||||
|
const _TrustedEndpoint({required this.host, this.port});
|
||||||
|
|
||||||
|
final String host;
|
||||||
|
final int? port;
|
||||||
|
|
||||||
|
static _TrustedEndpoint? fromUrl(String url) {
|
||||||
|
final uri = _normalizeUrl(url);
|
||||||
|
if (uri == null || uri.host.isEmpty) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
final normalizedHost = uri.host.toLowerCase();
|
||||||
|
final normalizedPort = uri.hasPort ? uri.port : null;
|
||||||
|
return _TrustedEndpoint(host: normalizedHost, port: normalizedPort);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Uri? _normalizeUrl(String value) {
|
||||||
|
if (value.trim().isEmpty) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
Uri? parsed = Uri.tryParse(value.trim());
|
||||||
|
if (parsed == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (!parsed.hasScheme) {
|
||||||
|
parsed =
|
||||||
|
Uri.tryParse('https://${value.trim()}') ??
|
||||||
|
Uri.tryParse('http://${value.trim()}');
|
||||||
|
}
|
||||||
|
return parsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool matches(String otherHost, int otherPort) {
|
||||||
|
final normalizedHost = otherHost.toLowerCase();
|
||||||
|
if (normalizedHost != host) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (port == null) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return port == otherPort;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import '../models/server_config.dart';
|
||||||
|
|
||||||
|
void ensureInitialized() {}
|
||||||
|
|
||||||
|
void updateTrustedServers(Iterable<ServerConfig> configs) {}
|
||||||
|
|
||||||
|
void clearTrustedServers() {}
|
||||||
@@ -107,16 +107,22 @@ ActiveSocketStream attachUnifiedChunkedStreaming({
|
|||||||
persistentController.add(data);
|
persistentController.add(data);
|
||||||
},
|
},
|
||||||
onDone: () async {
|
onDone: () async {
|
||||||
DebugLogger.stream('Source stream onDone fired, hasReceivedData=$hasReceivedData');
|
DebugLogger.stream(
|
||||||
|
'Source stream onDone fired, hasReceivedData=$hasReceivedData',
|
||||||
|
);
|
||||||
|
|
||||||
// If stream closes immediately without data, it's likely due to backgrounding/network drop
|
// If stream closes immediately without data, it's likely due to backgrounding/network drop
|
||||||
// Not a natural completion
|
// Not a natural completion
|
||||||
if (!hasReceivedData) {
|
if (!hasReceivedData) {
|
||||||
DebugLogger.stream('Stream closed without data - likely interrupted, not completing');
|
DebugLogger.stream(
|
||||||
|
'Stream closed without data - likely interrupted, not completing',
|
||||||
|
);
|
||||||
// Check if app is backgrounding - if so, finish streaming with whatever we have
|
// Check if app is backgrounding - if so, finish streaming with whatever we have
|
||||||
await Future.delayed(const Duration(milliseconds: 300));
|
await Future.delayed(const Duration(milliseconds: 300));
|
||||||
if (persistentService.isInBackground) {
|
if (persistentService.isInBackground) {
|
||||||
DebugLogger.stream('App backgrounding during stream - finishing with current content');
|
DebugLogger.stream(
|
||||||
|
'App backgrounding during stream - finishing with current content',
|
||||||
|
);
|
||||||
finishStreaming();
|
finishStreaming();
|
||||||
}
|
}
|
||||||
// Don't close the controller to prevent cascading completion handlers
|
// Don't close the controller to prevent cascading completion handlers
|
||||||
@@ -127,14 +133,20 @@ ActiveSocketStream attachUnifiedChunkedStreaming({
|
|||||||
await Future.delayed(const Duration(milliseconds: 500));
|
await Future.delayed(const Duration(milliseconds: 500));
|
||||||
|
|
||||||
final isInBg = persistentService.isInBackground;
|
final isInBg = persistentService.isInBackground;
|
||||||
DebugLogger.stream('Stream onDone check: streamId=$streamId, isInBackground=$isInBg');
|
DebugLogger.stream(
|
||||||
|
'Stream onDone check: streamId=$streamId, isInBackground=$isInBg',
|
||||||
|
);
|
||||||
|
|
||||||
// Check if we're in background before closing
|
// Check if we're in background before closing
|
||||||
if (!isInBg) {
|
if (!isInBg) {
|
||||||
DebugLogger.stream('Closing stream controller for $streamId (foreground completion)');
|
DebugLogger.stream(
|
||||||
|
'Closing stream controller for $streamId (foreground completion)',
|
||||||
|
);
|
||||||
persistentController.close();
|
persistentController.close();
|
||||||
} else {
|
} else {
|
||||||
DebugLogger.stream('Source stream completed in background for $streamId - keeping open for recovery');
|
DebugLogger.stream(
|
||||||
|
'Source stream completed in background for $streamId - keeping open for recovery',
|
||||||
|
);
|
||||||
// Finish streaming to save the content we have
|
// Finish streaming to save the content we have
|
||||||
finishStreaming();
|
finishStreaming();
|
||||||
}
|
}
|
||||||
@@ -417,7 +429,8 @@ ActiveSocketStream attachUnifiedChunkedStreaming({
|
|||||||
if (name is String && name.isNotEmpty) {
|
if (name is String && name.isNotEmpty) {
|
||||||
final msgs = getMessages();
|
final msgs = getMessages();
|
||||||
// Quick string check before expensive regex
|
// Quick string check before expensive regex
|
||||||
final exists = (msgs.isNotEmpty) &&
|
final exists =
|
||||||
|
(msgs.isNotEmpty) &&
|
||||||
msgs.last.content.contains('name="$name"');
|
msgs.last.content.contains('name="$name"');
|
||||||
if (!exists) {
|
if (!exists) {
|
||||||
final status =
|
final status =
|
||||||
@@ -519,7 +532,8 @@ ActiveSocketStream attachUnifiedChunkedStreaming({
|
|||||||
if (name is String && name.isNotEmpty) {
|
if (name is String && name.isNotEmpty) {
|
||||||
final msgs = getMessages();
|
final msgs = getMessages();
|
||||||
// Quick string check before expensive regex
|
// Quick string check before expensive regex
|
||||||
final exists = (msgs.isNotEmpty) &&
|
final exists =
|
||||||
|
(msgs.isNotEmpty) &&
|
||||||
msgs.last.content.contains('name="$name"');
|
msgs.last.content.contains('name="$name"');
|
||||||
if (!exists) {
|
if (!exists) {
|
||||||
final status =
|
final status =
|
||||||
@@ -549,7 +563,8 @@ ActiveSocketStream attachUnifiedChunkedStreaming({
|
|||||||
if (name is String && name.isNotEmpty) {
|
if (name is String && name.isNotEmpty) {
|
||||||
final msgs = getMessages();
|
final msgs = getMessages();
|
||||||
// Quick string check before expensive regex
|
// Quick string check before expensive regex
|
||||||
final exists = (msgs.isNotEmpty) &&
|
final exists =
|
||||||
|
(msgs.isNotEmpty) &&
|
||||||
msgs.last.content.contains('name="$name"');
|
msgs.last.content.contains('name="$name"');
|
||||||
if (!exists) {
|
if (!exists) {
|
||||||
final status =
|
final status =
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ class _ServerConnectionPageState extends ConsumerState<ServerConnectionPage> {
|
|||||||
String? _connectionError;
|
String? _connectionError;
|
||||||
bool _isConnecting = false;
|
bool _isConnecting = false;
|
||||||
bool _showAdvancedSettings = false;
|
bool _showAdvancedSettings = false;
|
||||||
|
bool _allowSelfSignedCertificates = false;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@@ -45,9 +46,11 @@ class _ServerConnectionPageState extends ConsumerState<ServerConnectionPage> {
|
|||||||
|
|
||||||
Future<void> _prefillFromState() async {
|
Future<void> _prefillFromState() async {
|
||||||
final activeServer = await ref.read(activeServerProvider.future);
|
final activeServer = await ref.read(activeServerProvider.future);
|
||||||
if (activeServer != null) {
|
if (!mounted || activeServer == null) return;
|
||||||
|
setState(() {
|
||||||
_urlController.text = activeServer.url;
|
_urlController.text = activeServer.url;
|
||||||
}
|
_allowSelfSignedCertificates = activeServer.allowSelfSignedCertificates;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -75,6 +78,7 @@ class _ServerConnectionPageState extends ConsumerState<ServerConnectionPage> {
|
|||||||
url: url,
|
url: url,
|
||||||
customHeaders: Map<String, String>.from(_customHeaders),
|
customHeaders: Map<String, String>.from(_customHeaders),
|
||||||
isActive: true,
|
isActive: true,
|
||||||
|
allowSelfSignedCertificates: _allowSelfSignedCertificates,
|
||||||
);
|
);
|
||||||
|
|
||||||
final api = ApiService(serverConfig: tempConfig);
|
final api = ApiService(serverConfig: tempConfig);
|
||||||
@@ -536,6 +540,69 @@ class _ServerConnectionPageState extends ConsumerState<ServerConnectionPage> {
|
|||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
|
Container(
|
||||||
|
width: double.infinity,
|
||||||
|
padding: const EdgeInsets.all(Spacing.md),
|
||||||
|
margin: const EdgeInsets.only(bottom: Spacing.md),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: context.conduitTheme.surfaceContainer.withValues(
|
||||||
|
alpha: 0.3,
|
||||||
|
),
|
||||||
|
borderRadius: BorderRadius.circular(AppBorderRadius.small),
|
||||||
|
border: Border.all(
|
||||||
|
color: context.conduitTheme.dividerColor.withValues(alpha: 0.4),
|
||||||
|
width: BorderWidth.thin,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Platform.isIOS
|
||||||
|
? CupertinoIcons.lock_shield
|
||||||
|
: Icons.verified_user,
|
||||||
|
color: context.conduitTheme.iconSecondary,
|
||||||
|
size: IconSize.small,
|
||||||
|
),
|
||||||
|
const SizedBox(width: Spacing.sm),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
AppLocalizations.of(
|
||||||
|
context,
|
||||||
|
)!.allowSelfSignedCertificates,
|
||||||
|
style: context.conduitTheme.bodySmall?.copyWith(
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: context.conduitTheme.textPrimary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: Spacing.xs),
|
||||||
|
Text(
|
||||||
|
AppLocalizations.of(
|
||||||
|
context,
|
||||||
|
)!.allowSelfSignedCertificatesDescription,
|
||||||
|
style: context.conduitTheme.bodySmall?.copyWith(
|
||||||
|
color: context.conduitTheme.textSecondary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: Spacing.sm),
|
||||||
|
Switch.adaptive(
|
||||||
|
value: _allowSelfSignedCertificates,
|
||||||
|
onChanged: (value) {
|
||||||
|
setState(() {
|
||||||
|
_allowSelfSignedCertificates = value;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
activeTrackColor: context.conduitTheme.buttonPrimary,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
|
|||||||
@@ -17,8 +17,7 @@ class VoiceCallNotificationService {
|
|||||||
// Notification IDs and channels
|
// Notification IDs and channels
|
||||||
static const String _channelId = 'voice_call_channel';
|
static const String _channelId = 'voice_call_channel';
|
||||||
static const String _channelName = 'Voice Call';
|
static const String _channelName = 'Voice Call';
|
||||||
static const String _channelDescription =
|
static const String _channelDescription = 'Ongoing voice call notifications';
|
||||||
'Ongoing voice call notifications';
|
|
||||||
static const int _notificationId = 2001;
|
static const int _notificationId = 2001;
|
||||||
|
|
||||||
// Action IDs
|
// Action IDs
|
||||||
@@ -32,7 +31,9 @@ class VoiceCallNotificationService {
|
|||||||
Future<void> initialize() async {
|
Future<void> initialize() async {
|
||||||
if (_initialized) return;
|
if (_initialized) return;
|
||||||
|
|
||||||
const androidSettings = AndroidInitializationSettings('@mipmap/ic_launcher');
|
const androidSettings = AndroidInitializationSettings(
|
||||||
|
'@mipmap/ic_launcher',
|
||||||
|
);
|
||||||
const iosSettings = DarwinInitializationSettings(
|
const iosSettings = DarwinInitializationSettings(
|
||||||
requestAlertPermission: false,
|
requestAlertPermission: false,
|
||||||
requestBadgePermission: false,
|
requestBadgePermission: false,
|
||||||
@@ -72,7 +73,8 @@ class VoiceCallNotificationService {
|
|||||||
|
|
||||||
await _notifications
|
await _notifications
|
||||||
.resolvePlatformSpecificImplementation<
|
.resolvePlatformSpecificImplementation<
|
||||||
AndroidFlutterLocalNotificationsPlugin>()
|
AndroidFlutterLocalNotificationsPlugin
|
||||||
|
>()
|
||||||
?.createNotificationChannel(androidChannel);
|
?.createNotificationChannel(androidChannel);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -190,8 +192,10 @@ class VoiceCallNotificationService {
|
|||||||
/// Check if notifications are enabled
|
/// Check if notifications are enabled
|
||||||
Future<bool> areNotificationsEnabled() async {
|
Future<bool> areNotificationsEnabled() async {
|
||||||
if (Platform.isAndroid) {
|
if (Platform.isAndroid) {
|
||||||
final androidImpl = _notifications.resolvePlatformSpecificImplementation<
|
final androidImpl = _notifications
|
||||||
AndroidFlutterLocalNotificationsPlugin>();
|
.resolvePlatformSpecificImplementation<
|
||||||
|
AndroidFlutterLocalNotificationsPlugin
|
||||||
|
>();
|
||||||
return await androidImpl?.areNotificationsEnabled() ?? false;
|
return await androidImpl?.areNotificationsEnabled() ?? false;
|
||||||
} else if (Platform.isIOS) {
|
} else if (Platform.isIOS) {
|
||||||
// iOS doesn't have a direct check, assume enabled if initialized
|
// iOS doesn't have a direct check, assume enabled if initialized
|
||||||
@@ -203,13 +207,17 @@ class VoiceCallNotificationService {
|
|||||||
/// Request notification permissions
|
/// Request notification permissions
|
||||||
Future<bool> requestPermissions() async {
|
Future<bool> requestPermissions() async {
|
||||||
if (Platform.isAndroid) {
|
if (Platform.isAndroid) {
|
||||||
final androidImpl = _notifications.resolvePlatformSpecificImplementation<
|
final androidImpl = _notifications
|
||||||
AndroidFlutterLocalNotificationsPlugin>();
|
.resolvePlatformSpecificImplementation<
|
||||||
|
AndroidFlutterLocalNotificationsPlugin
|
||||||
|
>();
|
||||||
final granted = await androidImpl?.requestNotificationsPermission();
|
final granted = await androidImpl?.requestNotificationsPermission();
|
||||||
return granted ?? false;
|
return granted ?? false;
|
||||||
} else if (Platform.isIOS) {
|
} else if (Platform.isIOS) {
|
||||||
final iosImpl = _notifications
|
final iosImpl = _notifications
|
||||||
.resolvePlatformSpecificImplementation<IOSFlutterLocalNotificationsPlugin>();
|
.resolvePlatformSpecificImplementation<
|
||||||
|
IOSFlutterLocalNotificationsPlugin
|
||||||
|
>();
|
||||||
final granted = await iosImpl?.requestPermissions(
|
final granted = await iosImpl?.requestPermissions(
|
||||||
alert: true,
|
alert: true,
|
||||||
badge: false,
|
badge: false,
|
||||||
|
|||||||
@@ -148,9 +148,8 @@ class _VoiceCallPageState extends ConsumerState<VoiceCallPage>
|
|||||||
icon: const Icon(CupertinoIcons.xmark),
|
icon: const Icon(CupertinoIcons.xmark),
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
await _service?.stopCall();
|
await _service?.stopCall();
|
||||||
if (mounted) {
|
if (!context.mounted) return;
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
}
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1480,9 +1480,7 @@ class _ModernChatInputState extends ConsumerState<ModernChatInput>
|
|||||||
),
|
),
|
||||||
child: Center(
|
child: Center(
|
||||||
child: Icon(
|
child: Icon(
|
||||||
Platform.isIOS
|
Platform.isIOS ? CupertinoIcons.waveform : Icons.graphic_eq,
|
||||||
? CupertinoIcons.waveform
|
|
||||||
: Icons.graphic_eq,
|
|
||||||
size: IconSize.large,
|
size: IconSize.large,
|
||||||
color: enabledVoiceCall
|
color: enabledVoiceCall
|
||||||
? context.conduitTheme.buttonPrimaryText
|
? context.conduitTheme.buttonPrimaryText
|
||||||
|
|||||||
@@ -259,6 +259,14 @@
|
|||||||
"advancedSettings": "Erweiterte Einstellungen",
|
"advancedSettings": "Erweiterte Einstellungen",
|
||||||
"customHeaders": "Benutzerdefinierte Header",
|
"customHeaders": "Benutzerdefinierte Header",
|
||||||
"customHeadersDescription": "Füge benutzerdefinierte HTTP-Header für Authentifizierung, API-Schlüssel oder spezielle Serveranforderungen hinzu.",
|
"customHeadersDescription": "Füge benutzerdefinierte HTTP-Header für Authentifizierung, API-Schlüssel oder spezielle Serveranforderungen hinzu.",
|
||||||
|
"allowSelfSignedCertificates": "Selbstsignierten Zertifikaten vertrauen",
|
||||||
|
"@allowSelfSignedCertificates": {
|
||||||
|
"description": "Schalterbeschriftung zum Zulassen selbstsignierter TLS-Zertifikate für den konfigurierten Server."
|
||||||
|
},
|
||||||
|
"allowSelfSignedCertificatesDescription": "Akzeptiere das TLS-Zertifikat dieses Servers auch dann, wenn es selbstsigniert ist. Aktiviere diese Option nur für Server, denen du vertraust.",
|
||||||
|
"@allowSelfSignedCertificatesDescription": {
|
||||||
|
"description": "Hilfetext, der die Risiken beim Aktivieren des Schalters für selbstsignierte Zertifikate erklärt."
|
||||||
|
},
|
||||||
"headerNameEmpty": "Header-Name darf nicht leer sein",
|
"headerNameEmpty": "Header-Name darf nicht leer sein",
|
||||||
"headerNameTooLong": "Header-Name zu lang (max. 64 Zeichen)",
|
"headerNameTooLong": "Header-Name zu lang (max. 64 Zeichen)",
|
||||||
"headerNameInvalidChars": "Ungültiger Header-Name. Verwende nur Buchstaben, Zahlen und diese Zeichen: !#$&-^_`|~",
|
"headerNameInvalidChars": "Ungültiger Header-Name. Verwende nur Buchstaben, Zahlen und diese Zeichen: !#$&-^_`|~",
|
||||||
|
|||||||
@@ -550,6 +550,14 @@
|
|||||||
"@customHeaders": {"description": "Section title for adding custom HTTP headers."},
|
"@customHeaders": {"description": "Section title for adding custom HTTP headers."},
|
||||||
"customHeadersDescription": "Add custom HTTP headers for authentication, API keys, or special server requirements.",
|
"customHeadersDescription": "Add custom HTTP headers for authentication, API keys, or special server requirements.",
|
||||||
"@customHeadersDescription": {"description": "Helper text explaining use-cases for custom headers."},
|
"@customHeadersDescription": {"description": "Helper text explaining use-cases for custom headers."},
|
||||||
|
"allowSelfSignedCertificates": "Trust self-signed certificates",
|
||||||
|
"@allowSelfSignedCertificates": {
|
||||||
|
"description": "Toggle label that allows trusting self-signed TLS certificates for the configured server."
|
||||||
|
},
|
||||||
|
"allowSelfSignedCertificatesDescription": "Accept this server's TLS certificate even if it is self-signed. Enable only for servers you trust.",
|
||||||
|
"@allowSelfSignedCertificatesDescription": {
|
||||||
|
"description": "Helper text clarifying the risks of enabling the self-signed certificate toggle."
|
||||||
|
},
|
||||||
"headerNameEmpty": "Header name cannot be empty",
|
"headerNameEmpty": "Header name cannot be empty",
|
||||||
"@headerNameEmpty": {"description": "Validation message for empty header name."},
|
"@headerNameEmpty": {"description": "Validation message for empty header name."},
|
||||||
"headerNameTooLong": "Header name too long (max 64 characters)",
|
"headerNameTooLong": "Header name too long (max 64 characters)",
|
||||||
|
|||||||
@@ -259,6 +259,14 @@
|
|||||||
"advancedSettings": "Paramètres avancés",
|
"advancedSettings": "Paramètres avancés",
|
||||||
"customHeaders": "En-têtes personnalisés",
|
"customHeaders": "En-têtes personnalisés",
|
||||||
"customHeadersDescription": "Ajoutez des en-têtes HTTP personnalisés pour l'authentification, les clés API ou des exigences spécifiques du serveur.",
|
"customHeadersDescription": "Ajoutez des en-têtes HTTP personnalisés pour l'authentification, les clés API ou des exigences spécifiques du serveur.",
|
||||||
|
"allowSelfSignedCertificates": "Faire confiance aux certificats auto-signés",
|
||||||
|
"@allowSelfSignedCertificates": {
|
||||||
|
"description": "Libellé du commutateur permettant d'autoriser les certificats TLS auto-signés pour le serveur configuré."
|
||||||
|
},
|
||||||
|
"allowSelfSignedCertificatesDescription": "Acceptez le certificat TLS de ce serveur même s'il est auto-signé. Activez cette option uniquement pour les serveurs auxquels vous faites confiance.",
|
||||||
|
"@allowSelfSignedCertificatesDescription": {
|
||||||
|
"description": "Texte d'aide expliquant les risques liés à l'activation de l'option de certificat auto-signé."
|
||||||
|
},
|
||||||
"headerNameEmpty": "Le nom de l'en-tête ne peut pas être vide",
|
"headerNameEmpty": "Le nom de l'en-tête ne peut pas être vide",
|
||||||
"headerNameTooLong": "Nom d'en-tête trop long (max 64 caractères)",
|
"headerNameTooLong": "Nom d'en-tête trop long (max 64 caractères)",
|
||||||
"headerNameInvalidChars": "Nom d'en-tête invalide. Utilisez uniquement des lettres, des chiffres et ces symboles : !#$&-^_`|~",
|
"headerNameInvalidChars": "Nom d'en-tête invalide. Utilisez uniquement des lettres, des chiffres et ces symboles : !#$&-^_`|~",
|
||||||
|
|||||||
@@ -259,6 +259,14 @@
|
|||||||
"advancedSettings": "Impostazioni avanzate",
|
"advancedSettings": "Impostazioni avanzate",
|
||||||
"customHeaders": "Header personalizzati",
|
"customHeaders": "Header personalizzati",
|
||||||
"customHeadersDescription": "Aggiungi header HTTP personalizzati per autenticazione, chiavi API o requisiti speciali del server.",
|
"customHeadersDescription": "Aggiungi header HTTP personalizzati per autenticazione, chiavi API o requisiti speciali del server.",
|
||||||
|
"allowSelfSignedCertificates": "Considera attendibili i certificati autofirmati",
|
||||||
|
"@allowSelfSignedCertificates": {
|
||||||
|
"description": "Etichetta dell'interruttore che consente i certificati TLS autofirmati per il server configurato."
|
||||||
|
},
|
||||||
|
"allowSelfSignedCertificatesDescription": "Accetta il certificato TLS di questo server anche se è autofirmato. Attiva questa opzione solo per server di cui ti fidi.",
|
||||||
|
"@allowSelfSignedCertificatesDescription": {
|
||||||
|
"description": "Testo di supporto che chiarisce i rischi dell'attivazione dell'opzione per certificati autofirmati."
|
||||||
|
},
|
||||||
"headerNameEmpty": "Il nome header non può essere vuoto",
|
"headerNameEmpty": "Il nome header non può essere vuoto",
|
||||||
"headerNameTooLong": "Nome header troppo lungo (max 64 caratteri)",
|
"headerNameTooLong": "Nome header troppo lungo (max 64 caratteri)",
|
||||||
"headerNameInvalidChars": "Nome header non valido. Usa solo lettere, numeri e questi simboli: !#$&-^_`|~",
|
"headerNameInvalidChars": "Nome header non valido. Usa solo lettere, numeri e questi simboli: !#$&-^_`|~",
|
||||||
|
|||||||
@@ -1482,6 +1482,18 @@ abstract class AppLocalizations {
|
|||||||
/// **'Add custom HTTP headers for authentication, API keys, or special server requirements.'**
|
/// **'Add custom HTTP headers for authentication, API keys, or special server requirements.'**
|
||||||
String get customHeadersDescription;
|
String get customHeadersDescription;
|
||||||
|
|
||||||
|
/// Toggle label that allows trusting self-signed TLS certificates for the configured server.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Trust self-signed certificates'**
|
||||||
|
String get allowSelfSignedCertificates;
|
||||||
|
|
||||||
|
/// Helper text clarifying the risks of enabling the self-signed certificate toggle.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Accept this server\'s TLS certificate even if it is self-signed. Enable only for servers you trust.'**
|
||||||
|
String get allowSelfSignedCertificatesDescription;
|
||||||
|
|
||||||
/// Validation message for empty header name.
|
/// Validation message for empty header name.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
|
|||||||
@@ -760,6 +760,14 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||||||
String get customHeadersDescription =>
|
String get customHeadersDescription =>
|
||||||
'Füge benutzerdefinierte HTTP-Header für Authentifizierung, API-Schlüssel oder spezielle Serveranforderungen hinzu.';
|
'Füge benutzerdefinierte HTTP-Header für Authentifizierung, API-Schlüssel oder spezielle Serveranforderungen hinzu.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get allowSelfSignedCertificates =>
|
||||||
|
'Selbstsignierten Zertifikaten vertrauen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get allowSelfSignedCertificatesDescription =>
|
||||||
|
'Akzeptiere das TLS-Zertifikat dieses Servers auch dann, wenn es selbstsigniert ist. Aktiviere diese Option nur für Server, denen du vertraust.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get headerNameEmpty => 'Header-Name darf nicht leer sein';
|
String get headerNameEmpty => 'Header-Name darf nicht leer sein';
|
||||||
|
|
||||||
|
|||||||
@@ -754,6 +754,13 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||||||
String get customHeadersDescription =>
|
String get customHeadersDescription =>
|
||||||
'Add custom HTTP headers for authentication, API keys, or special server requirements.';
|
'Add custom HTTP headers for authentication, API keys, or special server requirements.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get allowSelfSignedCertificates => 'Trust self-signed certificates';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get allowSelfSignedCertificatesDescription =>
|
||||||
|
'Accept this server\'s TLS certificate even if it is self-signed. Enable only for servers you trust.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get headerNameEmpty => 'Header name cannot be empty';
|
String get headerNameEmpty => 'Header name cannot be empty';
|
||||||
|
|
||||||
|
|||||||
@@ -765,6 +765,14 @@ class AppLocalizationsFr extends AppLocalizations {
|
|||||||
String get customHeadersDescription =>
|
String get customHeadersDescription =>
|
||||||
'Ajoutez des en-têtes HTTP personnalisés pour l\'authentification, les clés API ou des exigences spécifiques du serveur.';
|
'Ajoutez des en-têtes HTTP personnalisés pour l\'authentification, les clés API ou des exigences spécifiques du serveur.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get allowSelfSignedCertificates =>
|
||||||
|
'Faire confiance aux certificats auto-signés';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get allowSelfSignedCertificatesDescription =>
|
||||||
|
'Acceptez le certificat TLS de ce serveur même s\'il est auto-signé. Activez cette option uniquement pour les serveurs auxquels vous faites confiance.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get headerNameEmpty => 'Le nom de l\'en-tête ne peut pas être vide';
|
String get headerNameEmpty => 'Le nom de l\'en-tête ne peut pas être vide';
|
||||||
|
|
||||||
|
|||||||
@@ -756,6 +756,14 @@ class AppLocalizationsIt extends AppLocalizations {
|
|||||||
String get customHeadersDescription =>
|
String get customHeadersDescription =>
|
||||||
'Aggiungi header HTTP personalizzati per autenticazione, chiavi API o requisiti speciali del server.';
|
'Aggiungi header HTTP personalizzati per autenticazione, chiavi API o requisiti speciali del server.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get allowSelfSignedCertificates =>
|
||||||
|
'Considera attendibili i certificati autofirmati';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get allowSelfSignedCertificatesDescription =>
|
||||||
|
'Accetta il certificato TLS di questo server anche se è autofirmato. Attiva questa opzione solo per server di cui ti fidi.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get headerNameEmpty => 'Il nome header non può essere vuoto';
|
String get headerNameEmpty => 'Il nome header non può essere vuoto';
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import 'core/providers/app_providers.dart';
|
|||||||
import 'core/persistence/hive_bootstrap.dart';
|
import 'core/persistence/hive_bootstrap.dart';
|
||||||
import 'core/persistence/persistence_migrator.dart';
|
import 'core/persistence/persistence_migrator.dart';
|
||||||
import 'core/persistence/persistence_providers.dart';
|
import 'core/persistence/persistence_providers.dart';
|
||||||
|
import 'core/services/self_signed_certificate_manager.dart';
|
||||||
import 'core/router/app_router.dart';
|
import 'core/router/app_router.dart';
|
||||||
import 'shared/widgets/offline_indicator.dart';
|
import 'shared/widgets/offline_indicator.dart';
|
||||||
import 'features/auth/providers/unified_auth_providers.dart';
|
import 'features/auth/providers/unified_auth_providers.dart';
|
||||||
@@ -29,6 +30,8 @@ void main() {
|
|||||||
() async {
|
() async {
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
||||||
|
SelfSignedCertificateManager.instance.ensureInitialized();
|
||||||
|
|
||||||
// Global error handlers
|
// Global error handlers
|
||||||
FlutterError.onError = (FlutterErrorDetails details) {
|
FlutterError.onError = (FlutterErrorDetails details) {
|
||||||
DebugLogger.error(
|
DebugLogger.error(
|
||||||
|
|||||||
@@ -727,7 +727,9 @@ class _DetailsBlockSyntax extends md.BlockSyntax {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check for summary tag
|
// Check for summary tag
|
||||||
final summaryMatch = RegExp(r'^<summary>(.*?)<\/summary>$').firstMatch(line);
|
final summaryMatch = RegExp(
|
||||||
|
r'^<summary>(.*?)<\/summary>$',
|
||||||
|
).firstMatch(line);
|
||||||
if (summaryMatch != null) {
|
if (summaryMatch != null) {
|
||||||
summary = summaryMatch.group(1) ?? '';
|
summary = summaryMatch.group(1) ?? '';
|
||||||
parser.advance();
|
parser.advance();
|
||||||
|
|||||||
@@ -13,10 +13,7 @@ class ConduitMarkdownPreprocessor {
|
|||||||
r'^[ \t]+```([^\n`]*)\s*$',
|
r'^[ \t]+```([^\n`]*)\s*$',
|
||||||
multiLine: true,
|
multiLine: true,
|
||||||
);
|
);
|
||||||
static final _dedentCloseRegex = RegExp(
|
static final _dedentCloseRegex = RegExp(r'^[ \t]+```\s*$', multiLine: true);
|
||||||
r'^[ \t]+```\s*$',
|
|
||||||
multiLine: true,
|
|
||||||
);
|
|
||||||
static final _inlineClosingRegex = RegExp(r'([^\r\n`])```(?=\s*(?:\r?\n|$))');
|
static final _inlineClosingRegex = RegExp(r'([^\r\n`])```(?=\s*(?:\r?\n|$))');
|
||||||
static final _labelThenDashRegex = RegExp(
|
static final _labelThenDashRegex = RegExp(
|
||||||
r'^(\*\*[^\n*]+\*\*.*)\n(\s*-{3,}\s*$)',
|
r'^(\*\*[^\n*]+\*\*.*)\n(\s*-{3,}\s*$)',
|
||||||
@@ -45,7 +42,10 @@ class ConduitMarkdownPreprocessor {
|
|||||||
|
|
||||||
// Dedent opening fences to avoid partial code-block detection when the
|
// Dedent opening fences to avoid partial code-block detection when the
|
||||||
// model indents fences by accident.
|
// model indents fences by accident.
|
||||||
output = output.replaceAllMapped(_dedentOpenRegex, (match) => '```${match[1]}');
|
output = output.replaceAllMapped(
|
||||||
|
_dedentOpenRegex,
|
||||||
|
(match) => '```${match[1]}',
|
||||||
|
);
|
||||||
|
|
||||||
// Dedent closing fences for the same reason as the opening fences.
|
// Dedent closing fences for the same reason as the opening fences.
|
||||||
output = output.replaceAllMapped(_dedentCloseRegex, (_) => '```');
|
output = output.replaceAllMapped(_dedentCloseRegex, (_) => '```');
|
||||||
|
|||||||
Reference in New Issue
Block a user