feat: add CarouselDetailScreen and implement app configuration fetching during initialization
This commit is contained in:
parent
c32589aba4
commit
e73fc63e87
123
lib/screens/carousel_detail_screen.dart
Normal file
123
lib/screens/carousel_detail_screen.dart
Normal file
@ -0,0 +1,123 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:typed_data';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_html/flutter_html.dart';
|
||||
import '../theme/app_theme.dart';
|
||||
|
||||
/// Detail screen for a tapped carousel slide.
|
||||
/// Displays the banner image (base64 or network URL), title, and rich HTML detail content.
|
||||
class CarouselDetailScreen extends StatelessWidget {
|
||||
final dynamic slide;
|
||||
const CarouselDetailScreen({super.key, required this.slide});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final title = slide['name'] as String? ?? 'Slide Details';
|
||||
final bodyHtml = slide['body'] as String? ?? '';
|
||||
final base64Img = slide['image'] as String?;
|
||||
final externalUrl = slide['image_url'] as String?;
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: Text(title)),
|
||||
body: SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Slide image (full width)
|
||||
_buildSlideImage(base64Img, externalUrl),
|
||||
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(20, 20, 20, 8),
|
||||
child: Text(
|
||||
title,
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
),
|
||||
),
|
||||
|
||||
if (bodyHtml.isNotEmpty)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
child: Html(
|
||||
data: bodyHtml,
|
||||
style: {
|
||||
'body': Style(
|
||||
color: AppTheme.onSurface,
|
||||
fontFamily: 'Manrope',
|
||||
fontSize: FontSize(15),
|
||||
lineHeight: const LineHeight(1.6),
|
||||
),
|
||||
'p': Style(margin: Margins.only(bottom: 12)),
|
||||
'h1': Style(
|
||||
color: AppTheme.onSurface,
|
||||
fontSize: FontSize(22),
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
'h2': Style(
|
||||
color: AppTheme.onSurface,
|
||||
fontSize: FontSize(18),
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
'a': Style(color: AppTheme.secondary),
|
||||
},
|
||||
),
|
||||
)
|
||||
else
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Text(
|
||||
'No content available.',
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 32),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSlideImage(String? base64Img, String? externalUrl) {
|
||||
if (base64Img != null && base64Img.isNotEmpty) {
|
||||
try {
|
||||
final Uint8List bytes = base64Decode(base64Img);
|
||||
return Image.memory(
|
||||
bytes,
|
||||
width: double.infinity,
|
||||
height: 220,
|
||||
fit: BoxFit.cover,
|
||||
);
|
||||
} catch (_) {
|
||||
return _placeholder();
|
||||
}
|
||||
} else if (externalUrl != null && externalUrl.isNotEmpty) {
|
||||
return Image.network(
|
||||
externalUrl,
|
||||
width: double.infinity,
|
||||
height: 220,
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder: (_, __, ___) => _placeholder(),
|
||||
loadingBuilder: (ctx, child, progress) {
|
||||
if (progress == null) return child;
|
||||
return Container(
|
||||
height: 220,
|
||||
color: AppTheme.surfaceContainer,
|
||||
child: const Center(child: CircularProgressIndicator()),
|
||||
);
|
||||
},
|
||||
);
|
||||
} else {
|
||||
return _placeholder();
|
||||
}
|
||||
}
|
||||
|
||||
Widget _placeholder() {
|
||||
return Container(
|
||||
height: 220,
|
||||
color: AppTheme.surfaceContainer,
|
||||
child: const Center(
|
||||
child: Icon(Icons.image_rounded, size: 56, color: AppTheme.outlineVariant),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -23,6 +23,18 @@ class _LoginScreenState extends State<LoginScreen> {
|
||||
final _passwordController = TextEditingController();
|
||||
bool _isLoading = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadAppConfig();
|
||||
}
|
||||
|
||||
Future<void> _loadAppConfig() async {
|
||||
try {
|
||||
await OdooService.getAppConfig();
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
void _login() async {
|
||||
setState(() => _isLoading = true);
|
||||
try {
|
||||
|
||||
@ -42,6 +42,13 @@ class _MainShellState extends State<MainShell> {
|
||||
_notificationTimer = Timer.periodic(const Duration(seconds: 30), (_) {
|
||||
_fetchNotificationCount();
|
||||
});
|
||||
_loadAppConfig();
|
||||
}
|
||||
|
||||
Future<void> _loadAppConfig() async {
|
||||
try {
|
||||
await OdooService.getAppConfig();
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
@override
|
||||
@ -121,8 +128,10 @@ class _MainShellState extends State<MainShell> {
|
||||
Icons.person_rounded,
|
||||
];
|
||||
|
||||
return ListenableBuilder(
|
||||
listenable: ThemeManager.instance,
|
||||
builder: (context, _) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: ThemeManager.instance.brandLogo.isNotEmpty
|
||||
@ -200,5 +209,7 @@ class _MainShellState extends State<MainShell> {
|
||||
}),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -94,9 +94,10 @@ class OdooService {
|
||||
|
||||
/// Fetch app configuration (About Us URL, Contact Us URL, Branding & Theme).
|
||||
static Future<Map<String, String>> getAppConfig() async {
|
||||
final tempClient = OdooClient(AppConfig.odooUrl);
|
||||
final activeClient = OdooService().client;
|
||||
final clientToUse = activeClient ?? OdooClient(AppConfig.odooUrl);
|
||||
try {
|
||||
final res = await tempClient.callRPC('/api/loyalty/app_config', 'call', {});
|
||||
final res = await clientToUse.callRPC('/api/loyalty/app_config', 'call', {});
|
||||
if (res != null && res['status'] == 'success') {
|
||||
final configMap = {
|
||||
'about_us_url': (res['about_us_url'] as String?) ?? '',
|
||||
@ -117,7 +118,9 @@ class OdooService {
|
||||
} catch (_) {
|
||||
return {'about_us_url': '', 'contact_us_url': ''};
|
||||
} finally {
|
||||
tempClient.close();
|
||||
if (activeClient == null) {
|
||||
clientToUse.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -230,9 +233,10 @@ class OdooService {
|
||||
|
||||
/// Fetch public branch information (includes lat/lng for geolocation sorting).
|
||||
static Future<List<dynamic>> getPublicBranches() async {
|
||||
final tempClient = OdooClient(AppConfig.odooUrl);
|
||||
final activeClient = OdooService().client;
|
||||
final clientToUse = activeClient ?? OdooClient(AppConfig.odooUrl);
|
||||
try {
|
||||
final res = await tempClient.callRPC(
|
||||
final res = await clientToUse.callRPC(
|
||||
'/api/loyalty/branches',
|
||||
'call',
|
||||
{}
|
||||
@ -245,7 +249,9 @@ class OdooService {
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
} finally {
|
||||
tempClient.close();
|
||||
if (activeClient == null) {
|
||||
clientToUse.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,8 +2,8 @@ import 'dart:convert';
|
||||
import 'dart:typed_data';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:smooth_page_indicator/smooth_page_indicator.dart';
|
||||
import 'package:url_launcher/url_launcher.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.
|
||||
@ -39,14 +39,11 @@ class _CarouselWidgetState extends State<CarouselWidget> {
|
||||
Future.delayed(const Duration(seconds: 4), _autoScroll);
|
||||
}
|
||||
|
||||
Future<void> _onTap(dynamic slide) async {
|
||||
final linkUrl = slide['link_url'] as String?;
|
||||
if (linkUrl != null && linkUrl.isNotEmpty) {
|
||||
final uri = Uri.tryParse(linkUrl);
|
||||
if (uri != null && await canLaunchUrl(uri)) {
|
||||
await launchUrl(uri, mode: LaunchMode.externalApplication);
|
||||
}
|
||||
}
|
||||
void _onTap(dynamic slide) {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(builder: (_) => CarouselDetailScreen(slide: slide)),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
Loading…
Reference in New Issue
Block a user