add ticker page bloc and avatar disk cache

This commit is contained in:
2026-07-12 23:18:53 +02:00
parent 94794ff092
commit 9b5198c6db
18 changed files with 944 additions and 233 deletions
+6 -70
View File
@@ -2,7 +2,6 @@ import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:intl/intl.dart';
import '../../../api/marianumconnect/queries/get_ticker/get_ticker_response.dart';
import '../../../api/marianumconnect/queries/get_ticker_nav/get_ticker_nav_response.dart';
@@ -21,6 +20,7 @@ import '../../../widget/prosemirror/pm_json_view.dart';
import 'widgets/ticker_content_card.dart';
import 'widgets/ticker_nav_list.dart';
import 'widgets/ticker_page_body.dart';
import 'widgets/ticker_updated_bar.dart';
/// Ticker module entry. Wires the [TickerBloc] to the presentation
/// [TickerScaffold]; the "Aktuelles" home surface is driven through the loadable
@@ -46,8 +46,6 @@ class TickerView extends StatelessWidget {
onSelectionChanged: (slug) => bloc.add(
Emit<TickerState>((state) => state.copyWith(selectedSlug: slug)),
),
homePublishedAt: data?.ticker?.publishedAt,
onRefreshHome: bloc.retry,
homeBuilder: (context, onLinkTap) =>
LoadableStateConsumer<TickerBloc, TickerState>(
child: (state, loading) => _TickerHome(
@@ -93,14 +91,6 @@ class TickerScaffold extends StatefulWidget {
/// it (null = home). Also fired when a stale [initialSlug] is discarded.
final void Function(String? slug)? onSelectionChanged;
/// ISO publish timestamp of the current "Aktuelles" post, shown as a dated
/// refresh button in the app bar while the home surface is open. Null hides
/// the button (no post / no date / on a page, which carries no date).
final String? homePublishedAt;
/// Tapped from the app bar's dated button to reload the ticker.
final VoidCallback? onRefreshHome;
const TickerScaffold({
super.key,
required this.sections,
@@ -108,8 +98,6 @@ class TickerScaffold extends StatefulWidget {
this.pageBuilder,
this.initialSlug,
this.onSelectionChanged,
this.homePublishedAt,
this.onRefreshHome,
});
static const double sidebarBreakpoint = 900;
@@ -326,22 +314,6 @@ class _TickerScaffoldState extends State<TickerScaffold> {
appBar: AppBar(
title: Text(slug == null ? 'Ticker' : (_selectedTitle ?? 'Ticker')),
actions: [
// The "Aktuelles" post's date, shown only on the home surface
// (pages carry no date). Tapping reloads the ticker.
if (slug == null)
Builder(
builder: (context) {
final updatedAt = _formatPublishedAt(
context,
widget.homePublishedAt,
);
if (updatedAt == null) return const SizedBox.shrink();
return _UpdatedAtButton(
text: updatedAt,
onTap: widget.onRefreshHome,
);
},
),
// Always present so "Aktuelles" is a fixed anchor; greyed out
// (disabled) while it is the current surface.
IconButton(
@@ -415,8 +387,12 @@ class _TickerHome extends StatelessWidget {
}
return ListView(
physics: const AlwaysScrollableScrollPhysics(),
padding: const EdgeInsets.only(bottom: AppSpacing.lg),
children: [_CurrentTickerCard(ticker: ticker, onLinkTap: onLinkTap)],
children: [
TickerUpdatedBar(publishedAt: ticker.publishedAt),
_CurrentTickerCard(ticker: ticker, onLinkTap: onLinkTap),
],
);
}
}
@@ -440,46 +416,6 @@ class _CurrentTickerCard extends StatelessWidget {
}
}
/// App bar button showing when the "Aktuelles" post was last updated; tapping
/// reloads the ticker. Styled to sit on the app bar (onSurface foreground) with
/// a clock icon and the full date in a tooltip.
class _UpdatedAtButton extends StatelessWidget {
final String text;
final VoidCallback? onTap;
const _UpdatedAtButton({required this.text, this.onTap});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Padding(
padding: const EdgeInsets.symmetric(horizontal: AppSpacing.xs),
child: Tooltip(
message: 'Aktualisiert am $text',
child: TextButton.icon(
onPressed: onTap,
icon: const Icon(Icons.schedule, size: 16),
label: Text(text),
style: TextButton.styleFrom(
foregroundColor: theme.colorScheme.onSurface,
textStyle: theme.textTheme.labelMedium,
),
),
),
);
}
}
/// Formats the ISO `publishedAt` as `dd.MM.yyyy, HH:mm` in the device locale, or
/// null when it is missing/unparseable so callers can drop the line entirely.
String? _formatPublishedAt(BuildContext context, String? iso) {
if (iso == null || iso.isEmpty) return null;
final parsed = DateTime.tryParse(iso);
if (parsed == null) return null;
final locale = Localizations.localeOf(context).toString();
return DateFormat('dd.MM.yyyy, HH:mm', locale).format(parsed.toLocal());
}
class _UnavailableHint extends StatelessWidget {
final String webUrl;