import 'package:flutter/material.dart'; import 'package:url_launcher/url_launcher.dart'; import '../models/source.dart'; import '../state/app_state.dart'; /// User-facing app name. Platform launcher labels (AndroidManifest, Info.plist, /// web/manifest.json, windows/runner) are set separately. const appName = 'Tagesbyte'; String timeAgo(DateTime? date) { if (date == null) return ''; final diff = DateTime.now().difference(date.toLocal()); if (diff.inMinutes < 1) return 'just now'; if (diff.inMinutes < 60) return '${diff.inMinutes} min ago'; if (diff.inHours < 24) return '${diff.inHours} h ago'; if (diff.inDays == 1) return 'yesterday'; return '${diff.inDays} days ago'; } String formatDateTime(DateTime date) { final d = date.toLocal(); String two(int n) => n.toString().padLeft(2, '0'); return '${two(d.day)}.${two(d.month)}.${d.year} ${two(d.hour)}:${two(d.minute)}'; } const weekdayNames = [ 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday', ]; const monthNames = [ 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December', ]; bool isSameDay(DateTime a, DateTime b) => a.year == b.year && a.month == b.month && a.day == b.day; /// The line above the articles of [kind], e.g. /// "12 articles · last 24 h · 3 paid/ads hidden". String articleCountLine(AppState state, SourceKind kind) { final hidden = state.hiddenCountOf(kind); return '${state.articlesOf(kind).length} articles · last ${_windowLabel(state.windowOf(kind))}' '${hidden > 0 ? ' · $hidden paid/ads hidden' : ''}'; } String _windowLabel(Duration window) => window.inHours < 72 ? '${window.inHours} h' : '${window.inDays} days'; Future openUrl(BuildContext context, String url) async { final ok = await launchUrl(Uri.parse(url), mode: LaunchMode.externalApplication); if (!ok && context.mounted) { ScaffoldMessenger.of(context) .showSnackBar(SnackBar(content: Text('Could not open $url'))); } } /// App bar button that refreshes all Sources; off while a refresh runs. class RefreshButton extends StatelessWidget { const RefreshButton({super.key, required this.state}); final AppState state; @override Widget build(BuildContext context) => IconButton( tooltip: 'Refresh', onPressed: state.loading ? null : state.refresh, icon: const Icon(Icons.refresh), ); } /// A scrollable [list] that refreshes all Sources when pulled down, under a /// progress bar while a refresh runs and an optional [notice]. class RefreshableList extends StatelessWidget { const RefreshableList({super.key, required this.state, required this.list, this.notice}); final AppState state; final Widget list; final Widget? notice; @override Widget build(BuildContext context) => Column( children: [ if (state.loading) const LinearProgressIndicator(), if (notice != null) notice!, Expanded(child: RefreshIndicator(onRefresh: state.refresh, child: list)), ], ); } /// Centered icon + text + optional action, used for empty and error states. class EmptyState extends StatelessWidget { const EmptyState({ super.key, required this.icon, required this.title, this.message, this.action, }); final IconData icon; final String title; final String? message; final Widget? action; @override Widget build(BuildContext context) { final theme = Theme.of(context); return Center( child: Padding( padding: const EdgeInsets.all(32), child: Column( mainAxisSize: MainAxisSize.min, children: [ Icon(icon, size: 56, color: theme.colorScheme.primary), const SizedBox(height: 16), Text(title, style: theme.textTheme.titleMedium, textAlign: TextAlign.center), if (message != null) ...[ const SizedBox(height: 8), Text( message!, style: theme.textTheme.bodyMedium ?.copyWith(color: theme.colorScheme.onSurfaceVariant), textAlign: TextAlign.center, ), ], if (action != null) ...[const SizedBox(height: 20), action!], ], ), ), ); } }