odoo_loyalty_app/lib/widgets/carousel_widget.dart

161 lines
4.6 KiB
Dart

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<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();
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),
),
);
}
}