odoo_loyalty_app/lib/widgets/carousel_widget.dart

147 lines
4.0 KiB
Dart

import 'dart:convert';
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:smooth_page_indicator/smooth_page_indicator.dart';
import '../theme/app_theme.dart';
import '../screens/carousel_detail_screen.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<dynamic> slides;
const CarouselWidget({super.key, required this.slides});
@override
State<CarouselWidget> createState() => _CarouselWidgetState();
}
class _CarouselWidgetState extends State<CarouselWidget> {
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();
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: AppTheme.surfaceContainer,
activeDotColor: AppTheme.secondary,
),
),
),
],
);
}
}
class _SlideImage extends StatelessWidget {
final dynamic slide;
const _SlideImage({required this.slide});
@override
Widget build(BuildContext context) {
final base64Img = slide['image'] as String?;
final externalUrl = slide['image_url'] as String?;
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();
}
} else if (externalUrl != null && externalUrl.isNotEmpty) {
// External URL image
image = Image.network(
externalUrl,
fit: BoxFit.cover,
width: double.infinity,
errorBuilder: (_, __, ___) => _placeholder(),
loadingBuilder: (ctx, child, progress) {
if (progress == null) return child;
return const Center(child: CircularProgressIndicator());
},
);
} else {
image = _placeholder();
}
return Container(
margin: const EdgeInsets.symmetric(horizontal: 16),
decoration: const BoxDecoration(color: AppTheme.surfaceContainerLow),
child: ClipRect(child: image),
);
}
Widget _placeholder() {
return Container(
color: AppTheme.surfaceContainer,
child: const Center(
child: Icon(Icons.image_rounded, size: 48, color: AppTheme.outlineVariant),
),
);
}
}