import 'dart:ui'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../providers/providers.dart'; import '../../sources/source.dart'; class ComicCover extends ConsumerStatefulWidget { const ComicCover({ super.key, required this.url, this.sourceId, this.width, this.height, this.fit = BoxFit.cover, }); final String? url; final SourceId? sourceId; final double? width; final double? height; final BoxFit fit; @override ConsumerState createState() => _ComicCoverState(); } class _ComicCoverState extends ConsumerState { bool _revealed = false; void _setRevealed(bool value) { if (_revealed == value) return; setState(() => _revealed = value); } @override Widget build(BuildContext context) { final peek = ref.watch(peekModeProvider); final cache = ref.watch(imageCacheManagerProvider); Map headers = const {}; final sid = widget.sourceId; if (sid != null) { final registry = ref.watch(sourceRegistryProvider); if (registry.isEnabled(sid)) { headers = registry.get(sid).imageHeaders; } } if (widget.url == null || widget.url!.isEmpty) { return Container( width: widget.width, height: widget.height, color: Theme.of(context).colorScheme.surfaceContainerHighest, child: const Icon(Icons.menu_book_outlined), ); } final image = CachedNetworkImage( imageUrl: widget.url!, cacheManager: cache, httpHeaders: headers, width: widget.width, height: widget.height, fit: widget.fit, placeholder: (context, url) => Container( width: widget.width, height: widget.height, color: Theme.of(context).colorScheme.surfaceContainerHighest, ), errorWidget: (context, url, error) => Container( width: widget.width, height: widget.height, color: Theme.of(context).colorScheme.surfaceContainerHighest, child: const Icon(Icons.broken_image_outlined), ), ); if (!peek) return image; if (_revealed) { return GestureDetector( behavior: HitTestBehavior.opaque, onLongPressEnd: (_) => _setRevealed(false), onLongPressCancel: () => _setRevealed(false), child: image, ); } // Heavy blur + dark veil so covers are unreadable in public. return GestureDetector( behavior: HitTestBehavior.opaque, onLongPressStart: (_) => _setRevealed(true), child: SizedBox( width: widget.width, height: widget.height, child: Stack( fit: StackFit.expand, children: [ ClipRect( child: ImageFiltered( imageFilter: ImageFilter.blur( sigmaX: 36, sigmaY: 36, tileMode: TileMode.clamp, ), child: image, ), ), ColoredBox( color: Colors.black.withValues(alpha: 0.45), child: const Center( child: Icon( Icons.visibility_off, color: Colors.white54, size: 28, ), ), ), ], ), ), ); } } class PeekModeToggleButton extends ConsumerWidget { const PeekModeToggleButton({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { final peek = ref.watch(peekModeProvider); return IconButton( tooltip: peek ? 'Peek mode on (covers hidden)' : 'Peek mode off', onPressed: () => ref.read(peekModeProvider.notifier).toggle(), icon: Icon(peek ? Icons.visibility_off : Icons.visibility), ); } } class ComicGridTile extends StatelessWidget { const ComicGridTile({ super.key, required this.comic, required this.onTap, this.titleMaxLines = 2, }); final CatalogComic comic; final VoidCallback onTap; final int titleMaxLines; @override Widget build(BuildContext context) { return InkWell( onTap: onTap, borderRadius: BorderRadius.circular(8), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Expanded( child: ClipRRect( borderRadius: BorderRadius.circular(8), child: ComicCover(url: comic.coverUrl, sourceId: comic.source), ), ), const SizedBox(height: 6), Text( comic.title, maxLines: titleMaxLines, overflow: TextOverflow.ellipsis, style: Theme.of(context).textTheme.bodyMedium, ), ], ), ); } }