refactor: resolve all hardcoded color references to support dynamic branding colors from backend via Theme.of(context)

This commit is contained in:
Suherdy Yacob 2026-06-14 11:09:57 +07:00
parent f092a77b0f
commit 370099930d
6 changed files with 97 additions and 84 deletions

View File

@ -153,6 +153,9 @@ class _BranchesScreenState extends State<BranchesScreen> {
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final colorScheme = theme.colorScheme;
return _isLoading
? const Center(child: CircularProgressIndicator())
: RefreshIndicator(
@ -163,17 +166,17 @@ class _BranchesScreenState extends State<BranchesScreen> {
if (_locationDenied)
Container(
width: double.infinity,
color: AppTheme.surfaceContainerLow,
color: colorScheme.surfaceContainerLow,
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
child: Row(
children: [
const Icon(Icons.location_off, size: 16, color: AppTheme.onSurfaceVariant),
Icon(Icons.location_off, size: 16, color: colorScheme.onSurfaceVariant),
const SizedBox(width: 8),
Expanded(
child: Text(
'Location not available. Showing branches alphabetically.',
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: AppTheme.onSurfaceVariant,
style: theme.textTheme.bodySmall?.copyWith(
color: colorScheme.onSurfaceVariant,
),
),
),
@ -183,16 +186,16 @@ class _BranchesScreenState extends State<BranchesScreen> {
else if (_userPosition != null)
Container(
width: double.infinity,
color: AppTheme.surfaceContainerLow,
color: colorScheme.surfaceContainerLow,
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
child: Row(
children: [
const Icon(Icons.my_location, size: 16, color: AppTheme.secondary),
Icon(Icons.my_location, size: 16, color: colorScheme.secondary),
const SizedBox(width: 8),
Text(
'Sorted by distance from your location',
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: AppTheme.onSurfaceVariant,
style: theme.textTheme.bodySmall?.copyWith(
color: colorScheme.onSurfaceVariant,
),
),
],
@ -234,7 +237,7 @@ class _BranchesScreenState extends State<BranchesScreen> {
return Container(
margin: const EdgeInsets.only(bottom: 12),
decoration: BoxDecoration(
color: AppTheme.surfaceContainerLow,
color: colorScheme.surfaceContainerLow,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
@ -251,18 +254,18 @@ class _BranchesScreenState extends State<BranchesScreen> {
leading: Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: AppTheme.secondaryContainer,
color: colorScheme.secondaryContainer,
borderRadius: BorderRadius.circular(12),
),
child: const Icon(Icons.storefront,
color: AppTheme.secondary),
child: Icon(Icons.storefront,
color: colorScheme.secondary),
),
title: Row(
children: [
Expanded(
child: Text(
branch['name'] ?? 'Mapan Branch',
style: Theme.of(context)
style: theme
.textTheme
.titleMedium
?.copyWith(
@ -277,16 +280,16 @@ class _BranchesScreenState extends State<BranchesScreen> {
padding: const EdgeInsets.symmetric(
horizontal: 8, vertical: 3),
decoration: BoxDecoration(
color: AppTheme.secondaryContainer,
color: colorScheme.secondaryContainer,
borderRadius: BorderRadius.circular(12),
),
child: Text(
distanceLabel,
style: Theme.of(context)
style: theme
.textTheme
.labelLarge
?.copyWith(
color: AppTheme.onSecondaryContainer,
color: colorScheme.onSecondaryContainer,
fontSize: 10,
fontWeight: FontWeight.bold,
),
@ -303,22 +306,22 @@ class _BranchesScreenState extends State<BranchesScreen> {
addressParts.isEmpty
? 'No address specified'
: addressParts,
style: Theme.of(context).textTheme.bodyMedium,
style: theme.textTheme.bodyMedium,
),
if (phone.isNotEmpty) ...[
const SizedBox(height: 4),
Row(
children: [
const Icon(Icons.phone,
Icon(Icons.phone,
size: 14,
color: AppTheme.onSurfaceVariant),
color: colorScheme.onSurfaceVariant),
const SizedBox(width: 4),
Text(phone,
style: Theme.of(context)
style: theme
.textTheme
.bodySmall
?.copyWith(
color: AppTheme.onSurfaceVariant)),
color: colorScheme.onSurfaceVariant)),
],
),
]
@ -327,13 +330,13 @@ class _BranchesScreenState extends State<BranchesScreen> {
),
trailing: phone.isNotEmpty
? IconButton(
icon: const Icon(Icons.chat_bubble,
color: AppTheme.onSurface),
icon: Icon(Icons.chat_bubble,
color: colorScheme.onSurface),
onPressed: () => _launchWhatsApp(phone),
tooltip: 'Chat on WhatsApp',
)
: const Icon(Icons.chevron_right,
color: AppTheme.onSurfaceVariant),
: Icon(Icons.chevron_right,
color: colorScheme.onSurfaceVariant),
),
);
},

View File

@ -1,6 +1,5 @@
import 'package:flutter/material.dart';
import '../services/odoo_service.dart';
import '../theme/app_theme.dart';
import '../widgets/carousel_widget.dart';
import '../widgets/promo_card_widget.dart';
import '../widgets/subscription_list_widget.dart';
@ -130,20 +129,25 @@ class _LoyaltyCardTile extends StatelessWidget {
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final colorScheme = theme.colorScheme;
final programName = (card['program_id']?[1] as String? ?? '').toLowerCase();
String tier = 'MEMBER';
if (programName.contains('silver')) tier = 'SILVER MEMBER';
if (programName.contains('gold')) tier = 'GOLD MEMBER';
if (programName.contains('platinum')) tier = 'PLATINUM MEMBER';
final onPrimary = colorScheme.onPrimary;
final accentColor = colorScheme.secondary;
return Container(
margin: const EdgeInsets.fromLTRB(16, 16, 16, 8),
padding: const EdgeInsets.all(24),
decoration: BoxDecoration(
color: AppTheme.primary, // Rich brick red background
color: colorScheme.primary,
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: const Color(0xFFF3DCA2), // Golden border
color: accentColor.withValues(alpha: 0.5),
width: 1.5,
),
boxShadow: [
@ -163,8 +167,8 @@ class _LoyaltyCardTile extends StatelessWidget {
Expanded(
child: Text(
'${card['program_id']?[1] ?? 'Loyalty Program'}',
style: Theme.of(context).textTheme.titleLarge?.copyWith(
color: Colors.white,
style: theme.textTheme.titleLarge?.copyWith(
color: onPrimary,
fontFamily: 'serif',
),
softWrap: true,
@ -174,13 +178,13 @@ class _LoyaltyCardTile extends StatelessWidget {
Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: const Color(0xFFB58428), // Golden honey badge background
color: accentColor,
borderRadius: BorderRadius.circular(20),
),
child: Text(
tier,
style: Theme.of(context).textTheme.labelLarge?.copyWith(
color: Colors.white,
style: theme.textTheme.labelLarge?.copyWith(
color: colorScheme.primary.computeLuminance() > 0.5 ? Colors.black : Colors.white,
fontWeight: FontWeight.bold,
fontSize: 9,
letterSpacing: 0.8,
@ -192,8 +196,8 @@ class _LoyaltyCardTile extends StatelessWidget {
const SizedBox(height: 24),
Text(
'MEMBERSHIP CODE',
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Colors.white.withValues(alpha: 0.7),
style: theme.textTheme.bodySmall?.copyWith(
color: onPrimary.withValues(alpha: 0.7),
fontWeight: FontWeight.bold,
fontSize: 10,
letterSpacing: 1.0,
@ -202,8 +206,8 @@ class _LoyaltyCardTile extends StatelessWidget {
const SizedBox(height: 4),
Text(
'${card['code'] ?? 'N/A'}',
style: Theme.of(context).textTheme.titleMedium?.copyWith(
color: Colors.white,
style: theme.textTheme.titleMedium?.copyWith(
color: onPrimary,
fontFamily: 'monospace',
fontSize: 16,
fontWeight: FontWeight.bold,
@ -219,8 +223,8 @@ class _LoyaltyCardTile extends StatelessWidget {
children: [
Text(
'AVAILABLE POINTS',
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Colors.white.withValues(alpha: 0.7),
style: theme.textTheme.bodySmall?.copyWith(
color: onPrimary.withValues(alpha: 0.7),
fontWeight: FontWeight.bold,
fontSize: 10,
letterSpacing: 1.0,
@ -229,16 +233,16 @@ class _LoyaltyCardTile extends StatelessWidget {
const SizedBox(height: 4),
Row(
children: [
const Icon(
Icon(
Icons.restaurant_rounded,
color: Color(0xFFF3DCA2),
color: accentColor,
size: 16,
),
const SizedBox(width: 6),
Text(
'Dine & Save',
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Colors.white.withValues(alpha: 0.9),
style: theme.textTheme.bodyMedium?.copyWith(
color: onPrimary.withValues(alpha: 0.9),
fontStyle: FontStyle.italic,
),
),
@ -248,8 +252,8 @@ class _LoyaltyCardTile extends StatelessWidget {
),
Text(
'${card['points'] ?? 0}',
style: Theme.of(context).textTheme.displayLarge?.copyWith(
color: const Color(0xFFF3DCA2), // Bright golden amber accent
style: theme.textTheme.displayLarge?.copyWith(
color: accentColor,
fontWeight: FontWeight.bold,
),
),

View File

@ -5,7 +5,6 @@ import 'package:shared_preferences/shared_preferences.dart';
import '../services/odoo_service.dart';
import '../services/notification_service.dart';
import '../services/theme_manager.dart';
import '../theme/app_theme.dart';
import 'notifications_screen.dart';
import 'loyalty_dashboard.dart';
import 'branches_screen.dart';
@ -195,7 +194,7 @@ class _MainShellState extends State<MainShell> {
onDestinationSelected: (index) {
setState(() => _currentIndex = index);
},
backgroundColor: AppTheme.surfaceContainerLowest,
backgroundColor: colorScheme.surfaceContainerLowest,
indicatorColor: colorScheme.primary,
destinations: List.generate(4, (i) {
return NavigationDestination(

View File

@ -2,7 +2,6 @@ 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.
@ -55,6 +54,8 @@ class _CarouselWidgetState extends State<CarouselWidget> {
@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: [
@ -83,8 +84,8 @@ class _CarouselWidgetState extends State<CarouselWidget> {
dotHeight: 6,
dotWidth: 6,
expansionFactor: 3,
dotColor: AppTheme.surfaceContainer,
activeDotColor: AppTheme.secondary,
dotColor: colorScheme.surfaceContainer,
activeDotColor: colorScheme.secondary,
),
),
),
@ -99,6 +100,8 @@ class _SlideImage extends StatelessWidget {
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final colorScheme = theme.colorScheme;
final base64Img = slide['image'] as String?;
final externalUrl = slide['image_url'] as String?;
@ -110,7 +113,7 @@ class _SlideImage extends StatelessWidget {
final Uint8List bytes = base64Decode(base64Img);
image = Image.memory(bytes, fit: BoxFit.cover, width: double.infinity);
} catch (_) {
image = _placeholder();
image = _placeholder(colorScheme);
}
} else if (externalUrl != null && externalUrl.isNotEmpty) {
// External URL image
@ -118,20 +121,20 @@ class _SlideImage extends StatelessWidget {
externalUrl,
fit: BoxFit.cover,
width: double.infinity,
errorBuilder: (_, __, ___) => _placeholder(),
errorBuilder: (_, __, ___) => _placeholder(colorScheme),
loadingBuilder: (ctx, child, progress) {
if (progress == null) return child;
return const Center(child: CircularProgressIndicator());
},
);
} else {
image = _placeholder();
image = _placeholder(colorScheme);
}
return Container(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
decoration: BoxDecoration(
color: AppTheme.surfaceContainerLow,
color: colorScheme.surfaceContainerLow,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
@ -148,11 +151,11 @@ class _SlideImage extends StatelessWidget {
);
}
Widget _placeholder() {
Widget _placeholder(ColorScheme colorScheme) {
return Container(
color: AppTheme.surfaceContainer,
child: const Center(
child: Icon(Icons.image_rounded, size: 48, color: AppTheme.outlineVariant),
color: colorScheme.surfaceContainer,
child: Center(
child: Icon(Icons.image_rounded, size: 48, color: colorScheme.outline),
),
);
}

View File

@ -1,7 +1,6 @@
import 'dart:convert';
import 'dart:typed_data';
import 'package:flutter/material.dart';
import '../theme/app_theme.dart';
import '../screens/promo_detail_screen.dart';
/// Horizontal scrollable row of promo highlight cards.
@ -48,6 +47,8 @@ class _PromoCard extends StatelessWidget {
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final colorScheme = theme.colorScheme;
final base64Img = promo['image_128'] as String?;
final title = promo['name'] as String? ?? '';
@ -58,10 +59,10 @@ class _PromoCard extends StatelessWidget {
imageWidget = Image.memory(bytes, fit: BoxFit.cover,
width: double.infinity, height: 110);
} catch (_) {
imageWidget = _imagePlaceholder();
imageWidget = _imagePlaceholder(colorScheme);
}
} else {
imageWidget = _imagePlaceholder();
imageWidget = _imagePlaceholder(colorScheme);
}
return GestureDetector(
@ -75,10 +76,10 @@ class _PromoCard extends StatelessWidget {
width: 140,
margin: const EdgeInsets.only(right: 12, bottom: 6),
decoration: BoxDecoration(
color: AppTheme.surfaceContainerLowest,
color: colorScheme.surfaceContainerLowest,
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: AppTheme.outlineVariant.withValues(alpha: 0.3),
color: colorScheme.outline.withValues(alpha: 0.3),
width: 1,
),
boxShadow: [
@ -104,8 +105,8 @@ class _PromoCard extends StatelessWidget {
padding: const EdgeInsets.all(8.0),
child: Text(
title,
style: Theme.of(context).textTheme.labelLarge?.copyWith(
color: AppTheme.onSurface,
style: theme.textTheme.labelLarge?.copyWith(
color: colorScheme.onSurface,
fontWeight: FontWeight.bold,
fontSize: 11,
),
@ -119,11 +120,11 @@ class _PromoCard extends StatelessWidget {
);
}
Widget _imagePlaceholder() {
Widget _imagePlaceholder(ColorScheme colorScheme) {
return Container(
color: AppTheme.surfaceContainer,
child: const Center(
child: Icon(Icons.local_offer_rounded, size: 32, color: AppTheme.outlineVariant),
color: colorScheme.surfaceContainer,
child: Center(
child: Icon(Icons.local_offer_rounded, size: 32, color: colorScheme.outline),
),
);
}

View File

@ -74,6 +74,9 @@ class _SubscriptionCard extends StatelessWidget {
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final colorScheme = theme.colorScheme;
final programName = sub['program_id'] is List
? (sub['program_id'][1] as String? ?? 'Subscription')
: 'Subscription';
@ -86,9 +89,9 @@ class _SubscriptionCard extends StatelessWidget {
margin: const EdgeInsets.fromLTRB(16, 8, 16, 8),
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: AppTheme.surfaceContainerLowest,
color: colorScheme.surfaceContainerLowest,
border: Border.all(
color: active ? AppTheme.secondary.withValues(alpha: 0.4) : AppTheme.outlineVariant.withValues(alpha: 0.4),
color: active ? colorScheme.secondary.withValues(alpha: 0.4) : colorScheme.outline.withValues(alpha: 0.4),
width: 1.5,
),
borderRadius: BorderRadius.circular(16),
@ -111,13 +114,13 @@ class _SubscriptionCard extends StatelessWidget {
Icon(
Icons.card_membership_rounded,
size: 18,
color: active ? AppTheme.secondary : AppTheme.outlineVariant,
color: active ? colorScheme.secondary : colorScheme.outline,
),
const SizedBox(width: 8),
Text(
'SUBSCRIPTION CARD',
style: Theme.of(context).textTheme.labelLarge?.copyWith(
color: active ? AppTheme.secondary : AppTheme.outlineVariant,
style: theme.textTheme.labelLarge?.copyWith(
color: active ? colorScheme.secondary : colorScheme.outline,
fontWeight: FontWeight.bold,
fontSize: 10,
letterSpacing: 1.0,
@ -150,9 +153,9 @@ class _SubscriptionCard extends StatelessWidget {
const SizedBox(height: 14),
Text(
programName,
style: Theme.of(context).textTheme.titleMedium?.copyWith(
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
color: AppTheme.onSurface,
color: colorScheme.onSurface,
fontFamily: 'serif',
),
),
@ -165,18 +168,18 @@ class _SubscriptionCard extends StatelessWidget {
children: [
Text(
'Card Number',
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: AppTheme.onSurfaceVariant,
style: theme.textTheme.bodySmall?.copyWith(
color: colorScheme.onSurfaceVariant,
fontSize: 10,
),
),
const SizedBox(height: 2),
Text(
code.isNotEmpty ? code : 'N/A',
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
style: theme.textTheme.bodyMedium?.copyWith(
fontFamily: 'monospace',
fontWeight: FontWeight.bold,
color: AppTheme.onSurface,
color: colorScheme.onSurface,
),
),
],
@ -186,16 +189,16 @@ class _SubscriptionCard extends StatelessWidget {
children: [
Text(
'Validity Period',
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: AppTheme.onSurfaceVariant,
style: theme.textTheme.bodySmall?.copyWith(
color: colorScheme.onSurfaceVariant,
fontSize: 10,
),
),
const SizedBox(height: 2),
Text(
'$startDate - $endDate',
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: AppTheme.onSurface,
style: theme.textTheme.bodyMedium?.copyWith(
color: colorScheme.onSurface,
fontWeight: FontWeight.w600,
),
),