feat(news,calendar): improve corporate calendar filters and news sentiment dialogs
This commit is contained in:
@@ -7,10 +7,12 @@ class AdvancedNewsFilterBar extends StatelessWidget {
|
||||
final DateTime? selectedDate;
|
||||
final String? selectedIsin;
|
||||
final bool hasSentimentOnly;
|
||||
final String? selectedSentimentFilter; // null (Alle), 'POSITIVE', 'NEUTRAL', 'NEGATIVE'
|
||||
final Function(String?) onSearchChanged;
|
||||
final Function(DateTime?) onDateChanged;
|
||||
final Function(String?) onIsinChanged;
|
||||
final Function(bool) onSentimentToggleChanged;
|
||||
final Function(String?) onSentimentFilterChanged;
|
||||
final VoidCallback onResetFilters;
|
||||
|
||||
const AdvancedNewsFilterBar({
|
||||
@@ -19,10 +21,12 @@ class AdvancedNewsFilterBar extends StatelessWidget {
|
||||
required this.selectedDate,
|
||||
required this.selectedIsin,
|
||||
required this.hasSentimentOnly,
|
||||
this.selectedSentimentFilter,
|
||||
required this.onSearchChanged,
|
||||
required this.onDateChanged,
|
||||
required this.onIsinChanged,
|
||||
required this.onSentimentToggleChanged,
|
||||
required this.onSentimentFilterChanged,
|
||||
required this.onResetFilters,
|
||||
});
|
||||
|
||||
@@ -38,6 +42,7 @@ class AdvancedNewsFilterBar extends StatelessWidget {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Row 1: Search Query & ISIN Input
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
@@ -102,8 +107,14 @@ class AdvancedNewsFilterBar extends StatelessWidget {
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
|
||||
// Row 2: Date Picker, Sentiment Toggle Chips (Gut, Neutral, Schlecht), and Reset
|
||||
Wrap(
|
||||
spacing: 10,
|
||||
runSpacing: 8,
|
||||
crossAxisAlignment: WrapCrossAlignment.center,
|
||||
children: [
|
||||
// Date picker
|
||||
InkWell(
|
||||
onTap: () async {
|
||||
final date = await showDatePicker(
|
||||
@@ -138,6 +149,7 @@ class AdvancedNewsFilterBar extends StatelessWidget {
|
||||
border: Border.all(color: selectedDate != null ? AppTheme.primaryEmerald : AppTheme.glassBorder),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Icons.calendar_today, size: 16, color: selectedDate != null ? AppTheme.primaryEmerald : AppTheme.textMuted),
|
||||
const SizedBox(width: 8),
|
||||
@@ -160,9 +172,10 @@ class AdvancedNewsFilterBar extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
|
||||
// AI Analyzed Only Chip
|
||||
FilterChip(
|
||||
label: const Text('Nur Analysiert (KI)'),
|
||||
label: const Text('Nur KI-Analysiert'),
|
||||
selected: hasSentimentOnly,
|
||||
onSelected: onSentimentToggleChanged,
|
||||
backgroundColor: Colors.black.withValues(alpha: 0.2),
|
||||
@@ -171,14 +184,33 @@ class AdvancedNewsFilterBar extends StatelessWidget {
|
||||
side: BorderSide(color: hasSentimentOnly ? AppTheme.accentCyan : AppTheme.glassBorder),
|
||||
labelStyle: TextStyle(
|
||||
color: hasSentimentOnly ? AppTheme.accentCyan : AppTheme.textMuted,
|
||||
fontSize: 13,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
|
||||
// Sentiment Toggle Buttons (Alle, Gut, Neutral, Schlecht)
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black.withValues(alpha: 0.25),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: AppTheme.glassBorder),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
_sentimentToggleItem('Alle', null, Colors.white70),
|
||||
_sentimentToggleItem('Gut (Positiv)', 'POSITIVE', AppTheme.primaryEmerald, icon: Icons.trending_up),
|
||||
_sentimentToggleItem('Neutral', 'NEUTRAL', AppTheme.accentCyan, icon: Icons.remove),
|
||||
_sentimentToggleItem('Schlecht (Negativ)', 'NEGATIVE', AppTheme.accentRed, icon: Icons.trending_down),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Reset Filter Button
|
||||
TextButton.icon(
|
||||
onPressed: onResetFilters,
|
||||
icon: Icon(Icons.refresh, size: 16, color: AppTheme.textMuted),
|
||||
label: Text('Reset Filter', style: TextStyle(color: AppTheme.textMuted, fontSize: 13)),
|
||||
icon: Icon(Icons.refresh, size: 15, color: AppTheme.textMuted),
|
||||
label: Text('Reset', style: TextStyle(color: AppTheme.textMuted, fontSize: 12)),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -186,5 +218,36 @@ class AdvancedNewsFilterBar extends StatelessWidget {
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Widget _sentimentToggleItem(String label, String? value, Color activeColor, {IconData? icon}) {
|
||||
final isSelected = selectedSentimentFilter == value;
|
||||
return GestureDetector(
|
||||
onTap: () => onSentimentFilterChanged(value),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 7),
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected ? activeColor.withValues(alpha: 0.2) : Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
border: isSelected ? Border.all(color: activeColor.withValues(alpha: 0.6)) : null,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (icon != null) ...[
|
||||
Icon(icon, size: 13, color: isSelected ? activeColor : AppTheme.textMuted),
|
||||
const SizedBox(width: 4),
|
||||
],
|
||||
Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
color: isSelected ? activeColor : AppTheme.textMuted,
|
||||
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
|
||||
fontSize: 11.5,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,25 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
import '../models/news_article_model.dart';
|
||||
import '../../../core/network/api_client.dart';
|
||||
import '../../../core/theme/app_theme.dart';
|
||||
import '../../../core/widgets/status_badge.dart';
|
||||
import '../../asset_detail/views/asset_detail_screen.dart';
|
||||
import '../../favorites/cubit/favorites_cubit.dart';
|
||||
import '../models/news_article_model.dart';
|
||||
import '../repositories/news_repository.dart';
|
||||
import 'finbert_sentiment_tab.dart';
|
||||
|
||||
class ArticleSentimentDialog extends StatefulWidget {
|
||||
final NewsArticleModel articleData;
|
||||
final ApiClient? apiClient;
|
||||
final ValueChanged<NewsArticleModel>? onArticleUpdated;
|
||||
|
||||
const ArticleSentimentDialog({
|
||||
super.key,
|
||||
required this.articleData,
|
||||
this.apiClient,
|
||||
this.onArticleUpdated,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -19,10 +28,14 @@ class ArticleSentimentDialog extends StatefulWidget {
|
||||
|
||||
class _ArticleSentimentDialogState extends State<ArticleSentimentDialog> with SingleTickerProviderStateMixin {
|
||||
late TabController _tabController;
|
||||
late NewsArticleModel _currentArticle;
|
||||
bool _isReanalyzing = false;
|
||||
String? _reanalyzeError;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_currentArticle = widget.articleData;
|
||||
_tabController = TabController(length: 2, vsync: this);
|
||||
}
|
||||
|
||||
@@ -33,7 +46,7 @@ class _ArticleSentimentDialogState extends State<ArticleSentimentDialog> with Si
|
||||
}
|
||||
|
||||
void _openOriginalSource() async {
|
||||
final urlStr = widget.articleData.sourceUrl;
|
||||
final urlStr = _currentArticle.sourceUrl;
|
||||
if (urlStr.isNotEmpty) {
|
||||
final uri = Uri.parse(urlStr);
|
||||
if (await canLaunchUrl(uri)) {
|
||||
@@ -42,15 +55,60 @@ class _ArticleSentimentDialogState extends State<ArticleSentimentDialog> with Si
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _reanalyzeSentiment() async {
|
||||
if (_isReanalyzing || _currentArticle.id.isEmpty) return;
|
||||
|
||||
setState(() {
|
||||
_isReanalyzing = true;
|
||||
_reanalyzeError = null;
|
||||
});
|
||||
|
||||
try {
|
||||
final client = widget.apiClient ?? context.read<ApiClient>();
|
||||
final repo = NewsRepository(apiClient: client, backendUrl: ApiClient.baseUrl);
|
||||
final updatedArticle = await repo.reanalyzeArticle(_currentArticle.id);
|
||||
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_currentArticle = updatedArticle;
|
||||
_isReanalyzing = false;
|
||||
});
|
||||
|
||||
widget.onArticleUpdated?.call(updatedArticle);
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Row(
|
||||
children: [
|
||||
Icon(Icons.check_circle, color: AppTheme.primaryEmerald, size: 20),
|
||||
const SizedBox(width: 8),
|
||||
const Text('Sentiment-Analyse erfolgreich erneuert!'),
|
||||
],
|
||||
),
|
||||
backgroundColor: AppTheme.cardSurface,
|
||||
duration: const Duration(seconds: 3),
|
||||
),
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isReanalyzing = false;
|
||||
_reanalyzeError = e.toString();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final article = widget.articleData;
|
||||
final article = _currentArticle;
|
||||
final title = article.title.isNotEmpty ? article.title : 'Nachrichtenartikel';
|
||||
final author = article.author.isNotEmpty ? article.author : 'Finlytic News';
|
||||
final summary = article.summary;
|
||||
final sourceUrl = article.sourceUrl;
|
||||
final contentRaw = article.contentRaw;
|
||||
final publishedAt = "${article.publishedAt.day}.${article.publishedAt.month}.${article.publishedAt.year}";
|
||||
final publishedAt = "${article.publishedAt.day.toString().padLeft(2, '0')}.${article.publishedAt.month.toString().padLeft(2, '0')}.${article.publishedAt.year}";
|
||||
final status = article.status.isNotEmpty ? article.status : 'Completed';
|
||||
final rawSentiment = article.sentiment;
|
||||
|
||||
@@ -63,6 +121,9 @@ class _ArticleSentimentDialogState extends State<ArticleSentimentDialog> with Si
|
||||
color: status.toLowerCase().contains('analyz') ? AppTheme.primaryEmerald : AppTheme.accentCyan,
|
||||
);
|
||||
|
||||
final client = widget.apiClient ?? (context.mounted ? context.read<ApiClient>() : null);
|
||||
final favourites = context.read<FavoritesCubit>().state.favoriteDetails;
|
||||
|
||||
return Dialog(
|
||||
backgroundColor: AppTheme.cardSurface,
|
||||
shape: RoundedRectangleBorder(
|
||||
@@ -70,13 +131,15 @@ class _ArticleSentimentDialogState extends State<ArticleSentimentDialog> with Si
|
||||
side: BorderSide(color: AppTheme.glassBorder),
|
||||
),
|
||||
child: Container(
|
||||
width: 650,
|
||||
height: 600,
|
||||
width: 720,
|
||||
height: 640,
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Header
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
@@ -88,7 +151,7 @@ class _ArticleSentimentDialogState extends State<ArticleSentimentDialog> with Si
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
'Quelle: $author • $publishedAt',
|
||||
style: TextStyle(color: AppTheme.textMuted, fontSize: 12),
|
||||
@@ -96,15 +159,64 @@ class _ArticleSentimentDialogState extends State<ArticleSentimentDialog> with Si
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
listBadge,
|
||||
IconButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
icon: const Icon(Icons.close, color: Colors.white70),
|
||||
const SizedBox(width: 12),
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
// Sentiment Reanalyze Icon Button (directly left of badge)
|
||||
IconButton(
|
||||
tooltip: 'KI-Sentiment neu analysieren',
|
||||
onPressed: _isReanalyzing ? null : _reanalyzeSentiment,
|
||||
icon: _isReanalyzing
|
||||
? SizedBox(
|
||||
width: 14,
|
||||
height: 14,
|
||||
child: CircularProgressIndicator(strokeWidth: 2, color: AppTheme.accentCyan),
|
||||
)
|
||||
: Icon(Icons.auto_awesome, size: 18, color: AppTheme.accentCyan),
|
||||
padding: const EdgeInsets.all(6),
|
||||
constraints: const BoxConstraints(),
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
listBadge,
|
||||
const SizedBox(width: 8),
|
||||
IconButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
icon: const Icon(Icons.close, color: Colors.white70, size: 20),
|
||||
padding: EdgeInsets.zero,
|
||||
constraints: const BoxConstraints(),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
if (_reanalyzeError != null) ...[
|
||||
const SizedBox(height: 8),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: AppTheme.accentRed.withValues(alpha: 0.15),
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
border: Border.all(color: AppTheme.accentRed.withValues(alpha: 0.4)),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.error_outline, size: 16, color: AppTheme.accentRed),
|
||||
const SizedBox(width: 6),
|
||||
Expanded(
|
||||
child: Text(
|
||||
_reanalyzeError!,
|
||||
style: TextStyle(color: AppTheme.accentRed, fontSize: 11),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// Tab Bar
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: AppTheme.glassSurface,
|
||||
@@ -118,12 +230,14 @@ class _ArticleSentimentDialogState extends State<ArticleSentimentDialog> with Si
|
||||
unselectedLabelColor: AppTheme.textMuted,
|
||||
indicatorSize: TabBarIndicatorSize.tab,
|
||||
tabs: const [
|
||||
Tab(icon: Icon(Icons.article_outlined, size: 18), text: 'Artikel-Inhalt'),
|
||||
Tab(icon: Icon(Icons.psychology_outlined, size: 18), text: 'FinBERT Sentiment'),
|
||||
Tab(icon: Icon(Icons.article_outlined, size: 18), text: 'Artikel & Assets'),
|
||||
Tab(icon: Icon(Icons.psychology_outlined, size: 18), text: 'FinBERT KI-Sentiment'),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// Tab Views
|
||||
Expanded(
|
||||
child: TabBarView(
|
||||
controller: _tabController,
|
||||
@@ -134,37 +248,84 @@ class _ArticleSentimentDialogState extends State<ArticleSentimentDialog> with Si
|
||||
children: [
|
||||
if (summary.isNotEmpty) ...[
|
||||
const Text('Zusammenfassung:', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 13, color: Colors.white)),
|
||||
const SizedBox(height: 4),
|
||||
const SizedBox(height: 6),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(10),
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: AppTheme.glassSurface,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: AppTheme.glassBorder),
|
||||
),
|
||||
child: Text(summary, style: const TextStyle(color: Colors.white70, fontSize: 13)),
|
||||
child: Text(summary, style: const TextStyle(color: Colors.white70, fontSize: 13, height: 1.4)),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
const SizedBox(height: 14),
|
||||
],
|
||||
if (article.matchedAssets.isNotEmpty) ...[
|
||||
const Text('Zugeordnete Assets:', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 13, color: Colors.white)),
|
||||
const SizedBox(height: 6),
|
||||
Row(
|
||||
children: [
|
||||
const Text('Erkannte Unternehmen & Assets:', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 13, color: Colors.white)),
|
||||
const SizedBox(width: 6),
|
||||
Text('(Klick öffnet Asset-Details)', style: TextStyle(color: AppTheme.textMuted, fontSize: 11)),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Wrap(
|
||||
spacing: 6,
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
children: article.matchedAssets.map((asset) {
|
||||
return Chip(
|
||||
label: Text('${asset.symbol} (${asset.isin})', style: const TextStyle(fontSize: 11, color: Colors.white)),
|
||||
backgroundColor: AppTheme.glassSurface,
|
||||
side: BorderSide(color: AppTheme.glassBorder),
|
||||
final isin = asset.isin;
|
||||
final name = asset.name.isNotEmpty ? asset.name : (asset.symbol.isNotEmpty ? asset.symbol : isin);
|
||||
final match = favourites.where((e) => e.isin == isin);
|
||||
final symbol = match.isEmpty ? null : match.first;
|
||||
|
||||
return ActionChip(
|
||||
avatar: Icon(Icons.open_in_new, size: 14, color: AppTheme.accentCyan),
|
||||
label: Text(
|
||||
name,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppTheme.accentCyan,
|
||||
),
|
||||
),
|
||||
backgroundColor: AppTheme.accentCyan.withValues(alpha: 0.1),
|
||||
side: BorderSide(color: AppTheme.accentCyan.withValues(alpha: 0.35)),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
||||
onPressed: () {
|
||||
if (client != null && isin.isNotEmpty) {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (_) => AssetDetailScreen(
|
||||
isin: isin,
|
||||
name: name,
|
||||
symbol: symbol != null ? symbol.symbol : asset.symbol,
|
||||
apiClient: client,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
const SizedBox(height: 14),
|
||||
],
|
||||
const Text('Vollständiger Artikeltext:', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 13, color: Colors.white)),
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
contentRaw.isNotEmpty ? contentRaw : 'Kein vollständiger Text verfügbar.',
|
||||
style: const TextStyle(color: Colors.white70, fontSize: 13, height: 1.4),
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black.withValues(alpha: 0.2),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: AppTheme.glassBorder),
|
||||
),
|
||||
child: Text(
|
||||
contentRaw.isNotEmpty ? contentRaw : (summary.isNotEmpty ? summary : 'Kein vollständiger Text verfügbar.'),
|
||||
style: const TextStyle(color: Colors.white70, fontSize: 13, height: 1.5),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -174,6 +335,8 @@ class _ArticleSentimentDialogState extends State<ArticleSentimentDialog> with Si
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// Footer
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
@@ -181,15 +344,20 @@ class _ArticleSentimentDialogState extends State<ArticleSentimentDialog> with Si
|
||||
TextButton.icon(
|
||||
onPressed: _openOriginalSource,
|
||||
icon: const Icon(Icons.open_in_new, size: 16),
|
||||
label: const Text('Originalquelle im Browser öffnen'),
|
||||
label: const Text('Originalquelle im Web öffnen'),
|
||||
style: TextButton.styleFrom(foregroundColor: AppTheme.accentCyan),
|
||||
)
|
||||
else
|
||||
const SizedBox.shrink(),
|
||||
ElevatedButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
style: ElevatedButton.styleFrom(backgroundColor: AppTheme.primaryEmerald, foregroundColor: Colors.black),
|
||||
child: const Text('Schließen'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppTheme.primaryEmerald,
|
||||
foregroundColor: Colors.black,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
||||
),
|
||||
child: const Text('Schließen', style: TextStyle(fontWeight: FontWeight.bold)),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'package:finlytic_app/core/widgets/status_badge.dart';
|
||||
import 'package:finlytic_app/features/favorites/cubit/favorites_cubit.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
@@ -10,15 +11,17 @@ import '../../asset_detail/views/asset_detail_screen.dart';
|
||||
import '../models/news_article_model.dart';
|
||||
import 'article_sentiment_dialog.dart';
|
||||
|
||||
/// News Card Item displaying news info, tagged assets, timestamp, and sentiment.
|
||||
/// News Card Item displaying news info, tagged assets, timestamp, and sentiment badge (wie auf dem Dashboard).
|
||||
class NewsCardItem extends StatelessWidget {
|
||||
final dynamic item;
|
||||
final ApiClient apiClient;
|
||||
final ValueChanged<NewsArticleModel>? onArticleUpdated;
|
||||
|
||||
const NewsCardItem({
|
||||
super.key,
|
||||
required this.item,
|
||||
required this.apiClient,
|
||||
this.onArticleUpdated,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -44,7 +47,9 @@ class NewsCardItem extends StatelessWidget {
|
||||
final String? sentimentLabel = sentimentObj?['label']?.toString() ?? article['sentiment']?.toString();
|
||||
final double? score = ((sentimentObj?['compoundScore'] ?? sentimentObj?['compound_score'] ?? article['sentimentScore']) as num?)?.toDouble();
|
||||
|
||||
|
||||
final Widget? badgeWidget = (sentimentLabel != null && sentimentLabel.trim().isNotEmpty)
|
||||
? StatusBadge.sentiment(sentimentLabel, score: score)
|
||||
: null;
|
||||
|
||||
final matchedAssetsRaw = article['MatchedAssets'] ?? article['matchedAssets'];
|
||||
final matchedAssets = matchedAssetsRaw is List ? matchedAssetsRaw : [];
|
||||
@@ -79,7 +84,11 @@ class NewsCardItem extends StatelessWidget {
|
||||
}
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (_) => ArticleSentimentDialog(articleData: articleModel),
|
||||
builder: (_) => ArticleSentimentDialog(
|
||||
articleData: articleModel,
|
||||
apiClient: apiClient,
|
||||
onArticleUpdated: onArticleUpdated,
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Column(
|
||||
@@ -89,43 +98,61 @@ class NewsCardItem extends StatelessWidget {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(title, style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 15)),
|
||||
child: Text(
|
||||
title,
|
||||
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 14.5),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
//badgeWidget,
|
||||
if (badgeWidget != null) ...[
|
||||
const SizedBox(width: 8),
|
||||
badgeWidget,
|
||||
],
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(summary, maxLines: 2, overflow: TextOverflow.ellipsis, style: TextStyle(color: AppTheme.textSecondary, fontSize: 13)),
|
||||
if (summary.isNotEmpty) ...[
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
summary,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(color: AppTheme.textSecondary, fontSize: 12.5),
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 10),
|
||||
Text('$author • $pubTime', style: TextStyle(color: AppTheme.textMuted, fontSize: 11)),
|
||||
if (matchedAssets.isNotEmpty) ...[
|
||||
const SizedBox(height: 8),
|
||||
Wrap(
|
||||
spacing: 4,
|
||||
spacing: 6,
|
||||
runSpacing: 4,
|
||||
children: matchedAssets.map((assetItem) {
|
||||
final isin = assetItem['isin']!;
|
||||
final name = assetItem['name']!;
|
||||
final String isin = (assetItem is Map ? (assetItem['isin'] ?? assetItem['Isin']) : assetItem)?.toString() ?? '';
|
||||
final String name = (assetItem is Map ? (assetItem['name'] ?? assetItem['Name'] ?? assetItem['symbol'] ?? isin) : isin)?.toString() ?? isin;
|
||||
final match = favourites.where((e) => e.isin == isin);
|
||||
final symbol = match.isEmpty ? null : match.first;
|
||||
return ActionChip(
|
||||
label: Text(name, style: TextStyle(fontSize: 10, color: AppTheme.accentCyan)),
|
||||
avatar: Icon(Icons.show_chart, size: 14, color: AppTheme.accentCyan),
|
||||
label: Text(name, style: TextStyle(fontSize: 11, color: AppTheme.accentCyan, fontWeight: FontWeight.bold)),
|
||||
backgroundColor: AppTheme.glassSurface,
|
||||
padding: EdgeInsets.zero,
|
||||
side: BorderSide(color: AppTheme.glassBorder),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 0),
|
||||
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
onPressed: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (_) => AssetDetailScreen(
|
||||
isin: assetItem,
|
||||
name: name,
|
||||
symbol: symbol != null ? symbol.symbol : null,
|
||||
apiClient: apiClient,
|
||||
if (isin.isNotEmpty) {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (_) => AssetDetailScreen(
|
||||
isin: isin,
|
||||
name: name,
|
||||
symbol: symbol != null ? symbol.symbol : null,
|
||||
apiClient: apiClient,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
}).toList(),
|
||||
|
||||
Reference in New Issue
Block a user