Share the article count line and refresh scaffolding of Today and Learn (review items 5, 6)

articleCountLine, RefreshButton and RefreshableList live in common.dart.
LearnScreen._describe becomes _windowLabel there, and LearnScreen._section
is _kindSection like in SourcesScreen.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
CedricandClaude Opus 5.5 committed 2026-10-16 16:07:37 +02:00
1 parent 246564f1d9
commit b773c363fe
3 files changed
+65 -51

No files matched your search

+47
View File
@@ -1,6 +1,9 @@
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';
@@ -33,6 +36,17 @@ const monthNames = [
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<void> openUrl(BuildContext context, String url) async {
final ok = await launchUrl(Uri.parse(url), mode: LaunchMode.externalApplication);
if (!ok && context.mounted) {
@@ -41,6 +55,39 @@ Future<void> openUrl(BuildContext context, String url) async {
}
}
/// 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({
+8 -24
View File
@@ -31,7 +31,6 @@ class _FeedScreenState extends State<FeedScreen> {
.where((a) => _sourceId == null || a.source.id == _sourceId)
.toList();
final sourcesInFeed = {for (final a in all) a.source.id: a.source};
final hidden = state.hiddenCountOf(SourceKind.news);
return Scaffold(
appBar: AppBar(
@@ -40,38 +39,23 @@ class _FeedScreenState extends State<FeedScreen> {
children: [
const Text(appName),
Text(
state.loading
? 'Loading feeds…'
: '${all.length} articles · last ${state.settings.maxAgeHours} h'
'${hidden > 0 ? ' · $hidden paid/ads hidden' : ''}',
state.loading ? 'Loading feeds…' : articleCountLine(state, SourceKind.news),
style: Theme.of(context).textTheme.bodySmall,
),
],
),
actions: [
IconButton(
tooltip: 'Refresh',
onPressed: state.loading ? null : state.refresh,
icon: const Icon(Icons.refresh),
),
],
actions: [RefreshButton(state: state)],
bottom: PreferredSize(
preferredSize: const Size.fromHeight(52),
child: _filterBar(sourcesInFeed.values.toList()),
),
),
body: Column(
children: [
if (state.loading) const LinearProgressIndicator(),
if (state.feedErrors.isNotEmpty && !state.loading)
_ErrorBanner(errors: state.feedErrors),
Expanded(
child: RefreshIndicator(
onRefresh: state.refresh,
child: _list(state, visible),
),
),
],
body: RefreshableList(
state: state,
notice: state.feedErrors.isNotEmpty && !state.loading
? _ErrorBanner(errors: state.feedErrors)
: null,
list: _list(state, visible),
),
);
},
+10 -27
View File
@@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
import '../models/source.dart';
import '../state/app_state.dart';
import 'article_screen.dart';
import 'common.dart';
import 'feed_screen.dart';
/// Reading that keeps for days, from the Sources that aren't News: one
@@ -22,37 +23,23 @@ class LearnScreen extends StatelessWidget {
builder: (context, _) => Scaffold(
appBar: AppBar(
title: const Text('Learn'),
actions: [
IconButton(
tooltip: 'Refresh',
onPressed: state.loading ? null : state.refresh,
icon: const Icon(Icons.refresh),
),
],
actions: [RefreshButton(state: state)],
),
body: Column(
children: [
if (state.loading) const LinearProgressIndicator(),
Expanded(
child: RefreshIndicator(
onRefresh: state.refresh,
child: ListView(
padding: const EdgeInsets.fromLTRB(12, 0, 12, 24),
children: [for (final kind in kinds) ..._section(context, kind)],
),
),
),
],
body: RefreshableList(
state: state,
list: ListView(
padding: const EdgeInsets.fromLTRB(12, 0, 12, 24),
children: [for (final kind in kinds) ..._kindSection(context, kind)],
),
),
),
);
}
List<Widget> _section(BuildContext context, SourceKind kind) {
List<Widget> _kindSection(BuildContext context, SourceKind kind) {
final theme = Theme.of(context);
final muted = theme.colorScheme.onSurfaceVariant;
final articles = state.articlesOf(kind);
final hidden = state.hiddenCountOf(kind);
final hasSources = state.enabledSources.any((s) => s.kind == kind);
return [
Padding(
@@ -63,8 +50,7 @@ class LearnScreen extends StatelessWidget {
Text(kind.label,
style: theme.textTheme.titleMedium?.copyWith(color: theme.colorScheme.primary)),
Text(
'${articles.length} articles · last ${_describe(state.windowOf(kind))}'
'${hidden > 0 ? ' · $hidden paid/ads hidden' : ''}',
articleCountLine(state, kind),
style: theme.textTheme.bodySmall?.copyWith(color: muted),
),
],
@@ -92,7 +78,4 @@ class LearnScreen extends StatelessWidget {
),
];
}
static String _describe(Duration window) =>
window.inHours < 72 ? '${window.inHours} h' : '${window.inDays} days';
}