Merge pull request 'Release v26.03.27.1' (#11) from dev into main
Release v26.03.27.1
This commit was merged in pull request #11.
This commit is contained in:
@@ -70,5 +70,14 @@
|
||||
<action android:name="android.intent.action.PROCESS_TEXT"/>
|
||||
<data android:mimeType="text/plain"/>
|
||||
</intent>
|
||||
<!-- url_launcher: open http/https links in browser -->
|
||||
<intent>
|
||||
<action android:name="android.intent.action.VIEW"/>
|
||||
<data android:scheme="https"/>
|
||||
</intent>
|
||||
<intent>
|
||||
<action android:name="android.intent.action.VIEW"/>
|
||||
<data android:scheme="http"/>
|
||||
</intent>
|
||||
</queries>
|
||||
</manifest>
|
||||
|
||||
@@ -9,6 +9,7 @@ import '../../providers/briefing_provider.dart';
|
||||
import '../../providers/api_client_provider.dart';
|
||||
import '../../widgets/chat_message_bubble.dart';
|
||||
import '../../widgets/weather_card.dart';
|
||||
import '../../widgets/news_card.dart';
|
||||
import 'briefing_history_screen.dart';
|
||||
|
||||
class BriefingScreen extends ConsumerStatefulWidget {
|
||||
@@ -322,55 +323,27 @@ class _BriefingMessageItem extends StatelessWidget {
|
||||
final bool hasWeatherKey = isAssistant && meta != null && meta.containsKey('weather');
|
||||
final weatherData = hasWeatherKey ? meta['weather'] as Map<String, dynamic>? : null;
|
||||
|
||||
// RSS reactions
|
||||
final rssItemIds = isAssistant && meta != null
|
||||
? (meta['rss_item_ids'] as List<dynamic>?)?.cast<int>() ?? []
|
||||
: <int>[];
|
||||
|
||||
final scheme = Theme.of(context).colorScheme;
|
||||
// RSS news cards
|
||||
final rssItemsRaw = isAssistant && meta != null
|
||||
? (meta['rss_items'] as List<dynamic>?)?.cast<Map<String, dynamic>>() ?? []
|
||||
: <Map<String, dynamic>>[];
|
||||
final rssItems = rssItemsRaw.map(RssItemMeta.fromJson).toList();
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
if (hasWeatherKey) WeatherCard(weather: weatherData),
|
||||
ChatMessageBubble(message: message),
|
||||
if (rssItemIds.isNotEmpty)
|
||||
if (rssItems.isNotEmpty)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 8, bottom: 4),
|
||||
padding: const EdgeInsets.fromLTRB(4, 4, 4, 4),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: rssItemIds.asMap().entries.map((entry) {
|
||||
final index = entry.key;
|
||||
final itemId = entry.value;
|
||||
final current = reactions[itemId];
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(top: 4),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
'Story ${index + 1}',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: scheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
_ReactionButton(
|
||||
emoji: '👍',
|
||||
active: current == 'up',
|
||||
onTap: () => onReaction(itemId, 'up'),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
_ReactionButton(
|
||||
emoji: '👎',
|
||||
active: current == 'down',
|
||||
onTap: () => onReaction(itemId, 'down'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: rssItems.map((item) => NewsCard(
|
||||
item: item,
|
||||
reaction: reactions[item.id],
|
||||
onReaction: onReaction,
|
||||
)).toList(),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -378,39 +351,6 @@ class _BriefingMessageItem extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class _ReactionButton extends StatelessWidget {
|
||||
final String emoji;
|
||||
final bool active;
|
||||
final VoidCallback onTap;
|
||||
|
||||
const _ReactionButton({
|
||||
required this.emoji,
|
||||
required this.active,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final scheme = Theme.of(context).colorScheme;
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 150),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
|
||||
decoration: BoxDecoration(
|
||||
color: active
|
||||
? scheme.primary.withValues(alpha: 0.12)
|
||||
: Colors.transparent,
|
||||
border: Border.all(
|
||||
color: active ? scheme.primary : scheme.outlineVariant,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
child: Text(emoji, style: const TextStyle(fontSize: 14)),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _GradientSendButton extends StatelessWidget {
|
||||
final VoidCallback? onPressed;
|
||||
|
||||
@@ -0,0 +1,184 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
class RssItemMeta {
|
||||
final int id;
|
||||
final String title;
|
||||
final String url;
|
||||
final String source;
|
||||
final String snippet;
|
||||
final DateTime? publishedAt;
|
||||
|
||||
const RssItemMeta({
|
||||
required this.id,
|
||||
required this.title,
|
||||
required this.url,
|
||||
required this.source,
|
||||
required this.snippet,
|
||||
this.publishedAt,
|
||||
});
|
||||
|
||||
factory RssItemMeta.fromJson(Map<String, dynamic> json) => RssItemMeta(
|
||||
id: json['id'] as int,
|
||||
title: json['title'] as String? ?? '',
|
||||
url: json['url'] as String? ?? '',
|
||||
source: json['source'] as String? ?? '',
|
||||
snippet: json['snippet'] as String? ?? '',
|
||||
publishedAt: json['published_at'] != null
|
||||
? DateTime.tryParse(json['published_at'] as String)
|
||||
: null,
|
||||
);
|
||||
|
||||
String get relativeDate {
|
||||
if (publishedAt == null) return '';
|
||||
final diff = DateTime.now().difference(publishedAt!);
|
||||
if (diff.inHours < 24) return '${diff.inHours}h ago';
|
||||
if (diff.inHours < 48) return 'Yesterday';
|
||||
return '${publishedAt!.month}/${publishedAt!.day}';
|
||||
}
|
||||
}
|
||||
|
||||
class NewsCard extends StatelessWidget {
|
||||
final RssItemMeta item;
|
||||
final String? reaction; // 'up' | 'down' | null
|
||||
final void Function(int itemId, String reaction) onReaction;
|
||||
|
||||
const NewsCard({
|
||||
super.key,
|
||||
required this.item,
|
||||
required this.reaction,
|
||||
required this.onReaction,
|
||||
});
|
||||
|
||||
Future<void> _openUrl() async {
|
||||
if (item.url.isEmpty) return;
|
||||
final uri = Uri.tryParse(item.url);
|
||||
if (uri != null && await canLaunchUrl(uri)) {
|
||||
await launchUrl(uri, mode: LaunchMode.externalApplication);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final scheme = Theme.of(context).colorScheme;
|
||||
final textTheme = Theme.of(context).textTheme;
|
||||
|
||||
return Card(
|
||||
margin: const EdgeInsets.symmetric(vertical: 4),
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
side: BorderSide(color: scheme.outlineVariant),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(12, 10, 12, 10),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Source + date row
|
||||
Row(
|
||||
children: [
|
||||
if (item.source.isNotEmpty)
|
||||
Text(
|
||||
item.source.toUpperCase(),
|
||||
style: textTheme.labelSmall?.copyWith(
|
||||
color: scheme.primary,
|
||||
fontWeight: FontWeight.w700,
|
||||
letterSpacing: 0.6,
|
||||
),
|
||||
),
|
||||
if (item.source.isNotEmpty && item.relativeDate.isNotEmpty)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 8),
|
||||
child: Text(
|
||||
item.relativeDate,
|
||||
style: textTheme.labelSmall?.copyWith(
|
||||
color: scheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
// Title — tappable if URL present
|
||||
GestureDetector(
|
||||
onTap: item.url.isNotEmpty ? _openUrl : null,
|
||||
child: Text(
|
||||
item.title,
|
||||
style: textTheme.bodyMedium?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: item.url.isNotEmpty ? scheme.primary : scheme.onSurface,
|
||||
decoration: item.url.isNotEmpty ? TextDecoration.underline : null,
|
||||
decorationColor: scheme.primary,
|
||||
),
|
||||
),
|
||||
),
|
||||
// Snippet
|
||||
if (item.snippet.isNotEmpty) ...[
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
item.snippet,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: textTheme.bodySmall?.copyWith(
|
||||
color: scheme.onSurfaceVariant,
|
||||
height: 1.45,
|
||||
),
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 8),
|
||||
// Reaction buttons
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
_ReactionButton(
|
||||
emoji: '👍',
|
||||
active: reaction == 'up',
|
||||
onTap: () => onReaction(item.id, 'up'),
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
_ReactionButton(
|
||||
emoji: '👎',
|
||||
active: reaction == 'down',
|
||||
onTap: () => onReaction(item.id, 'down'),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ReactionButton extends StatelessWidget {
|
||||
final String emoji;
|
||||
final bool active;
|
||||
final VoidCallback onTap;
|
||||
|
||||
const _ReactionButton({
|
||||
required this.emoji,
|
||||
required this.active,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final scheme = Theme.of(context).colorScheme;
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 150),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
|
||||
decoration: BoxDecoration(
|
||||
color: active ? scheme.primary.withValues(alpha: 0.12) : Colors.transparent,
|
||||
border: Border.all(
|
||||
color: active ? scheme.primary : scheme.outlineVariant,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
child: Text(emoji, style: const TextStyle(fontSize: 14)),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -27,6 +27,7 @@ dependencies:
|
||||
flutter_markdown_plus: ^1.0.7
|
||||
google_fonts: ^8.0.2
|
||||
flutter_timezone: ^5.0.2
|
||||
url_launcher: ^6.3.1
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
||||
Reference in New Issue
Block a user