188 lines
5.8 KiB
Dart
188 lines
5.8 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
import 'package:go_router/go_router.dart';
|
|
|
|
import '../../core/constants.dart';
|
|
import '../../data/models/briefing_feed.dart';
|
|
import '../../providers/api_client_provider.dart';
|
|
import '../../providers/news_provider.dart';
|
|
import '../../widgets/news_card.dart';
|
|
|
|
class NewsScreen extends ConsumerStatefulWidget {
|
|
const NewsScreen({super.key});
|
|
|
|
@override
|
|
ConsumerState<NewsScreen> createState() => _NewsScreenState();
|
|
}
|
|
|
|
class _NewsScreenState extends ConsumerState<NewsScreen> {
|
|
final Set<int> _openingChat = {};
|
|
|
|
Future<void> _handleDiscuss(int itemId) async {
|
|
if (_openingChat.contains(itemId)) return;
|
|
setState(() => _openingChat.add(itemId));
|
|
try {
|
|
final conversationId =
|
|
await ref.read(chatApiProvider).openArticleInChat(itemId);
|
|
if (mounted) {
|
|
context.push(Routes.chat.replaceFirst(':id', '$conversationId'));
|
|
}
|
|
} catch (_) {
|
|
if (mounted) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(content: Text('Failed to open article in chat.')),
|
|
);
|
|
}
|
|
} finally {
|
|
if (mounted) setState(() => _openingChat.remove(itemId));
|
|
}
|
|
}
|
|
|
|
Future<void> _loadMore() async {
|
|
try {
|
|
await ref.read(newsProvider.notifier).loadMore();
|
|
} catch (_) {
|
|
if (mounted) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(content: Text('Failed to load more articles.')),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final newsAsync = ref.watch(newsProvider);
|
|
final feedsAsync = ref.watch(feedsProvider);
|
|
final scheme = Theme.of(context).colorScheme;
|
|
|
|
return Scaffold(
|
|
appBar: AppBar(
|
|
title: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text('News', style: Theme.of(context).textTheme.titleLarge),
|
|
Text(
|
|
'Last 90 days',
|
|
style: Theme.of(context).textTheme.labelSmall?.copyWith(
|
|
color: scheme.onSurfaceVariant,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
body: newsAsync.when(
|
|
loading: () => const Center(child: CircularProgressIndicator()),
|
|
error: (e, _) => Center(
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
const Text('Could not load news.'),
|
|
const SizedBox(height: 12),
|
|
FilledButton.tonal(
|
|
onPressed: () => ref.invalidate(newsProvider),
|
|
child: const Text('Retry'),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
data: (news) => Column(
|
|
children: [
|
|
_FeedFilter(
|
|
feeds: feedsAsync.value ?? [],
|
|
selectedFeedId: news.selectedFeedId,
|
|
onChanged: (feedId) =>
|
|
ref.read(newsProvider.notifier).setFeed(feedId),
|
|
),
|
|
const Divider(height: 1),
|
|
Expanded(
|
|
child: RefreshIndicator(
|
|
onRefresh: () => ref.read(newsProvider.notifier).refresh(),
|
|
child: ListView.builder(
|
|
padding:
|
|
const EdgeInsets.symmetric(horizontal: 8, vertical: 8),
|
|
itemCount: news.items.length + 1,
|
|
itemBuilder: (_, i) {
|
|
if (i == news.items.length) {
|
|
if (!news.hasMore) return const SizedBox.shrink();
|
|
return Padding(
|
|
padding: const EdgeInsets.symmetric(vertical: 8),
|
|
child: Center(
|
|
child: news.loadingMore
|
|
? const CircularProgressIndicator()
|
|
: FilledButton.tonal(
|
|
onPressed: _loadMore,
|
|
child: const Text('Load more'),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
final item = news.items[i];
|
|
return NewsCard(
|
|
item: RssItemMeta.fromNewsItem(item),
|
|
reaction: news.reactions[item.id],
|
|
onReaction: (itemId, reaction) => ref
|
|
.read(newsProvider.notifier)
|
|
.toggleReaction(itemId, reaction),
|
|
onDiscuss: _openingChat.contains(item.id)
|
|
? null
|
|
: () => _handleDiscuss(item.id),
|
|
);
|
|
},
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _FeedFilter extends StatelessWidget {
|
|
final List<BriefingFeed> feeds;
|
|
final int? selectedFeedId;
|
|
final void Function(int? feedId) onChanged;
|
|
|
|
const _FeedFilter({
|
|
required this.feeds,
|
|
required this.selectedFeedId,
|
|
required this.onChanged,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Padding(
|
|
padding: const EdgeInsets.fromLTRB(16, 6, 16, 6),
|
|
child: Row(
|
|
children: [
|
|
Text(
|
|
'Feed:',
|
|
style: Theme.of(context).textTheme.labelMedium?.copyWith(
|
|
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
|
),
|
|
),
|
|
const SizedBox(width: 8),
|
|
DropdownButton<int?>(
|
|
value: selectedFeedId,
|
|
underline: const SizedBox.shrink(),
|
|
items: [
|
|
const DropdownMenuItem<int?>(
|
|
value: null,
|
|
child: Text('All feeds'),
|
|
),
|
|
...feeds.map(
|
|
(f) => DropdownMenuItem<int?>(
|
|
value: f.id,
|
|
child: Text(f.title),
|
|
),
|
|
),
|
|
],
|
|
onChanged: (v) => onChanged(v),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|