197 lines
6.9 KiB
Dart
197 lines
6.9 KiB
Dart
import 'dart:typed_data';
|
|
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
|
import 'package:syncfusion_flutter_pdfviewer/pdfviewer.dart';
|
|
|
|
import '../../../../api/errors/error_mapper.dart';
|
|
import '../../../../api/marianumconnect/queries/get_ticker_page/get_ticker_page_response.dart';
|
|
import '../../../../routing/app_routes.dart';
|
|
import '../../../../state/app/infrastructure/loadable_state/loadable_state.dart';
|
|
import '../../../../state/app/infrastructure/loadable_state/view/loadable_state_consumer.dart';
|
|
import '../../../../state/app/infrastructure/utility_widgets/bloc_module.dart';
|
|
import '../../../../state/app/modules/ticker/bloc/ticker_page_bloc.dart';
|
|
import '../../../../state/app/modules/ticker/bloc/ticker_page_state.dart';
|
|
import '../../../../state/app/modules/ticker/repository/ticker_page_repository.dart';
|
|
import '../../../../theming/app_theme.dart';
|
|
import '../../../../widget/app_progress_indicator.dart';
|
|
import '../../../../widget/placeholder_view.dart';
|
|
import '../../../../widget/prosemirror/pm_json_view.dart';
|
|
import '../../../../widget/route_transition_gate.dart';
|
|
import 'ticker_content_card.dart';
|
|
import 'ticker_updated_bar.dart';
|
|
|
|
/// Embeddable renderer for a single ticker page: drives a per-slug
|
|
/// [TickerPageBloc] through [LoadableStateConsumer], so pages behave like the
|
|
/// home surface (cache, background refresh, offline banner, pull-to-refresh).
|
|
/// Carries no Scaffold so it works both in-place in [TickerView] and in the
|
|
/// standalone `TickerPageView`. A REDIRECT opens the browser and invokes
|
|
/// [onRedirect] so the host can leave this page.
|
|
class TickerPageBody extends StatelessWidget {
|
|
final String slug;
|
|
final void Function(String href) onLinkTap;
|
|
final VoidCallback? onRedirect;
|
|
|
|
const TickerPageBody({
|
|
super.key,
|
|
required this.slug,
|
|
required this.onLinkTap,
|
|
this.onRedirect,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) =>
|
|
BlocModule<TickerPageBloc, LoadableState<TickerPageState>>(
|
|
// A slug switch must rebuild the provider with a fresh bloc.
|
|
key: ValueKey(slug),
|
|
create: (context) => TickerPageBloc(slug),
|
|
child: (context, bloc, _) =>
|
|
LoadableStateConsumer<TickerPageBloc, TickerPageState>(
|
|
isReady: (state) => state.page != null,
|
|
child: (state, loading) => _TickerPageContent(
|
|
page: state.page!,
|
|
onLinkTap: onLinkTap,
|
|
onRedirect: onRedirect,
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
/// Renders a resolved [TickerPageResponse] by kind. Stateful so a REDIRECT
|
|
/// fires only once across the consumer's background-refresh rebuilds.
|
|
class _TickerPageContent extends StatefulWidget {
|
|
final TickerPageResponse page;
|
|
final void Function(String href) onLinkTap;
|
|
final VoidCallback? onRedirect;
|
|
|
|
const _TickerPageContent({
|
|
required this.page,
|
|
required this.onLinkTap,
|
|
this.onRedirect,
|
|
});
|
|
|
|
@override
|
|
State<_TickerPageContent> createState() => _TickerPageContentState();
|
|
}
|
|
|
|
class _TickerPageContentState extends State<_TickerPageContent> {
|
|
bool _redirected = false;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final page = widget.page;
|
|
switch (page.kind) {
|
|
case TickerPageKind.redirect:
|
|
final url = page.externalUrl;
|
|
if (!_redirected) {
|
|
_redirected = true;
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
if (!mounted) return;
|
|
if (url != null && url.isNotEmpty) AppRoutes.openExternalUrl(url);
|
|
widget.onRedirect?.call();
|
|
});
|
|
}
|
|
return const Center(child: AppProgressIndicator.large());
|
|
case TickerPageKind.proxiedFile:
|
|
return Column(
|
|
children: [
|
|
// For proxied files the data currency is the last successful
|
|
// proxy fetch, not the page's publish date.
|
|
TickerUpdatedBar(publishedAt: page.fileFetchedAt ?? page.publishedAt),
|
|
Expanded(
|
|
child: _ProxiedFileView(
|
|
repo: context.read<TickerPageBloc>().repo,
|
|
slug: page.slug ?? context.read<TickerPageBloc>().slug,
|
|
),
|
|
),
|
|
],
|
|
);
|
|
default:
|
|
final content = page.content;
|
|
if (content == null) {
|
|
return PlaceholderView(
|
|
icon: Icons.public_off_outlined,
|
|
text: 'Dieser Inhalt ist in der App nicht verfügbar.',
|
|
button: page.webUrl == null
|
|
? null
|
|
: ElevatedButton.icon(
|
|
onPressed: () => AppRoutes.openWebUrl(page.webUrl!),
|
|
icon: const Icon(Icons.open_in_new),
|
|
label: const Text('Im Browser öffnen'),
|
|
),
|
|
);
|
|
}
|
|
return SingleChildScrollView(
|
|
// Pull-to-refresh must trigger even when content fits the viewport.
|
|
physics: const AlwaysScrollableScrollPhysics(),
|
|
padding: const EdgeInsets.only(bottom: AppSpacing.lg),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
children: [
|
|
TickerUpdatedBar(publishedAt: page.publishedAt),
|
|
TickerContentCard(
|
|
child: PmJsonView(json: content, onLinkTap: widget.onLinkTap),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
class _ProxiedFileView extends StatefulWidget {
|
|
final TickerPageRepository repo;
|
|
final String slug;
|
|
|
|
const _ProxiedFileView({required this.repo, required this.slug});
|
|
|
|
@override
|
|
State<_ProxiedFileView> createState() => _ProxiedFileViewState();
|
|
}
|
|
|
|
class _ProxiedFileViewState extends State<_ProxiedFileView> {
|
|
late Future<Uint8List> _bytes;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_bytes = widget.repo.getPageFile(widget.slug);
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) => FutureBuilder<Uint8List>(
|
|
future: _bytes,
|
|
builder: (context, snapshot) {
|
|
if (snapshot.connectionState != ConnectionState.done) {
|
|
return const Center(child: AppProgressIndicator.large());
|
|
}
|
|
final error = snapshot.error;
|
|
if (error != null) {
|
|
return PlaceholderView(
|
|
icon: Icons.error_outline,
|
|
text: errorToUserMessage(error),
|
|
button: errorAllowsRetry(error)
|
|
? ElevatedButton.icon(
|
|
onPressed: () => setState(() {
|
|
_bytes = widget.repo.getPageFile(widget.slug);
|
|
}),
|
|
icon: const Icon(Icons.refresh),
|
|
label: const Text('Erneut versuchen'),
|
|
)
|
|
: null,
|
|
);
|
|
}
|
|
final bytes = snapshot.data!;
|
|
if (bytes.isEmpty) {
|
|
return const PlaceholderView(
|
|
icon: Icons.picture_as_pdf_outlined,
|
|
text: 'Das Dokument konnte nicht geladen werden.',
|
|
);
|
|
}
|
|
return RouteTransitionGate(
|
|
builder: (context) => SfPdfViewer.memory(bytes),
|
|
);
|
|
},
|
|
);
|
|
}
|