import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import 'package:worldhopper/l10n/app_localizations.dart'; import 'package:worldhopper/config/constants.dart'; import 'package:worldhopper/models/opds_entry.dart'; import 'package:worldhopper/providers/connectivity_provider.dart'; import 'package:worldhopper/providers/series_navigation_provider.dart'; /// Overlay widget shown when reading is complete. /// /// - If a next entry exists in the series feed, shows "Next: [title]". /// - If there is no next entry (last in series or no feed context), /// shows "Finished" with a link back to the OPDS entry (online) /// or the library (offline). /// /// Can be dismissed by swiping down or tapping the close button. class NextInSeriesOverlay extends ConsumerStatefulWidget { final String serverId; final String? feedUrl; final OPDSEntry entry; final bool isVisible; final bool einkMode; final Future Function()? onBeforeNavigate; const NextInSeriesOverlay({ super.key, required this.serverId, this.feedUrl, required this.entry, required this.isVisible, this.einkMode = false, this.onBeforeNavigate, }); @override ConsumerState createState() => _NextInSeriesOverlayState(); } class _NextInSeriesOverlayState extends ConsumerState { bool _dismissed = false; @override Widget build(BuildContext context) { if (_dismissed || !widget.isVisible) { return const SizedBox.shrink(); } // If we have a feed URL, try to resolve the next entry if (widget.feedUrl != null) { final request = NextInSeriesRequest( serverId: widget.serverId, feedUrl: widget.feedUrl!, currentEntryId: widget.entry.id, ); final nextEntryAsync = ref.watch(nextInSeriesProvider(request)); return nextEntryAsync.when( data: (nextEntry) { if (nextEntry != null) { return _buildNextOverlay(context, nextEntry); } return _buildFinishedOverlay(context); }, loading: () => const SizedBox.shrink(), error: (_, __) => _buildFinishedOverlay(context), ); } // No feed context — just show "Finished" return _buildFinishedOverlay(context); } // --------------------------------------------------------------------------- // "Next in series" bar // --------------------------------------------------------------------------- Widget _buildNextOverlay(BuildContext context, OPDSEntry nextEntry) { return _buildDismissibleBar( key: 'next-in-series-${nextEntry.id}', color: Theme.of(context).colorScheme.primaryContainer, onColor: Theme.of(context).colorScheme.onPrimaryContainer, onTap: () => _navigateToNext(context, nextEntry), icon: Icons.skip_next, label: AppLocalizations.of(context).nextInSeriesNext, title: nextEntry.title, trailingIcon: Icons.arrow_forward, ); } // --------------------------------------------------------------------------- // "Finished reading" bar // --------------------------------------------------------------------------- Widget _buildFinishedOverlay(BuildContext context) { final l10n = AppLocalizations.of(context); final connectivityState = ref.watch(connectivityStateProvider); final isOnline = connectivityState.whenOrNull( data: (online) => online, ) ?? true; return _buildDismissibleBar( key: 'finished-reading-${widget.entry.id}', color: Theme.of(context).colorScheme.secondaryContainer, onColor: Theme.of(context).colorScheme.onSecondaryContainer, onTap: () => _navigateBack(context, isOnline), icon: Icons.check_circle_outline, label: l10n.nextInSeriesFinished, title: isOnline ? l10n.nextInSeriesBackToDetails : l10n.nextInSeriesBackToLibrary, trailingIcon: Icons.arrow_back, ); } // --------------------------------------------------------------------------- // Shared bar builder // --------------------------------------------------------------------------- Widget _buildDismissibleBar({ required String key, required Color color, required Color onColor, required VoidCallback onTap, required IconData icon, required String label, required String title, required IconData trailingIcon, }) { final barContent = Material( elevation: 8, borderRadius: BorderRadius.circular(12), color: color, child: InkWell( borderRadius: BorderRadius.circular(12), onTap: onTap, child: Padding( padding: const EdgeInsets.only(left: 16, top: 12, bottom: 12, right: 4), child: Row( children: [ Icon(icon, color: onColor), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ Text( label, style: Theme.of(context).textTheme.labelSmall?.copyWith( color: onColor.withValues(alpha: 0.7), ), ), Text( title, style: Theme.of(context).textTheme.titleSmall?.copyWith( color: onColor, fontWeight: FontWeight.bold, ), maxLines: 1, overflow: TextOverflow.ellipsis, ), ], ), ), const SizedBox(width: 4), Icon(trailingIcon, color: onColor), SizedBox( width: 36, height: 36, child: IconButton( padding: EdgeInsets.zero, iconSize: 20, icon: Icon( Icons.close, color: onColor.withValues(alpha: 0.7), ), onPressed: () { setState(() { _dismissed = true; }); }, ), ), ], ), ), ), ); return Positioned( bottom: MediaQuery.of(context).padding.bottom + 16, left: 16, right: 16, child: widget.einkMode ? barContent : Dismissible( key: ValueKey(key), direction: DismissDirection.down, onDismissed: (_) { setState(() { _dismissed = true; }); }, child: barContent, ), ); } // --------------------------------------------------------------------------- // Navigation helpers // --------------------------------------------------------------------------- void _navigateToNext(BuildContext context, OPDSEntry nextEntry) async { await widget.onBeforeNavigate?.call(); if (!context.mounted) return; if (nextEntry.isEpub) { context.pushReplacement( AppConstants.routeEpubReader, extra: { 'entry': nextEntry, 'serverId': widget.serverId, 'feedUrl': widget.feedUrl, }, ); } else if (nextEntry.hasStreamLink) { context.pushReplacement( AppConstants.routeReader, extra: { 'entry': nextEntry, 'serverId': widget.serverId, 'initialPage': 0, 'feedUrl': widget.feedUrl, }, ); } else { context.pushReplacement( AppConstants.routePublicationDetail, extra: { 'entry': nextEntry, 'serverId': widget.serverId, 'feedUrl': widget.feedUrl, }, ); } } void _navigateBack(BuildContext context, bool isOnline) async { await widget.onBeforeNavigate?.call(); if (!context.mounted) return; if (isOnline) { // Go to the publication detail screen for the current entry context.pushReplacement( AppConstants.routePublicationDetail, extra: { 'entry': widget.entry, 'serverId': widget.serverId, 'feedUrl': widget.feedUrl, }, ); } else { // Go to the library context.go(AppConstants.routeLibrary); } } }