refactor: unused files
This commit is contained in:
@@ -1,739 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../../shared/theme/app_theme.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_animate/flutter_animate.dart';
|
||||
import 'dart:io' show Platform;
|
||||
import '../../../shared/theme/theme_extensions.dart';
|
||||
import '../../../shared/widgets/loading_states.dart';
|
||||
import '../../../shared/widgets/empty_states.dart';
|
||||
|
||||
import '../../../shared/utils/platform_utils.dart';
|
||||
import '../services/conversation_search_service.dart';
|
||||
import '../../../core/providers/app_providers.dart';
|
||||
import '../../../core/utils/debug_logger.dart';
|
||||
|
||||
/// Advanced conversation search widget with filters and results
|
||||
class ConversationSearchWidget extends ConsumerStatefulWidget {
|
||||
final Function(String conversationId, String? messageId)? onResultTap;
|
||||
final bool showFilters;
|
||||
|
||||
const ConversationSearchWidget({
|
||||
super.key,
|
||||
this.onResultTap,
|
||||
this.showFilters = true,
|
||||
});
|
||||
|
||||
@override
|
||||
ConsumerState<ConversationSearchWidget> createState() =>
|
||||
_ConversationSearchWidgetState();
|
||||
}
|
||||
|
||||
class _ConversationSearchWidgetState
|
||||
extends ConsumerState<ConversationSearchWidget> {
|
||||
final TextEditingController _searchController = TextEditingController();
|
||||
final FocusNode _searchFocus = FocusNode();
|
||||
bool _isSearching = false;
|
||||
bool _showFilters = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_searchController.addListener(_onSearchChanged);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_searchController.removeListener(_onSearchChanged);
|
||||
_searchController.dispose();
|
||||
_searchFocus.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _onSearchChanged() {
|
||||
final query = _searchController.text.trim();
|
||||
ref.read(searchQueryProvider.notifier).state = query;
|
||||
|
||||
if (query.isNotEmpty) {
|
||||
_performSearch(query);
|
||||
} else {
|
||||
ref.read(conversationSearchResultsProvider.notifier).state = null;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _performSearch(String query) async {
|
||||
if (_isSearching) return;
|
||||
|
||||
setState(() {
|
||||
_isSearching = true;
|
||||
});
|
||||
|
||||
try {
|
||||
final searchService = ref.read(conversationSearchServiceProvider);
|
||||
final conversations = ref
|
||||
.read(conversationsProvider)
|
||||
.when(
|
||||
data: (data) => data,
|
||||
loading: () => <dynamic>[],
|
||||
error: (_, _) => <dynamic>[],
|
||||
);
|
||||
|
||||
final options = ref.read(searchOptionsProvider);
|
||||
|
||||
final results = await searchService.searchConversations(
|
||||
conversations: conversations.cast(),
|
||||
query: query,
|
||||
options: options,
|
||||
);
|
||||
|
||||
ref.read(conversationSearchResultsProvider.notifier).state = results;
|
||||
} catch (e) {
|
||||
DebugLogger.error('Search error', e);
|
||||
} finally {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isSearching = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final conduitTheme = context.conduitTheme;
|
||||
final searchResults = ref.watch(conversationSearchResultsProvider);
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
// Search header
|
||||
Container(
|
||||
padding: const EdgeInsets.all(Spacing.md),
|
||||
decoration: BoxDecoration(
|
||||
color: conduitTheme.cardBackground,
|
||||
border: Border(
|
||||
bottom: BorderSide(
|
||||
color: conduitTheme.cardBorder,
|
||||
width: BorderWidth.regular,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
// Search input
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: conduitTheme.inputBackground,
|
||||
borderRadius: BorderRadius.circular(AppBorderRadius.md),
|
||||
border: Border.all(
|
||||
color: _searchFocus.hasFocus
|
||||
? conduitTheme.inputBorderFocused
|
||||
: conduitTheme.inputBorder,
|
||||
width: BorderWidth.regular,
|
||||
),
|
||||
),
|
||||
child: TextField(
|
||||
controller: _searchController,
|
||||
focusNode: _searchFocus,
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Search conversations...',
|
||||
hintStyle: TextStyle(
|
||||
color: context.conduitTheme.inputPlaceholder,
|
||||
fontSize: AppTypography.bodyLarge,
|
||||
),
|
||||
prefixIcon: Icon(
|
||||
Platform.isIOS
|
||||
? CupertinoIcons.search
|
||||
: Icons.search,
|
||||
color: context.conduitTheme.iconSecondary,
|
||||
size: AppTypography.headlineMedium,
|
||||
),
|
||||
suffixIcon: _isSearching
|
||||
? Padding(
|
||||
padding: const EdgeInsets.all(Spacing.md),
|
||||
child: ConduitLoading.inline(
|
||||
size: Spacing.md,
|
||||
),
|
||||
)
|
||||
: _searchController.text.isNotEmpty
|
||||
? IconButton(
|
||||
icon: Icon(
|
||||
Platform.isIOS
|
||||
? CupertinoIcons.clear
|
||||
: Icons.clear,
|
||||
color: context.conduitTheme.iconSecondary,
|
||||
size: AppTypography.headlineMedium,
|
||||
),
|
||||
onPressed: () {
|
||||
_searchController.clear();
|
||||
_searchFocus.unfocus();
|
||||
},
|
||||
)
|
||||
: null,
|
||||
border: InputBorder.none,
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: Spacing.md,
|
||||
vertical: Spacing.xs,
|
||||
),
|
||||
),
|
||||
style: TextStyle(
|
||||
color: context.conduitTheme.inputText,
|
||||
fontSize: AppTypography.bodyLarge,
|
||||
),
|
||||
onSubmitted: (_) => _searchFocus.unfocus(),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Filter toggle
|
||||
if (widget.showFilters) ...[
|
||||
const SizedBox(width: Spacing.xs),
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
PlatformUtils.lightHaptic();
|
||||
setState(() {
|
||||
_showFilters = !_showFilters;
|
||||
});
|
||||
},
|
||||
child: Container(
|
||||
width: Spacing.xxl + Spacing.xs,
|
||||
height: Spacing.xxl + Spacing.xs,
|
||||
decoration: BoxDecoration(
|
||||
color: _showFilters
|
||||
? AppTheme.neutral50.withValues(alpha: 0.2)
|
||||
: Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(
|
||||
AppBorderRadius.md,
|
||||
),
|
||||
border: Border.all(
|
||||
color: _showFilters
|
||||
? AppTheme.neutral50.withValues(alpha: 0.3)
|
||||
: conduitTheme.inputBorder,
|
||||
width: BorderWidth.regular,
|
||||
),
|
||||
),
|
||||
child: Icon(
|
||||
Platform.isIOS
|
||||
? CupertinoIcons.slider_horizontal_3
|
||||
: Icons.tune,
|
||||
color: AppTheme.neutral50.withValues(alpha: 0.8),
|
||||
size: AppTypography.headlineMedium,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
|
||||
// Search filters
|
||||
if (_showFilters && widget.showFilters)
|
||||
_buildSearchFilters(conduitTheme),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Search results
|
||||
Expanded(child: _buildSearchResults(conduitTheme, searchResults)),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSearchFilters(ConduitThemeExtension theme) {
|
||||
final options = ref.watch(searchOptionsProvider);
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(top: Spacing.md),
|
||||
padding: const EdgeInsets.all(Spacing.md),
|
||||
decoration: BoxDecoration(
|
||||
color: AppTheme.neutral50.withValues(alpha: 0.05),
|
||||
borderRadius: BorderRadius.circular(AppBorderRadius.md),
|
||||
border: Border.all(color: theme.cardBorder, width: BorderWidth.regular),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Search in:',
|
||||
style: theme.bodySmall?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppTheme.neutral50.withValues(alpha: 0.8),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: Spacing.xs),
|
||||
|
||||
// Search scope toggles
|
||||
Wrap(
|
||||
spacing: Spacing.md,
|
||||
runSpacing: Spacing.sm,
|
||||
children: [
|
||||
_buildFilterToggle(
|
||||
'Titles',
|
||||
options.searchTitles,
|
||||
(value) =>
|
||||
_updateSearchOptions(options.copyWith(searchTitles: value)),
|
||||
),
|
||||
_buildFilterToggle(
|
||||
'Messages',
|
||||
options.searchMessages,
|
||||
(value) => _updateSearchOptions(
|
||||
options.copyWith(searchMessages: value),
|
||||
),
|
||||
),
|
||||
_buildFilterToggle(
|
||||
'Tags',
|
||||
options.searchTags,
|
||||
(value) =>
|
||||
_updateSearchOptions(options.copyWith(searchTags: value)),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: Spacing.md),
|
||||
|
||||
Text(
|
||||
'Message type:',
|
||||
style: theme.bodySmall?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppTheme.neutral50.withValues(alpha: 0.8),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: Spacing.xs),
|
||||
|
||||
// Role filter
|
||||
Wrap(
|
||||
spacing: Spacing.md,
|
||||
runSpacing: Spacing.sm,
|
||||
children: [
|
||||
_buildFilterChip(
|
||||
'All',
|
||||
options.roleFilter == null,
|
||||
() => _updateSearchOptions(options.copyWith(roleFilter: null)),
|
||||
),
|
||||
_buildFilterChip(
|
||||
'My messages',
|
||||
options.roleFilter == 'user',
|
||||
() =>
|
||||
_updateSearchOptions(options.copyWith(roleFilter: 'user')),
|
||||
),
|
||||
_buildFilterChip(
|
||||
'AI messages',
|
||||
options.roleFilter == 'assistant',
|
||||
() => _updateSearchOptions(
|
||||
options.copyWith(roleFilter: 'assistant'),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
).animate().slideY(
|
||||
begin: -0.5,
|
||||
end: 0,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
curve: Curves.easeOut,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildFilterToggle(
|
||||
String label,
|
||||
bool value,
|
||||
Function(bool) onChanged,
|
||||
) {
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
PlatformUtils.selectionHaptic();
|
||||
onChanged(!value);
|
||||
},
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
width: AppTypography.headlineMedium,
|
||||
height: AppTypography.headlineMedium,
|
||||
decoration: BoxDecoration(
|
||||
color: value ? AppTheme.brandPrimary : Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(AppBorderRadius.xs),
|
||||
border: Border.all(
|
||||
color: value
|
||||
? AppTheme.brandPrimary
|
||||
: AppTheme.neutral50.withValues(alpha: 0.3),
|
||||
width: BorderWidth.regular,
|
||||
),
|
||||
),
|
||||
child: value
|
||||
? const Icon(
|
||||
Icons.check,
|
||||
color: AppTheme.neutral50,
|
||||
size: AppTypography.labelLarge,
|
||||
)
|
||||
: null,
|
||||
),
|
||||
const SizedBox(width: Spacing.sm),
|
||||
Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
color: AppTheme.neutral50.withValues(alpha: 0.8),
|
||||
fontSize: AppTypography.labelLarge,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildFilterChip(String label, bool isActive, VoidCallback onTap) {
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
PlatformUtils.selectionHaptic();
|
||||
onTap();
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: Spacing.xs,
|
||||
vertical: Spacing.xs + Spacing.xxs,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: isActive
|
||||
? AppTheme.brandPrimary.withValues(alpha: 0.2)
|
||||
: Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(AppBorderRadius.lg),
|
||||
border: Border.all(
|
||||
color: isActive
|
||||
? AppTheme.brandPrimary
|
||||
: AppTheme.neutral50.withValues(alpha: 0.3),
|
||||
width: BorderWidth.regular,
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
color: isActive
|
||||
? AppTheme.brandPrimary
|
||||
: AppTheme.neutral50.withValues(alpha: 0.8),
|
||||
fontSize: AppTypography.labelMedium,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _updateSearchOptions(ConversationSearchOptions newOptions) {
|
||||
ref.read(searchOptionsProvider.notifier).state = newOptions;
|
||||
|
||||
// Re-search with new options if we have a query
|
||||
final query = _searchController.text.trim();
|
||||
if (query.isNotEmpty) {
|
||||
_performSearch(query);
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildSearchResults(
|
||||
ConduitThemeExtension theme,
|
||||
ConversationSearchResults? results,
|
||||
) {
|
||||
if (_searchController.text.trim().isEmpty) {
|
||||
return _buildSearchPrompt(theme);
|
||||
}
|
||||
|
||||
if (results == null) {
|
||||
return Center(child: ConduitLoading.primary());
|
||||
}
|
||||
|
||||
if (results.isEmpty) {
|
||||
return SearchEmptyState(
|
||||
query: results.query,
|
||||
onClearSearch: () {
|
||||
_searchController.clear();
|
||||
_searchFocus.unfocus();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
// Results header
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: Spacing.md,
|
||||
vertical: Spacing.xs,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: AppTheme.neutral50.withValues(alpha: 0.05),
|
||||
border: Border(
|
||||
bottom: BorderSide(
|
||||
color: theme.cardBorder,
|
||||
width: BorderWidth.regular,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Text(
|
||||
'${results.length} of ${results.totalMatches} results',
|
||||
style: theme.bodySmall?.copyWith(
|
||||
color: AppTheme.neutral50.withValues(alpha: 0.7),
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
Text(
|
||||
'${results.searchDuration.inMilliseconds}ms',
|
||||
style: theme.bodySmall?.copyWith(
|
||||
color: AppTheme.neutral50.withValues(alpha: 0.5),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Results list
|
||||
Expanded(
|
||||
child: ListView.builder(
|
||||
itemCount: results.length,
|
||||
itemBuilder: (context, index) {
|
||||
final match = results.results[index];
|
||||
return _buildSearchResultItem(theme, match, index);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSearchPrompt(ConduitThemeExtension theme) {
|
||||
return ConduitEmptyState(
|
||||
title: 'Search your conversations',
|
||||
subtitle: 'Find messages, titles, and tags across all your conversations',
|
||||
icon: Platform.isIOS ? CupertinoIcons.search : Icons.search,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSearchResultItem(
|
||||
ConduitThemeExtension theme,
|
||||
ConversationSearchMatch match,
|
||||
int index,
|
||||
) {
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
PlatformUtils.lightHaptic();
|
||||
widget.onResultTap?.call(match.conversationId, match.messageId);
|
||||
},
|
||||
child: Container(
|
||||
margin: const EdgeInsets.symmetric(
|
||||
horizontal: Spacing.md,
|
||||
vertical: Spacing.xs,
|
||||
),
|
||||
padding: const EdgeInsets.all(Spacing.md),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.cardBackground,
|
||||
borderRadius: BorderRadius.circular(AppBorderRadius.md),
|
||||
border: Border.all(
|
||||
color: theme.cardBorder,
|
||||
width: BorderWidth.regular,
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Header with conversation title and match type
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
match.conversationTitle,
|
||||
style: theme.headingSmall?.copyWith(
|
||||
fontSize: AppTypography.bodyLarge,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: Spacing.sm),
|
||||
_buildMatchTypeBadge(match.matchType),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: Spacing.sm),
|
||||
|
||||
// Snippet with highlighted text
|
||||
_buildHighlightedSnippet(theme, match.highlightedSnippet),
|
||||
|
||||
const SizedBox(height: Spacing.sm),
|
||||
|
||||
// Footer with metadata
|
||||
Row(
|
||||
children: [
|
||||
if (match.messageRole != null) ...[
|
||||
_buildRoleBadge(match.messageRole!),
|
||||
const SizedBox(width: Spacing.sm),
|
||||
],
|
||||
Text(
|
||||
_formatTimestamp(match.timestamp),
|
||||
style: theme.caption,
|
||||
),
|
||||
const Spacer(),
|
||||
Text(
|
||||
'${match.relevanceScore.round()}% match',
|
||||
style: theme.caption?.copyWith(
|
||||
color: AppTheme.brandPrimary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
.animate(delay: Duration(milliseconds: index * 50))
|
||||
.fadeIn(duration: const Duration(milliseconds: 200))
|
||||
.slideX(begin: 0.3, end: 0);
|
||||
}
|
||||
|
||||
Widget _buildMatchTypeBadge(SearchMatchType type) {
|
||||
Color color;
|
||||
String label;
|
||||
|
||||
switch (type) {
|
||||
case SearchMatchType.title:
|
||||
color = AppTheme.info;
|
||||
label = 'Title';
|
||||
break;
|
||||
case SearchMatchType.message:
|
||||
color = AppTheme.success;
|
||||
label = 'Message';
|
||||
break;
|
||||
case SearchMatchType.tag:
|
||||
color = AppTheme.warning;
|
||||
label = 'Tag';
|
||||
break;
|
||||
}
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: Spacing.sm,
|
||||
vertical: Spacing.xxs,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: color.withValues(alpha: 0.2),
|
||||
borderRadius: BorderRadius.circular(AppBorderRadius.md),
|
||||
),
|
||||
child: Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
color: color,
|
||||
fontSize: AppTypography.labelSmall,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildRoleBadge(String role) {
|
||||
Color color;
|
||||
String label;
|
||||
|
||||
switch (role) {
|
||||
case 'user':
|
||||
color = AppTheme.brandPrimary;
|
||||
label = 'You';
|
||||
break;
|
||||
case 'assistant':
|
||||
color = AppTheme.success;
|
||||
label = 'AI';
|
||||
break;
|
||||
case 'system':
|
||||
color = AppTheme.warning;
|
||||
label = 'System';
|
||||
break;
|
||||
default:
|
||||
color = AppTheme.neutral400;
|
||||
label = role;
|
||||
}
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: Spacing.xs + Spacing.xxs,
|
||||
vertical: Spacing.xxs,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: color.withValues(alpha: 0.2),
|
||||
borderRadius: BorderRadius.circular(AppBorderRadius.sm),
|
||||
),
|
||||
child: Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
color: color,
|
||||
fontSize: AppTypography.labelSmall,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHighlightedSnippet(
|
||||
ConduitThemeExtension theme,
|
||||
String highlightedText,
|
||||
) {
|
||||
// Simple implementation - in a real app you'd want proper HTML parsing
|
||||
final parts = highlightedText.split('<mark>');
|
||||
final spans = <InlineSpan>[];
|
||||
|
||||
for (int i = 0; i < parts.length; i++) {
|
||||
final part = parts[i];
|
||||
if (i == 0) {
|
||||
spans.add(TextSpan(text: part));
|
||||
} else {
|
||||
final markParts = part.split('</mark>');
|
||||
if (markParts.length >= 2) {
|
||||
// Highlighted part
|
||||
spans.add(
|
||||
TextSpan(
|
||||
text: markParts[0],
|
||||
style: TextStyle(
|
||||
backgroundColor: AppTheme.brandPrimary.withValues(alpha: 0.3),
|
||||
color: AppTheme.neutral50,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
);
|
||||
// Rest of the text
|
||||
spans.add(TextSpan(text: markParts.sublist(1).join('</mark>')));
|
||||
} else {
|
||||
spans.add(TextSpan(text: part));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return RichText(
|
||||
text: TextSpan(
|
||||
style: theme.bodyMedium?.copyWith(
|
||||
color: AppTheme.neutral50.withValues(alpha: 0.8),
|
||||
height: 1.4,
|
||||
),
|
||||
children: spans,
|
||||
),
|
||||
maxLines: 3,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
);
|
||||
}
|
||||
|
||||
String _formatTimestamp(DateTime timestamp) {
|
||||
final now = DateTime.now();
|
||||
final diff = now.difference(timestamp);
|
||||
|
||||
if (diff.inDays > 7) {
|
||||
return '${timestamp.day}/${timestamp.month}/${timestamp.year}';
|
||||
} else if (diff.inDays > 0) {
|
||||
return '${diff.inDays}d ago';
|
||||
} else if (diff.inHours > 0) {
|
||||
return '${diff.inHours}h ago';
|
||||
} else if (diff.inMinutes > 0) {
|
||||
return '${diff.inMinutes}m ago';
|
||||
} else {
|
||||
return 'Just now';
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user