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:
1 parent
246564f1d9
commit
b773c363fe
3 files changed
+65
-51
No files matched your search
@@ -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({
|
||||
|
||||
Reference in new issue
Block a user