refactor: formatting
This commit is contained in:
@@ -15,7 +15,9 @@ class TokenValidator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check if it's an API key format (starts with sk- or similar)
|
// Check if it's an API key format (starts with sk- or similar)
|
||||||
if (token.startsWith('sk-') || token.startsWith('api-') || token.startsWith('key-')) {
|
if (token.startsWith('sk-') ||
|
||||||
|
token.startsWith('api-') ||
|
||||||
|
token.startsWith('key-')) {
|
||||||
// API key format - validate differently
|
// API key format - validate differently
|
||||||
if (token.length < 20) {
|
if (token.length < 20) {
|
||||||
return TokenValidationResult.invalid('API key too short');
|
return TokenValidationResult.invalid('API key too short');
|
||||||
|
|||||||
@@ -22,17 +22,23 @@ sealed class Folder with _$Folder {
|
|||||||
// Extract conversation IDs from items.chats if available
|
// Extract conversation IDs from items.chats if available
|
||||||
final items = json['items'] as Map<String, dynamic>?;
|
final items = json['items'] as Map<String, dynamic>?;
|
||||||
final chats = items?['chats'] as List?;
|
final chats = items?['chats'] as List?;
|
||||||
|
|
||||||
// Handle both string IDs and conversation objects
|
// Handle both string IDs and conversation objects
|
||||||
final conversationIds = chats?.map((chat) {
|
final conversationIds =
|
||||||
if (chat is String) {
|
chats
|
||||||
return chat;
|
?.map((chat) {
|
||||||
} else if (chat is Map<String, dynamic>) {
|
if (chat is String) {
|
||||||
return chat['id'] as String? ?? '';
|
return chat;
|
||||||
}
|
} else if (chat is Map<String, dynamic>) {
|
||||||
return '';
|
return chat['id'] as String? ?? '';
|
||||||
}).where((id) => id.isNotEmpty).toList().cast<String>() ?? <String>[];
|
}
|
||||||
|
return '';
|
||||||
|
})
|
||||||
|
.where((id) => id.isNotEmpty)
|
||||||
|
.toList()
|
||||||
|
.cast<String>() ??
|
||||||
|
<String>[];
|
||||||
|
|
||||||
// Handle Unix timestamp conversion
|
// Handle Unix timestamp conversion
|
||||||
DateTime? parseTimestamp(dynamic timestamp) {
|
DateTime? parseTimestamp(dynamic timestamp) {
|
||||||
if (timestamp == null) return null;
|
if (timestamp == null) return null;
|
||||||
@@ -44,7 +50,7 @@ sealed class Folder with _$Folder {
|
|||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create the modified JSON with proper field mapping
|
// Create the modified JSON with proper field mapping
|
||||||
return Folder(
|
return Folder(
|
||||||
id: json['id'] as String,
|
id: json['id'] as String,
|
||||||
|
|||||||
@@ -23,4 +23,4 @@ sealed class Tool with _$Tool {
|
|||||||
meta: json['meta'] as Map<String, dynamic>?,
|
meta: json['meta'] as Map<String, dynamic>?,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,12 +43,12 @@ class InputValidationService {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
final uri = Uri.parse(urlToValidate);
|
final uri = Uri.parse(urlToValidate);
|
||||||
|
|
||||||
// Validate scheme
|
// Validate scheme
|
||||||
if (!uri.hasScheme || (uri.scheme != 'http' && uri.scheme != 'https')) {
|
if (!uri.hasScheme || (uri.scheme != 'http' && uri.scheme != 'https')) {
|
||||||
return 'Use http:// or https:// only';
|
return 'Use http:// or https:// only';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate host
|
// Validate host
|
||||||
if (!uri.hasAuthority || uri.host.isEmpty) {
|
if (!uri.hasAuthority || uri.host.isEmpty) {
|
||||||
return 'Please enter a server address (e.g., 192.168.1.10:3000)';
|
return 'Please enter a server address (e.g., 192.168.1.10:3000)';
|
||||||
@@ -65,7 +65,6 @@ class InputValidationService {
|
|||||||
if (_isIPAddress(uri.host) && !_isValidIPAddress(uri.host)) {
|
if (_isIPAddress(uri.host) && !_isValidIPAddress(uri.host)) {
|
||||||
return 'Invalid IP address format (use 192.168.1.10)';
|
return 'Invalid IP address format (use 192.168.1.10)';
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return 'Invalid server address format';
|
return 'Invalid server address format';
|
||||||
}
|
}
|
||||||
@@ -82,7 +81,7 @@ class InputValidationService {
|
|||||||
static bool _isValidIPAddress(String ip) {
|
static bool _isValidIPAddress(String ip) {
|
||||||
final parts = ip.split('.');
|
final parts = ip.split('.');
|
||||||
if (parts.length != 4) return false;
|
if (parts.length != 4) return false;
|
||||||
|
|
||||||
for (final part in parts) {
|
for (final part in parts) {
|
||||||
final num = int.tryParse(part);
|
final num = int.tryParse(part);
|
||||||
if (num == null || num < 0 || num > 255) return false;
|
if (num == null || num < 0 || num > 255) return false;
|
||||||
|
|||||||
@@ -16,7 +16,9 @@ class OptimizedStorageService {
|
|||||||
required FlutterSecureStorage secureStorage,
|
required FlutterSecureStorage secureStorage,
|
||||||
required SharedPreferences prefs,
|
required SharedPreferences prefs,
|
||||||
}) : _prefs = prefs,
|
}) : _prefs = prefs,
|
||||||
_secureCredentialStorage = SecureCredentialStorage(instance: secureStorage);
|
_secureCredentialStorage = SecureCredentialStorage(
|
||||||
|
instance: secureStorage,
|
||||||
|
);
|
||||||
|
|
||||||
// Optimized key names with versioning
|
// Optimized key names with versioning
|
||||||
static const String _authTokenKey = 'auth_token_v3';
|
static const String _authTokenKey = 'auth_token_v3';
|
||||||
|
|||||||
@@ -74,7 +74,9 @@ class PersistentStreamingService with WidgetsBindingObserver {
|
|||||||
|
|
||||||
_connectivitySubscription?.cancel();
|
_connectivitySubscription?.cancel();
|
||||||
_connectivityService = service;
|
_connectivityService = service;
|
||||||
_connectivitySubscription = service.isConnected.listen(_handleConnectivityChange);
|
_connectivitySubscription = service.isConnected.listen(
|
||||||
|
_handleConnectivityChange,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _handleConnectivityChange(bool connected) {
|
void _handleConnectivityChange(bool connected) {
|
||||||
|
|||||||
@@ -336,10 +336,12 @@ class PlatformService {
|
|||||||
// on Android 15+. Only control icon brightness; colors come from theme + EdgeToEdge.
|
// on Android 15+. Only control icon brightness; colors come from theme + EdgeToEdge.
|
||||||
SystemChrome.setSystemUIOverlayStyle(
|
SystemChrome.setSystemUIOverlayStyle(
|
||||||
SystemUiOverlayStyle(
|
SystemUiOverlayStyle(
|
||||||
statusBarIconBrightness:
|
statusBarIconBrightness: isDarkContent
|
||||||
isDarkContent ? Brightness.dark : Brightness.light,
|
? Brightness.dark
|
||||||
systemNavigationBarIconBrightness:
|
: Brightness.light,
|
||||||
isDarkContent ? Brightness.dark : Brightness.light,
|
systemNavigationBarIconBrightness: isDarkContent
|
||||||
|
? Brightness.dark
|
||||||
|
: Brightness.light,
|
||||||
// Do NOT set status/navigation bar colors on Android.
|
// Do NOT set status/navigation bar colors on Android.
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -9,10 +9,12 @@ class SecureCredentialStorage {
|
|||||||
late final FlutterSecureStorage _secureStorage;
|
late final FlutterSecureStorage _secureStorage;
|
||||||
|
|
||||||
SecureCredentialStorage({FlutterSecureStorage? instance}) {
|
SecureCredentialStorage({FlutterSecureStorage? instance}) {
|
||||||
_secureStorage = instance ?? FlutterSecureStorage(
|
_secureStorage =
|
||||||
aOptions: _getAndroidOptions(),
|
instance ??
|
||||||
iOptions: _getIOSOptions(),
|
FlutterSecureStorage(
|
||||||
);
|
aOptions: _getAndroidOptions(),
|
||||||
|
iOptions: _getIOSOptions(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
static const String _credentialsKey = 'user_credentials_v2';
|
static const String _credentialsKey = 'user_credentials_v2';
|
||||||
|
|||||||
@@ -16,7 +16,9 @@ class StorageService {
|
|||||||
required SharedPreferences prefs,
|
required SharedPreferences prefs,
|
||||||
}) : _secureStorage = secureStorage,
|
}) : _secureStorage = secureStorage,
|
||||||
_prefs = prefs,
|
_prefs = prefs,
|
||||||
_secureCredentialStorage = SecureCredentialStorage(instance: secureStorage);
|
_secureCredentialStorage = SecureCredentialStorage(
|
||||||
|
instance: secureStorage,
|
||||||
|
);
|
||||||
|
|
||||||
// Secure storage keys
|
// Secure storage keys
|
||||||
static const String _authTokenKey = 'auth_token';
|
static const String _authTokenKey = 'auth_token';
|
||||||
|
|||||||
@@ -26,4 +26,4 @@ final toolsServiceProvider = Provider<ToolsService?>((ref) {
|
|||||||
final apiService = ref.watch(apiServiceProvider);
|
final apiService = ref.watch(apiServiceProvider);
|
||||||
if (apiService == null) return null;
|
if (apiService == null) return null;
|
||||||
return ToolsService(apiService);
|
return ToolsService(apiService);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -10,8 +10,8 @@ class InactivityWatchdog {
|
|||||||
required Duration window,
|
required Duration window,
|
||||||
required this.onTimeout,
|
required this.onTimeout,
|
||||||
Duration? absoluteCap,
|
Duration? absoluteCap,
|
||||||
}) : _window = window,
|
}) : _window = window,
|
||||||
_absoluteCap = absoluteCap;
|
_absoluteCap = absoluteCap;
|
||||||
|
|
||||||
final void Function() onTimeout;
|
final void Function() onTimeout;
|
||||||
|
|
||||||
@@ -80,4 +80,3 @@ class InactivityWatchdog {
|
|||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -44,35 +44,36 @@ class ReviewerModeService {
|
|||||||
bool isVoiceInput = false,
|
bool isVoiceInput = false,
|
||||||
}) {
|
}) {
|
||||||
final lowerMessage = userMessage.toLowerCase();
|
final lowerMessage = userMessage.toLowerCase();
|
||||||
|
|
||||||
// Determine response category
|
// Determine response category
|
||||||
String category = 'general';
|
String category = 'general';
|
||||||
|
|
||||||
if (lowerMessage.contains('hello') ||
|
if (lowerMessage.contains('hello') ||
|
||||||
lowerMessage.contains('hi') ||
|
lowerMessage.contains('hi') ||
|
||||||
lowerMessage.contains('hey') ||
|
lowerMessage.contains('hey') ||
|
||||||
lowerMessage.contains('greet')) {
|
lowerMessage.contains('greet')) {
|
||||||
category = 'greeting';
|
category = 'greeting';
|
||||||
} else if (lowerMessage.contains('code') ||
|
} else if (lowerMessage.contains('code') ||
|
||||||
lowerMessage.contains('program') ||
|
lowerMessage.contains('program') ||
|
||||||
lowerMessage.contains('function') ||
|
lowerMessage.contains('function') ||
|
||||||
lowerMessage.contains('debug')) {
|
lowerMessage.contains('debug')) {
|
||||||
category = 'code';
|
category = 'code';
|
||||||
} else if (lowerMessage.contains('feature') ||
|
} else if (lowerMessage.contains('feature') ||
|
||||||
lowerMessage.contains('capability') ||
|
lowerMessage.contains('capability') ||
|
||||||
lowerMessage.contains('what can') ||
|
lowerMessage.contains('what can') ||
|
||||||
lowerMessage.contains('help')) {
|
lowerMessage.contains('help')) {
|
||||||
category = 'features';
|
category = 'features';
|
||||||
} else if (filename != null) {
|
} else if (filename != null) {
|
||||||
category = 'attachments';
|
category = 'attachments';
|
||||||
} else if (isVoiceInput) {
|
} else if (isVoiceInput) {
|
||||||
category = 'voice';
|
category = 'voice';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get responses for category
|
// Get responses for category
|
||||||
final responses = _cannedResponses[category] ?? _cannedResponses['general']!;
|
final responses =
|
||||||
|
_cannedResponses[category] ?? _cannedResponses['general']!;
|
||||||
final response = responses[_random.nextInt(responses.length)];
|
final response = responses[_random.nextInt(responses.length)];
|
||||||
|
|
||||||
// Replace placeholders
|
// Replace placeholders
|
||||||
return response
|
return response
|
||||||
.replaceAll('{query}', userMessage)
|
.replaceAll('{query}', userMessage)
|
||||||
@@ -92,4 +93,4 @@ class ReviewerModeService {
|
|||||||
isVoiceInput: isVoiceInput,
|
isVoiceInput: isVoiceInput,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -65,4 +65,3 @@ class _PressableScaleState extends State<PressableScale>
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -229,7 +229,7 @@ class BrandService {
|
|||||||
return AppBar(
|
return AppBar(
|
||||||
title: Text(
|
title: Text(
|
||||||
title,
|
title,
|
||||||
style: context != null
|
style: context != null
|
||||||
? context.conduitTheme.headingSmall?.copyWith(
|
? context.conduitTheme.headingSmall?.copyWith(
|
||||||
color: context.conduitTheme.textPrimary,
|
color: context.conduitTheme.textPrimary,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
|
|||||||
@@ -3,13 +3,7 @@ import 'package:freezed_annotation/freezed_annotation.dart';
|
|||||||
part 'outbound_task.freezed.dart';
|
part 'outbound_task.freezed.dart';
|
||||||
part 'outbound_task.g.dart';
|
part 'outbound_task.g.dart';
|
||||||
|
|
||||||
enum TaskStatus {
|
enum TaskStatus { queued, running, succeeded, failed, cancelled }
|
||||||
queued,
|
|
||||||
running,
|
|
||||||
succeeded,
|
|
||||||
failed,
|
|
||||||
cancelled,
|
|
||||||
}
|
|
||||||
|
|
||||||
@freezed
|
@freezed
|
||||||
abstract class OutboundTask with _$OutboundTask {
|
abstract class OutboundTask with _$OutboundTask {
|
||||||
@@ -74,7 +68,6 @@ abstract class OutboundTask with _$OutboundTask {
|
|||||||
String? error,
|
String? error,
|
||||||
}) = GenerateImageTask;
|
}) = GenerateImageTask;
|
||||||
|
|
||||||
|
|
||||||
const factory OutboundTask.generateTitle({
|
const factory OutboundTask.generateTitle({
|
||||||
required String id,
|
required String id,
|
||||||
required String conversationId,
|
required String conversationId,
|
||||||
@@ -106,16 +99,16 @@ abstract class OutboundTask with _$OutboundTask {
|
|||||||
|
|
||||||
// Provide a unified nullable conversationId across variants
|
// Provide a unified nullable conversationId across variants
|
||||||
String? get maybeConversationId => map(
|
String? get maybeConversationId => map(
|
||||||
sendTextMessage: (t) => t.conversationId,
|
sendTextMessage: (t) => t.conversationId,
|
||||||
uploadMedia: (t) => t.conversationId,
|
uploadMedia: (t) => t.conversationId,
|
||||||
executeToolCall: (t) => t.conversationId,
|
executeToolCall: (t) => t.conversationId,
|
||||||
generateImage: (t) => t.conversationId,
|
generateImage: (t) => t.conversationId,
|
||||||
generateTitle: (t) => t.conversationId,
|
generateTitle: (t) => t.conversationId,
|
||||||
imageToDataUrl: (t) => t.conversationId,
|
imageToDataUrl: (t) => t.conversationId,
|
||||||
);
|
);
|
||||||
|
|
||||||
String get threadKey =>
|
String get threadKey =>
|
||||||
(maybeConversationId == null || maybeConversationId!.isEmpty)
|
(maybeConversationId == null || maybeConversationId!.isEmpty)
|
||||||
? 'new'
|
? 'new'
|
||||||
: maybeConversationId!;
|
: maybeConversationId!;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -751,10 +751,6 @@ class ConduitThemeExtension extends ThemeExtension<ConduitThemeExtension> {
|
|||||||
fontFamily: AppTypography.monospaceFontFamily,
|
fontFamily: AppTypography.monospaceFontFamily,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Extension method to easily access Conduit theme from BuildContext
|
/// Extension method to easily access Conduit theme from BuildContext
|
||||||
|
|||||||
@@ -31,7 +31,8 @@ class _ChatActionButtonState extends ConsumerState<ChatActionButton> {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final theme = context.conduitTheme;
|
final theme = context.conduitTheme;
|
||||||
final hapticEnabled = ref.read(hapticEnabledProvider);
|
final hapticEnabled = ref.read(hapticEnabledProvider);
|
||||||
final radius = widget.borderRadius ?? BorderRadius.circular(AppBorderRadius.lg);
|
final radius =
|
||||||
|
widget.borderRadius ?? BorderRadius.circular(AppBorderRadius.lg);
|
||||||
final overlay = theme.buttonPrimary.withValues(alpha: 0.08);
|
final overlay = theme.buttonPrimary.withValues(alpha: 0.08);
|
||||||
|
|
||||||
return Tooltip(
|
return Tooltip(
|
||||||
@@ -100,4 +101,3 @@ class _ChatActionButtonState extends ConsumerState<ChatActionButton> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -101,7 +101,8 @@ class ConduitButton extends ConsumerWidget {
|
|||||||
),
|
),
|
||||||
child: isLoading
|
child: isLoading
|
||||||
? Semantics(
|
? Semantics(
|
||||||
label: AppLocalizations.of(context)?.loadingContent ?? 'Loading',
|
label:
|
||||||
|
AppLocalizations.of(context)?.loadingContent ?? 'Loading',
|
||||||
excludeSemantics: true,
|
excludeSemantics: true,
|
||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
width: IconSize.small,
|
width: IconSize.small,
|
||||||
@@ -206,7 +207,10 @@ class ConduitInput extends StatelessWidget {
|
|||||||
SizedBox(height: Spacing.sm),
|
SizedBox(height: Spacing.sm),
|
||||||
],
|
],
|
||||||
Semantics(
|
Semantics(
|
||||||
label: semanticLabel ?? label ?? (AppLocalizations.of(context)?.inputField ?? 'Input field'),
|
label:
|
||||||
|
semanticLabel ??
|
||||||
|
label ??
|
||||||
|
(AppLocalizations.of(context)?.inputField ?? 'Input field'),
|
||||||
textField: true,
|
textField: true,
|
||||||
child: TextField(
|
child: TextField(
|
||||||
controller: controller,
|
controller: controller,
|
||||||
@@ -784,7 +788,10 @@ class AccessibleFormField extends StatelessWidget {
|
|||||||
SizedBox(height: isCompact ? Spacing.xs : Spacing.sm),
|
SizedBox(height: isCompact ? Spacing.xs : Spacing.sm),
|
||||||
],
|
],
|
||||||
Semantics(
|
Semantics(
|
||||||
label: semanticLabel ?? label ?? (AppLocalizations.of(context)?.inputField ?? 'Input field'),
|
label:
|
||||||
|
semanticLabel ??
|
||||||
|
label ??
|
||||||
|
(AppLocalizations.of(context)?.inputField ?? 'Input field'),
|
||||||
textField: true,
|
textField: true,
|
||||||
child: TextFormField(
|
child: TextFormField(
|
||||||
controller: controller,
|
controller: controller,
|
||||||
|
|||||||
@@ -348,8 +348,13 @@ class LoadingStateWrapper<T> extends StatelessWidget {
|
|||||||
return asyncValue.when(
|
return asyncValue.when(
|
||||||
data: builder,
|
data: builder,
|
||||||
loading: () => showLoadingOverlay
|
loading: () => showLoadingOverlay
|
||||||
? ConduitLoading.overlay(message: AppLocalizations.of(context)!.loadingContent)
|
? ConduitLoading.overlay(
|
||||||
: loadingWidget ?? ConduitLoading.primary(message: AppLocalizations.of(context)!.loadingContent),
|
message: AppLocalizations.of(context)!.loadingContent,
|
||||||
|
)
|
||||||
|
: loadingWidget ??
|
||||||
|
ConduitLoading.primary(
|
||||||
|
message: AppLocalizations.of(context)!.loadingContent,
|
||||||
|
),
|
||||||
error: (error, stackTrace) {
|
error: (error, stackTrace) {
|
||||||
if (errorBuilder != null) {
|
if (errorBuilder != null) {
|
||||||
return errorBuilder!(error, stackTrace);
|
return errorBuilder!(error, stackTrace);
|
||||||
|
|||||||
@@ -155,7 +155,11 @@ class ConduitMarkdownConfig {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
static Widget _buildBase64Image(String dataUrl, BuildContext context, ConduitThemeExtension theme) {
|
static Widget _buildBase64Image(
|
||||||
|
String dataUrl,
|
||||||
|
BuildContext context,
|
||||||
|
ConduitThemeExtension theme,
|
||||||
|
) {
|
||||||
try {
|
try {
|
||||||
// Extract base64 part from data URL
|
// Extract base64 part from data URL
|
||||||
final commaIndex = dataUrl.indexOf(',');
|
final commaIndex = dataUrl.indexOf(',');
|
||||||
|
|||||||
@@ -20,7 +20,8 @@ class StreamingMarkdownWidget extends StatefulWidget {
|
|||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<StreamingMarkdownWidget> createState() => _StreamingMarkdownWidgetState();
|
State<StreamingMarkdownWidget> createState() =>
|
||||||
|
_StreamingMarkdownWidgetState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _StreamingMarkdownWidgetState extends State<StreamingMarkdownWidget> {
|
class _StreamingMarkdownWidgetState extends State<StreamingMarkdownWidget> {
|
||||||
@@ -59,38 +60,38 @@ class _StreamingMarkdownWidgetState extends State<StreamingMarkdownWidget> {
|
|||||||
if (fenceCount % 2 != 0) {
|
if (fenceCount % 2 != 0) {
|
||||||
content += '\n```';
|
content += '\n```';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fix incomplete bold/italic markers
|
// Fix incomplete bold/italic markers
|
||||||
final boldCount = RegExp(r'\*\*').allMatches(content).length;
|
final boldCount = RegExp(r'\*\*').allMatches(content).length;
|
||||||
if (boldCount % 2 != 0) {
|
if (boldCount % 2 != 0) {
|
||||||
content += '**';
|
content += '**';
|
||||||
}
|
}
|
||||||
|
|
||||||
final italicCount = RegExp(r'(?<!\*)\*(?!\*)').allMatches(content).length;
|
final italicCount = RegExp(r'(?<!\*)\*(?!\*)').allMatches(content).length;
|
||||||
if (italicCount % 2 != 0) {
|
if (italicCount % 2 != 0) {
|
||||||
content += '*';
|
content += '*';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fix incomplete link brackets
|
// Fix incomplete link brackets
|
||||||
final openBrackets = '['.allMatches(content).length;
|
final openBrackets = '['.allMatches(content).length;
|
||||||
final closeBrackets = ']'.allMatches(content).length;
|
final closeBrackets = ']'.allMatches(content).length;
|
||||||
if (openBrackets > closeBrackets) {
|
if (openBrackets > closeBrackets) {
|
||||||
content += ']' * (openBrackets - closeBrackets);
|
content += ']' * (openBrackets - closeBrackets);
|
||||||
}
|
}
|
||||||
|
|
||||||
final openParens = '('.allMatches(content).length;
|
final openParens = '('.allMatches(content).length;
|
||||||
final closeParens = ')'.allMatches(content).length;
|
final closeParens = ')'.allMatches(content).length;
|
||||||
if (openParens > closeParens) {
|
if (openParens > closeParens) {
|
||||||
content += ')' * (openParens - closeParens);
|
content += ')' * (openParens - closeParens);
|
||||||
}
|
}
|
||||||
|
|
||||||
return content;
|
return content;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void didUpdateWidget(StreamingMarkdownWidget oldWidget) {
|
void didUpdateWidget(StreamingMarkdownWidget oldWidget) {
|
||||||
super.didUpdateWidget(oldWidget);
|
super.didUpdateWidget(oldWidget);
|
||||||
|
|
||||||
// Handle stream changes
|
// Handle stream changes
|
||||||
if (widget.contentStream != oldWidget.contentStream) {
|
if (widget.contentStream != oldWidget.contentStream) {
|
||||||
_streamSubscription?.cancel();
|
_streamSubscription?.cancel();
|
||||||
@@ -98,7 +99,7 @@ class _StreamingMarkdownWidgetState extends State<StreamingMarkdownWidget> {
|
|||||||
_streamSubscription = widget.contentStream!.listen(_handleChunk);
|
_streamSubscription = widget.contentStream!.listen(_handleChunk);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle static content changes
|
// Handle static content changes
|
||||||
if (widget.staticContent != oldWidget.staticContent) {
|
if (widget.staticContent != oldWidget.staticContent) {
|
||||||
setState(() {
|
setState(() {
|
||||||
@@ -182,9 +183,7 @@ class MarkdownWithLoading extends StatelessWidget {
|
|||||||
if (isLoading && (content == null || content!.isEmpty)) {
|
if (isLoading && (content == null || content!.isEmpty)) {
|
||||||
return Container(
|
return Container(
|
||||||
padding: padding ?? const EdgeInsets.all(16),
|
padding: padding ?? const EdgeInsets.all(16),
|
||||||
child: const Center(
|
child: const Center(child: CircularProgressIndicator()),
|
||||||
child: CircularProgressIndicator(),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -194,4 +193,4 @@ class MarkdownWithLoading extends StatelessWidget {
|
|||||||
padding: padding,
|
padding: padding,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,8 +22,9 @@ class MiddleEllipsisText extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return LayoutBuilder(
|
return LayoutBuilder(
|
||||||
builder: (context, constraints) {
|
builder: (context, constraints) {
|
||||||
final TextStyle effectiveStyle =
|
final TextStyle effectiveStyle = DefaultTextStyle.of(
|
||||||
DefaultTextStyle.of(context).style.merge(style);
|
context,
|
||||||
|
).style.merge(style);
|
||||||
final TextDirection direction = Directionality.of(context);
|
final TextDirection direction = Directionality.of(context);
|
||||||
final double maxWidth = constraints.maxWidth;
|
final double maxWidth = constraints.maxWidth;
|
||||||
|
|
||||||
@@ -74,8 +75,10 @@ class MiddleEllipsisText extends StatelessWidget {
|
|||||||
? ''
|
? ''
|
||||||
: text.substring(text.length - rightCount);
|
: text.substring(text.length - rightCount);
|
||||||
|
|
||||||
final trialSpan =
|
final trialSpan = TextSpan(
|
||||||
TextSpan(text: '$start$ellipsis$end', style: effectiveStyle);
|
text: '$start$ellipsis$end',
|
||||||
|
style: effectiveStyle,
|
||||||
|
);
|
||||||
final trialPainter = TextPainter(
|
final trialPainter = TextPainter(
|
||||||
text: trialSpan,
|
text: trialSpan,
|
||||||
textDirection: direction,
|
textDirection: direction,
|
||||||
@@ -116,4 +119,3 @@ class MiddleEllipsisText extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -130,7 +130,9 @@ class _OptimizedListState<T> extends ConsumerState<OptimizedList<T>> {
|
|||||||
return widget.emptyWidget ??
|
return widget.emptyWidget ??
|
||||||
ImprovedEmptyState(
|
ImprovedEmptyState(
|
||||||
title: AppLocalizations.of(context)!.noItems,
|
title: AppLocalizations.of(context)!.noItems,
|
||||||
subtitle: widget.emptyMessage ?? AppLocalizations.of(context)!.noItemsToDisplay,
|
subtitle:
|
||||||
|
widget.emptyMessage ??
|
||||||
|
AppLocalizations.of(context)!.noItemsToDisplay,
|
||||||
icon: Icons.inbox_outlined,
|
icon: Icons.inbox_outlined,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -138,7 +140,8 @@ class _OptimizedListState<T> extends ConsumerState<OptimizedList<T>> {
|
|||||||
// Build the list
|
// Build the list
|
||||||
Widget listWidget;
|
Widget listWidget;
|
||||||
|
|
||||||
final ScrollPhysics effectivePhysics = widget.physics ??
|
final ScrollPhysics effectivePhysics =
|
||||||
|
widget.physics ??
|
||||||
(widget.onRefresh != null
|
(widget.onRefresh != null
|
||||||
? const AlwaysScrollableScrollPhysics()
|
? const AlwaysScrollableScrollPhysics()
|
||||||
: const ClampingScrollPhysics());
|
: const ClampingScrollPhysics());
|
||||||
@@ -276,14 +279,16 @@ class OptimizedSliverList<T> extends ConsumerWidget {
|
|||||||
return SliverToBoxAdapter(
|
return SliverToBoxAdapter(
|
||||||
child:
|
child:
|
||||||
emptyWidget ??
|
emptyWidget ??
|
||||||
Builder(builder: (context) {
|
Builder(
|
||||||
final l10n = AppLocalizations.of(context)!;
|
builder: (context) {
|
||||||
return ImprovedEmptyState(
|
final l10n = AppLocalizations.of(context)!;
|
||||||
title: l10n.noItems,
|
return ImprovedEmptyState(
|
||||||
subtitle: emptyMessage ?? l10n.noItemsToDisplay,
|
title: l10n.noItems,
|
||||||
icon: Icons.inbox_outlined,
|
subtitle: emptyMessage ?? l10n.noItemsToDisplay,
|
||||||
);
|
icon: Icons.inbox_outlined,
|
||||||
}),
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,15 +9,18 @@ class SheetHandle extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Center(
|
return Center(
|
||||||
child: Container(
|
child: Container(
|
||||||
margin: margin ?? const EdgeInsets.only(top: Spacing.sm, bottom: Spacing.md),
|
margin:
|
||||||
|
margin ??
|
||||||
|
const EdgeInsets.only(top: Spacing.sm, bottom: Spacing.md),
|
||||||
width: 40,
|
width: 40,
|
||||||
height: 4,
|
height: 4,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: context.conduitTheme.textPrimary.withValues(alpha: Alpha.medium),
|
color: context.conduitTheme.textPrimary.withValues(
|
||||||
|
alpha: Alpha.medium,
|
||||||
|
),
|
||||||
borderRadius: BorderRadius.circular(AppBorderRadius.xs),
|
borderRadius: BorderRadius.circular(AppBorderRadius.xs),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -145,8 +145,10 @@ class ThemedDialogs {
|
|||||||
),
|
),
|
||||||
focusedBorder: OutlineInputBorder(
|
focusedBorder: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(AppBorderRadius.md),
|
borderRadius: BorderRadius.circular(AppBorderRadius.md),
|
||||||
borderSide:
|
borderSide: BorderSide(
|
||||||
BorderSide(color: theme.buttonPrimary, width: 1),
|
color: theme.buttonPrimary,
|
||||||
|
width: 1,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
contentPadding: const EdgeInsets.symmetric(
|
contentPadding: const EdgeInsets.symmetric(
|
||||||
horizontal: Spacing.md,
|
horizontal: Spacing.md,
|
||||||
@@ -155,8 +157,8 @@ class ThemedDialogs {
|
|||||||
),
|
),
|
||||||
onSubmitted: (v) {
|
onSubmitted: (v) {
|
||||||
final trimmed = v.trim();
|
final trimmed = v.trim();
|
||||||
final unchanged = (initialValue != null &&
|
final unchanged =
|
||||||
trimmed == initialValue.trim());
|
(initialValue != null && trimmed == initialValue.trim());
|
||||||
if (trimmed.isEmpty || unchanged) return;
|
if (trimmed.isEmpty || unchanged) return;
|
||||||
Navigator.of(ctx).pop(trimmed);
|
Navigator.of(ctx).pop(trimmed);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -56,12 +56,16 @@ Future<void> main(List<String> args) async {
|
|||||||
if (trPh == null) {
|
if (trPh == null) {
|
||||||
// If string exists but no meta placeholders, warn only.
|
// If string exists but no meta placeholders, warn only.
|
||||||
if (keys.contains(k) && basePh.isNotEmpty) {
|
if (keys.contains(k) && basePh.isNotEmpty) {
|
||||||
warnings.add('[${f.path}] Key "$k" missing @meta placeholders; base has ${basePh.toList()..sort()}');
|
warnings.add(
|
||||||
|
'[${f.path}] Key "$k" missing @meta placeholders; base has ${basePh.toList()..sort()}',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (basePh.length != trPh.length || !basePh.containsAll(trPh)) {
|
if (basePh.length != trPh.length || !basePh.containsAll(trPh)) {
|
||||||
warnings.add('[${f.path}] Placeholder mismatch for "$k": expected ${basePh.toList()..sort()}, got ${trPh.toList()..sort()}');
|
warnings.add(
|
||||||
|
'[${f.path}] Placeholder mismatch for "$k": expected ${basePh.toList()..sort()}, got ${trPh.toList()..sort()}',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -96,9 +100,7 @@ Map<String, dynamic> _readJson(File f) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Set<String> _nonMetaKeys(Map<String, dynamic> m) {
|
Set<String> _nonMetaKeys(Map<String, dynamic> m) {
|
||||||
return m.keys
|
return m.keys.where((k) => !k.startsWith('@') && k != '@@locale').toSet();
|
||||||
.where((k) => !k.startsWith('@') && k != '@@locale')
|
|
||||||
.toSet();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Map<String, Set<String>> _placeholdersMap(Map<String, dynamic> m) {
|
Map<String, Set<String>> _placeholdersMap(Map<String, dynamic> m) {
|
||||||
|
|||||||
@@ -42,7 +42,9 @@ Future<void> main() async {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (missingMeta.isEmpty && missingDescription.isEmpty) {
|
if (missingMeta.isEmpty && missingDescription.isEmpty) {
|
||||||
stdout.writeln('ARB descriptions check passed: all keys have @meta.description.');
|
stdout.writeln(
|
||||||
|
'ARB descriptions check passed: all keys have @meta.description.',
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,11 +55,12 @@ Future<void> main() async {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (missingDescription.isNotEmpty) {
|
if (missingDescription.isNotEmpty) {
|
||||||
stderr.writeln('Missing description in @meta for keys (${missingDescription.length}):');
|
stderr.writeln(
|
||||||
|
'Missing description in @meta for keys (${missingDescription.length}):',
|
||||||
|
);
|
||||||
for (final k in missingDescription) {
|
for (final k in missingDescription) {
|
||||||
stderr.writeln(' - $k');
|
stderr.writeln(' - $k');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
exit(1);
|
exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user