import 'dart:convert'; import 'dart:typed_data'; import 'package:flutter/material.dart'; import 'package:smooth_page_indicator/smooth_page_indicator.dart'; import '../screens/carousel_detail_screen.dart'; import '../utils/safe_cast.dart'; /// Auto-scrolling carousel widget that shows slides from CMS. /// Each slide can have an uploaded image (base64) or external image URL. class CarouselWidget extends StatefulWidget { final List slides; const CarouselWidget({super.key, required this.slides}); @override State createState() => _CarouselWidgetState(); } class _CarouselWidgetState extends State { final PageController _controller = PageController(); int _current = 0; @override void initState() { super.initState(); if (widget.slides.length > 1) { Future.delayed(const Duration(seconds: 4), _autoScroll); } } void _autoScroll() { if (!mounted) return; final next = (_current + 1) % widget.slides.length; _controller.animateToPage( next, duration: const Duration(milliseconds: 500), curve: Curves.easeInOut, ); Future.delayed(const Duration(seconds: 4), _autoScroll); } void _onTap(dynamic slide) { Navigator.push( context, MaterialPageRoute(builder: (_) => CarouselDetailScreen(slide: slide)), ); } @override void dispose() { _controller.dispose(); super.dispose(); } @override Widget build(BuildContext context) { if (widget.slides.isEmpty) return const SizedBox.shrink(); final theme = Theme.of(context); final colorScheme = theme.colorScheme; return Column( children: [ SizedBox( height: 180, child: PageView.builder( controller: _controller, itemCount: widget.slides.length, onPageChanged: (i) => setState(() => _current = i), itemBuilder: (context, index) { final slide = widget.slides[index]; return GestureDetector( onTap: () => _onTap(slide), child: _SlideImage(slide: slide), ); }, ), ), if (widget.slides.length > 1) Padding( padding: const EdgeInsets.only(top: 10), child: SmoothPageIndicator( controller: _controller, count: widget.slides.length, effect: ExpandingDotsEffect( dotHeight: 6, dotWidth: 6, expansionFactor: 3, dotColor: colorScheme.surfaceContainer, activeDotColor: colorScheme.secondary, ), ), ), ], ); } } class _SlideImage extends StatelessWidget { final dynamic slide; const _SlideImage({required this.slide}); @override Widget build(BuildContext context) { final theme = Theme.of(context); final colorScheme = theme.colorScheme; final base64Img = safeString(slide['image']); final externalUrl = safeString(slide['image_url']); Widget image; if (base64Img != null && base64Img.isNotEmpty) { // Uploaded image — decode base64 try { final Uint8List bytes = base64Decode(base64Img); image = Image.memory(bytes, fit: BoxFit.cover, width: double.infinity); } catch (_) { image = _placeholder(colorScheme); } } else if (externalUrl != null && externalUrl.isNotEmpty) { // External URL image image = Image.network( externalUrl, fit: BoxFit.cover, width: double.infinity, errorBuilder: (_, _, _) => _placeholder(colorScheme), loadingBuilder: (ctx, child, progress) { if (progress == null) return child; return const Center(child: CircularProgressIndicator()); }, ); } else { image = _placeholder(colorScheme); } return Container( margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), decoration: BoxDecoration( color: colorScheme.surfaceContainerLow, borderRadius: BorderRadius.circular(16), boxShadow: [ BoxShadow( color: Colors.black.withValues(alpha: 0.05), blurRadius: 10, offset: const Offset(0, 4), ), ], ), child: ClipRRect(borderRadius: BorderRadius.circular(16), child: image), ); } Widget _placeholder(ColorScheme colorScheme) { return Container( color: colorScheme.surfaceContainer, child: Center( child: Icon(Icons.image_rounded, size: 48, color: colorScheme.outline), ), ); } }