feat: add news ticker widget for scrolling headlines
- Add NewsTickerWidget with horizontal auto-scrolling at 40px/sec - Add NewsData and NewsHeadline Freezed models - Add news datasource fetching from /tools/news endpoint - Add news provider with 30-minute auto-refresh - Place ticker between Welcome card and System Stats on dashboard - Show placeholder headlines when no data (italic, muted style) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.5
parent
a376482cd1
commit
4331555f84
@@ -0,0 +1,38 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:tatlock_ui/core/api/api_client.dart';
|
||||
import 'package:tatlock_ui/features/front_hall/data/models/news_model.dart';
|
||||
|
||||
part 'news_datasource.g.dart';
|
||||
|
||||
/// Data source for news data operations.
|
||||
///
|
||||
/// Fetches news headlines from Core API.
|
||||
class NewsDatasource {
|
||||
NewsDatasource(this._dio);
|
||||
|
||||
final Dio _dio;
|
||||
|
||||
static const _basePath = '/tools/news';
|
||||
|
||||
/// Gets current news headlines.
|
||||
///
|
||||
/// Returns headlines for the news ticker.
|
||||
Future<NewsData> getNews() async {
|
||||
final response = await _dio.get<Map<String, dynamic>>(_basePath);
|
||||
final data = response.data;
|
||||
|
||||
if (data == null) {
|
||||
throw Exception('Failed to fetch news data');
|
||||
}
|
||||
|
||||
return NewsData.fromJson(data);
|
||||
}
|
||||
}
|
||||
|
||||
/// Provides the news datasource.
|
||||
@riverpod
|
||||
NewsDatasource newsDatasource(Ref ref) {
|
||||
final dio = ref.watch(coreApiClientProvider);
|
||||
return NewsDatasource(dio);
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'news_model.freezed.dart';
|
||||
part 'news_model.g.dart';
|
||||
|
||||
/// News response from Core API.
|
||||
/// Contains headlines for the news ticker.
|
||||
@freezed
|
||||
sealed class NewsData with _$NewsData {
|
||||
const factory NewsData({
|
||||
required List<NewsHeadline> headlines,
|
||||
String? category,
|
||||
List<String>? sources,
|
||||
@JsonKey(name: 'updated_at') required DateTime updatedAt,
|
||||
String? user,
|
||||
}) = _NewsData;
|
||||
|
||||
factory NewsData.fromJson(Map<String, dynamic> json) =>
|
||||
_$NewsDataFromJson(json);
|
||||
}
|
||||
|
||||
/// Single news headline.
|
||||
@freezed
|
||||
sealed class NewsHeadline with _$NewsHeadline {
|
||||
const factory NewsHeadline({
|
||||
required String title,
|
||||
String? description,
|
||||
String? source,
|
||||
String? url,
|
||||
}) = _NewsHeadline;
|
||||
|
||||
factory NewsHeadline.fromJson(Map<String, dynamic> json) =>
|
||||
_$NewsHeadlineFromJson(json);
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:tatlock_ui/features/front_hall/data/datasources/news_datasource.dart';
|
||||
import 'package:tatlock_ui/features/front_hall/data/models/news_model.dart';
|
||||
|
||||
part 'news_provider.g.dart';
|
||||
|
||||
/// Fetches news headlines from Core API.
|
||||
///
|
||||
/// Auto-invalidates to keep data fresh.
|
||||
/// News data in Qdrant has TTL, so periodic refresh is reasonable.
|
||||
@riverpod
|
||||
Future<NewsData> news(Ref ref) async {
|
||||
final datasource = ref.watch(newsDatasourceProvider);
|
||||
return datasource.getNews();
|
||||
}
|
||||
|
||||
/// Provides whether news data is available.
|
||||
///
|
||||
/// Used for conditional rendering of NewsTickerWidget.
|
||||
@riverpod
|
||||
bool hasNews(Ref ref) {
|
||||
final asyncValue = ref.watch(newsProvider);
|
||||
return asyncValue.maybeWhen(
|
||||
data: (data) => data.headlines.isNotEmpty,
|
||||
orElse: () => false,
|
||||
);
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:tatlock_ui/features/front_hall/data/models/system_stats_model.dart';
|
||||
import 'package:tatlock_ui/features/front_hall/presentation/providers/environment_provider.dart';
|
||||
import 'package:tatlock_ui/features/front_hall/presentation/providers/news_provider.dart';
|
||||
import 'package:tatlock_ui/features/front_hall/presentation/providers/system_stats_provider.dart';
|
||||
import 'package:tatlock_ui/shared/theme/stoplight_colors.dart';
|
||||
import 'package:tatlock_ui/shared/widgets/forecast_widget.dart';
|
||||
@@ -25,6 +26,7 @@ class DashboardContent extends ConsumerStatefulWidget {
|
||||
class _DashboardContentState extends ConsumerState<DashboardContent> {
|
||||
Timer? _systemStatsTimer;
|
||||
Timer? _environmentTimer;
|
||||
Timer? _newsTimer;
|
||||
|
||||
/// Tracks if we've logged the environment API user (log once per session)
|
||||
static bool _hasLoggedEnvUser = false;
|
||||
@@ -46,12 +48,18 @@ class _DashboardContentState extends ConsumerState<DashboardContent> {
|
||||
const Duration(hours: 1),
|
||||
(_) => ref.invalidate(environmentProvider),
|
||||
);
|
||||
// Refresh news every 30 minutes
|
||||
_newsTimer = Timer.periodic(
|
||||
const Duration(minutes: 30),
|
||||
(_) => ref.invalidate(newsProvider),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_systemStatsTimer?.cancel();
|
||||
_environmentTimer?.cancel();
|
||||
_newsTimer?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@@ -60,6 +68,7 @@ class _DashboardContentState extends ConsumerState<DashboardContent> {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
final systemStatsAsync = ref.watch(systemStatsProvider);
|
||||
final environmentAsync = ref.watch(environmentProvider);
|
||||
final newsAsync = ref.watch(newsProvider);
|
||||
|
||||
return ListView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
@@ -100,6 +109,14 @@ class _DashboardContentState extends ConsumerState<DashboardContent> {
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// News Ticker - full width
|
||||
newsAsync.when(
|
||||
data: (newsData) => NewsTickerWidget(newsData: newsData),
|
||||
loading: () => const NewsTickerWidget(),
|
||||
error: (error, stack) => const NewsTickerWidget(),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// System Stats - Gauges
|
||||
_SectionHeader(title: 'System Stats', icon: Icons.monitor_heart),
|
||||
const SizedBox(height: 8),
|
||||
|
||||
Reference in New Issue
Block a user