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();
|
final _passwordController = TextEditingController();
|
||||||
bool _isLoading = false;
|
bool _isLoading = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_loadAppConfig();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _loadAppConfig() async {
|
||||||
|
try {
|
||||||
|
await OdooService.getAppConfig();
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
|
||||||
void _login() async {
|
void _login() async {
|
||||||
setState(() => _isLoading = true);
|
setState(() => _isLoading = true);
|
||||||
try {
|
try {
|
||||||
|
|||||||
@ -42,6 +42,13 @@ class _MainShellState extends State<MainShell> {
|
|||||||
_notificationTimer = Timer.periodic(const Duration(seconds: 30), (_) {
|
_notificationTimer = Timer.periodic(const Duration(seconds: 30), (_) {
|
||||||
_fetchNotificationCount();
|
_fetchNotificationCount();
|
||||||
});
|
});
|
||||||
|
_loadAppConfig();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _loadAppConfig() async {
|
||||||
|
try {
|
||||||
|
await OdooService.getAppConfig();
|
||||||
|
} catch (_) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -121,8 +128,10 @@ class _MainShellState extends State<MainShell> {
|
|||||||
Icons.person_rounded,
|
Icons.person_rounded,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
return ListenableBuilder(
|
||||||
|
listenable: ThemeManager.instance,
|
||||||
|
builder: (context, _) {
|
||||||
final colorScheme = Theme.of(context).colorScheme;
|
final colorScheme = Theme.of(context).colorScheme;
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: ThemeManager.instance.brandLogo.isNotEmpty
|
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).
|
/// Fetch app configuration (About Us URL, Contact Us URL, Branding & Theme).
|
||||||
static Future<Map<String, String>> getAppConfig() async {
|
static Future<Map<String, String>> getAppConfig() async {
|
||||||
final tempClient = OdooClient(AppConfig.odooUrl);
|
final activeClient = OdooService().client;
|
||||||
|
final clientToUse = activeClient ?? OdooClient(AppConfig.odooUrl);
|
||||||
try {
|
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') {
|
if (res != null && res['status'] == 'success') {
|
||||||
final configMap = {
|
final configMap = {
|
||||||
'about_us_url': (res['about_us_url'] as String?) ?? '',
|
'about_us_url': (res['about_us_url'] as String?) ?? '',
|
||||||
@ -117,7 +118,9 @@ class OdooService {
|
|||||||
} catch (_) {
|
} catch (_) {
|
||||||
return {'about_us_url': '', 'contact_us_url': ''};
|
return {'about_us_url': '', 'contact_us_url': ''};
|
||||||
} finally {
|
} finally {
|
||||||
tempClient.close();
|
if (activeClient == null) {
|
||||||
|
clientToUse.close();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -230,9 +233,10 @@ class OdooService {
|
|||||||
|
|
||||||
/// Fetch public branch information (includes lat/lng for geolocation sorting).
|
/// Fetch public branch information (includes lat/lng for geolocation sorting).
|
||||||
static Future<List<dynamic>> getPublicBranches() async {
|
static Future<List<dynamic>> getPublicBranches() async {
|
||||||
final tempClient = OdooClient(AppConfig.odooUrl);
|
final activeClient = OdooService().client;
|
||||||
|
final clientToUse = activeClient ?? OdooClient(AppConfig.odooUrl);
|
||||||
try {
|
try {
|
||||||
final res = await tempClient.callRPC(
|
final res = await clientToUse.callRPC(
|
||||||
'/api/loyalty/branches',
|
'/api/loyalty/branches',
|
||||||
'call',
|
'call',
|
||||||
{}
|
{}
|
||||||
@ -245,7 +249,9 @@ class OdooService {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
rethrow;
|
rethrow;
|
||||||
} finally {
|
} finally {
|
||||||
tempClient.close();
|
if (activeClient == null) {
|
||||||
|
clientToUse.close();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,8 +2,8 @@ import 'dart:convert';
|
|||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:smooth_page_indicator/smooth_page_indicator.dart';
|
import 'package:smooth_page_indicator/smooth_page_indicator.dart';
|
||||||
import 'package:url_launcher/url_launcher.dart';
|
|
||||||
import '../theme/app_theme.dart';
|
import '../theme/app_theme.dart';
|
||||||
|
import '../screens/carousel_detail_screen.dart';
|
||||||
|
|
||||||
/// Auto-scrolling carousel widget that shows slides from CMS.
|
/// Auto-scrolling carousel widget that shows slides from CMS.
|
||||||
/// Each slide can have an uploaded image (base64) or external image URL.
|
/// 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.delayed(const Duration(seconds: 4), _autoScroll);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _onTap(dynamic slide) async {
|
void _onTap(dynamic slide) {
|
||||||
final linkUrl = slide['link_url'] as String?;
|
Navigator.push(
|
||||||
if (linkUrl != null && linkUrl.isNotEmpty) {
|
context,
|
||||||
final uri = Uri.tryParse(linkUrl);
|
MaterialPageRoute(builder: (_) => CarouselDetailScreen(slide: slide)),
|
||||||
if (uri != null && await canLaunchUrl(uri)) {
|
);
|
||||||
await launchUrl(uri, mode: LaunchMode.externalApplication);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user