feat: Add theme customization feature with ThemeEditorWidget

- Introduced ThemeEditorWidget for user-friendly theme selection.
- Added ThemeOption class to manage theme properties.
- Implemented ThemeController to handle theme application logic.
- Updated ThemeCustomizer to allow external theme changes.
- Refactored wave background components to support dynamic colors.
- Updated various screens to utilize the new theme system.
- Enhanced UI elements with consistent styling and improved responsiveness.
This commit is contained in:
Vaibhav Surve 2025-10-29 14:30:51 +05:30
parent cd21a3ac38
commit c78231d0fd
14 changed files with 795 additions and 442 deletions

View File

@ -2,39 +2,9 @@ import 'package:flutter/material.dart';
import 'package:marco/helpers/theme/theme_customizer.dart';
enum LeftBarThemeType { light, dark }
enum ContentThemeType { light, dark }
enum RightBarThemeType { light, dark }
enum ContentThemeColor {
primary,
secondary,
success,
info,
warning,
danger,
light,
dark,
pink,
green,
red,
brandRed,
brandGreen;
Color get color {
return (AdminTheme.theme.contentTheme.getMappedIntoThemeColor[this]
?['color']) ??
Colors.black;
}
Color get onColor {
return (AdminTheme.theme.contentTheme.getMappedIntoThemeColor[this]
?['onColor']) ??
Colors.white;
}
}
class LeftBarTheme {
final Color background, onBackground;
final Color labelColor;
@ -48,16 +18,15 @@ class LeftBarTheme {
this.activeItemBackground = const Color(0x15663399),
});
//-------------------------------------- Left Bar Theme ----------------------------------------//
static final LeftBarTheme lightLeftBarTheme = LeftBarTheme();
static final LeftBarTheme darkLeftBarTheme = LeftBarTheme(
background: const Color(0xff282c32),
onBackground: const Color(0xffdcdcdc),
labelColor: const Color(0xff32BFAE),
activeItemBackground: const Color(0x1532BFAE),
activeItemColor: const Color(0xff32BFAE));
background: const Color(0xff282c32),
onBackground: const Color(0xffdcdcdc),
labelColor: const Color(0xff32BFAE),
activeItemBackground: const Color(0x1532BFAE),
activeItemColor: const Color(0xff32BFAE),
);
static LeftBarTheme getThemeFromType(LeftBarThemeType leftBarThemeType) {
switch (leftBarThemeType) {
@ -78,13 +47,12 @@ class TopBarTheme {
this.onBackground = const Color(0xff313a46),
});
//-------------------------------------- Left Bar Theme ----------------------------------------//
static final TopBarTheme lightTopBarTheme = TopBarTheme();
static final TopBarTheme darkTopBarTheme = TopBarTheme(
background: const Color(0xff2c3036),
onBackground: const Color(0xffdcdcdc));
background: const Color(0xff2c3036),
onBackground: const Color(0xffdcdcdc),
);
}
class RightBarTheme {
@ -98,19 +66,41 @@ class RightBarTheme {
this.onDisabled = const Color(0xff313a46),
});
//-------------------------------------- Left Bar Theme ----------------------------------------//
static final RightBarTheme lightRightBarTheme = RightBarTheme(
disabled: const Color(0xffffffff),
onDisabled: const Color(0xffdee2e6),
activeSwitchBorderColor: const Color(0xff727cf5),
inactiveSwitchBorderColor: const Color(0xffdee2e6));
disabled: const Color(0xffffffff),
onDisabled: const Color(0xffdee2e6),
activeSwitchBorderColor: const Color(0xff727cf5),
inactiveSwitchBorderColor: const Color(0xffdee2e6),
);
static final RightBarTheme darkRightBarTheme = RightBarTheme(
disabled: const Color(0xff444d57),
activeSwitchBorderColor: const Color(0xff727cf5),
inactiveSwitchBorderColor: const Color(0xffdee2e6),
onDisabled: const Color(0xff515a65));
disabled: const Color(0xff444d57),
activeSwitchBorderColor: const Color(0xff727cf5),
inactiveSwitchBorderColor: const Color(0xffdee2e6),
onDisabled: const Color(0xff515a65),
);
}
enum ContentThemeColor {
primary,
secondary,
success,
info,
warning,
danger,
light,
dark,
pink,
green,
red;
Color get color {
return (AdminTheme.theme.contentTheme.getMappedIntoThemeColor[this]?['color']) ?? Colors.black;
}
Color get onColor {
return (AdminTheme.theme.contentTheme.getMappedIntoThemeColor[this]?['onColor']) ?? Colors.white;
}
}
class ContentTheme {
@ -127,44 +117,15 @@ class ContentTheme {
final Color purple, onPurple;
final Color pink, onPink;
final Color red, onRed;
final Color brandRed, onBrandRed;
final Color brandGreen, onBrandGreen;
final Color cardBackground, cardShadow, cardBorder, cardText, cardTextMuted;
final Color title;
final Color disabled, onDisabled;
Map<ContentThemeColor, Map<String, Color>> get getMappedIntoThemeColor {
var c = AdminTheme.theme.contentTheme;
return {
ContentThemeColor.primary: {'color': c.primary, 'onColor': c.onPrimary},
ContentThemeColor.secondary: {
'color': c.secondary,
'onColor': c.onSecondary
},
ContentThemeColor.success: {'color': c.success, 'onColor': c.onSuccess},
ContentThemeColor.info: {'color': c.info, 'onColor': c.onInfo},
ContentThemeColor.warning: {'color': c.warning, 'onColor': c.onWarning},
ContentThemeColor.danger: {'color': c.danger, 'onColor': c.onDanger},
ContentThemeColor.light: {'color': c.light, 'onColor': c.onLight},
ContentThemeColor.dark: {'color': c.dark, 'onColor': c.onDark},
ContentThemeColor.pink: {'color': c.pink, 'onColor': c.onPink},
ContentThemeColor.red: {'color': c.red, 'onColor': c.onRed},
ContentThemeColor.brandRed: {
'color': c.brandRed,
'onColor': c.onBrandRed
},
ContentThemeColor.green: {
'color': c.brandGreen,
'onColor': c.onBrandGreen
},
};
}
ContentTheme({
this.background = const Color(0xfffafbfe),
this.onBackground = const Color(0xffF1F1F2),
this.primary = const Color(0xFF49BF3C),
this.primary = const Color(0xff663399),
this.onPrimary = const Color(0xffffffff),
this.secondary = const Color(0xff6c757d),
this.onSecondary = const Color(0xffffffff),
@ -181,13 +142,11 @@ class ContentTheme {
this.dark = const Color(0xff313a46),
this.onDark = const Color(0xffffffff),
this.purple = const Color(0xff800080),
this.onPurple = const Color(0xffFF0000),
this.pink = const Color(0xffFF1087),
this.onPurple = const Color(0xffffffff),
this.pink = const Color(0xffff1087),
this.onPink = const Color(0xffffffff),
this.red = const Color(0xffFF0000),
this.red = const Color(0xffff0000),
this.onRed = const Color(0xffffffff),
this.brandRed = const Color.fromARGB(255, 255, 0, 0),
this.onBrandRed = const Color(0xffffffff),
this.cardBackground = const Color(0xffffffff),
this.cardShadow = const Color(0xffffffff),
this.cardBorder = const Color(0xffffffff),
@ -196,46 +155,105 @@ class ContentTheme {
this.title = const Color(0xff6c757d),
this.disabled = const Color(0xffffffff),
this.onDisabled = const Color(0xffffffff),
this.brandGreen = const Color(0xFF49BF3C),
this.onBrandGreen = const Color(0xFFFFFFFF),
});
static final ContentTheme lightContentTheme = ContentTheme(
primary: Color(0xFF49BF3C),
background: const Color(0xfffafbfe),
onBackground: const Color(0xff313a46),
cardBorder: const Color(0xffe8ecf1),
cardBackground: const Color(0xffffffff),
cardShadow: const Color(0xff9aa1ab),
cardText: const Color(0xff6c757d),
title: const Color(0xff6c757d),
cardTextMuted: const Color(0xff98a6ad),
brandRed: const Color.fromARGB(255, 255, 0, 0),
onBrandRed: const Color(0xffffffff),
);
Map<ContentThemeColor, Map<String, Color>> get getMappedIntoThemeColor {
return {
ContentThemeColor.primary: {'color': primary, 'onColor': onPrimary},
ContentThemeColor.secondary: {'color': secondary, 'onColor': onSecondary},
ContentThemeColor.success: {'color': success, 'onColor': onSuccess},
ContentThemeColor.info: {'color': info, 'onColor': onInfo},
ContentThemeColor.warning: {'color': warning, 'onColor': onWarning},
ContentThemeColor.danger: {'color': danger, 'onColor': onDanger},
ContentThemeColor.light: {'color': light, 'onColor': onLight},
ContentThemeColor.dark: {'color': dark, 'onColor': onDark},
ContentThemeColor.pink: {'color': pink, 'onColor': onPink},
ContentThemeColor.red: {'color': red, 'onColor': onRed},
};
}
static final ContentTheme darkContentTheme = ContentTheme(
primary: Color(0xFF49BF3C),
background: const Color(0xff343a40),
onBackground: const Color(0xffF1F1F2),
disabled: const Color(0xff444d57),
onDisabled: const Color(0xff515a65),
cardBorder: const Color(0xff464f5b),
cardBackground: const Color(0xff37404a),
cardShadow: const Color(0xff01030E),
cardText: const Color(0xffaab8c5),
title: const Color(0xffaab8c5),
cardTextMuted: const Color(0xff8391a2),
brandRed: const Color.fromARGB(255, 255, 0, 0),
onBrandRed: const Color(0xffffffff),
);
ContentTheme copyWith({
Color? primary,
Color? onPrimary,
Color? secondary,
Color? onSecondary,
Color? background,
Color? onBackground,
}) {
return ContentTheme(
primary: primary ?? this.primary,
onPrimary: onPrimary ?? this.onPrimary,
secondary: secondary ?? this.secondary,
onSecondary: onSecondary ?? this.onSecondary,
background: background ?? this.background,
onBackground: onBackground ?? this.onBackground,
success: success,
onSuccess: onSuccess,
danger: danger,
onDanger: onDanger,
warning: warning,
onWarning: onWarning,
info: info,
onInfo: onInfo,
light: light,
onLight: onLight,
dark: dark,
onDark: onDark,
purple: purple,
onPurple: onPurple,
pink: pink,
onPink: onPink,
red: red,
onRed: onRed,
cardBackground: cardBackground,
cardShadow: cardShadow,
cardBorder: cardBorder,
cardText: cardText,
cardTextMuted: cardTextMuted,
title: title,
disabled: disabled,
onDisabled: onDisabled,
);
}
static ContentTheme withColorTheme(
ColorThemeType colorTheme, {
ThemeMode mode = ThemeMode.light,
}) {
final baseTheme = mode == ThemeMode.light
? ContentTheme()
: ContentTheme(
primary: const Color(0xff32BFAE),
background: const Color(0xff343a40),
onBackground: const Color(0xffF1F1F2),
cardBorder: const Color(0xff464f5b),
cardBackground: const Color(0xff37404a),
cardShadow: const Color(0xff01030E),
cardText: const Color(0xffaab8c5),
title: const Color(0xffaab8c5),
cardTextMuted: const Color(0xff8391a2),
);
switch (colorTheme) {
case ColorThemeType.purple:
return baseTheme.copyWith(primary: const Color(0xff663399), onPrimary: Colors.white);
case ColorThemeType.red:
return baseTheme.copyWith(primary: const Color(0xffff0000), onPrimary: Colors.white);
case ColorThemeType.green:
return baseTheme.copyWith(primary: const Color(0xff49BF3C), onPrimary: Colors.white);
case ColorThemeType.blue:
return baseTheme.copyWith(primary: const Color(0xff007bff), onPrimary: Colors.white);
}
}
}
enum ColorThemeType { purple, red, green, blue }
class AdminTheme {
final ContentTheme contentTheme;
final LeftBarTheme leftBarTheme;
final RightBarTheme rightBarTheme;
final TopBarTheme topBarTheme;
final ContentTheme contentTheme;
AdminTheme({
required this.leftBarTheme,
@ -244,27 +262,22 @@ class AdminTheme {
required this.contentTheme,
});
//-------------------------------------- Left Bar Theme ----------------------------------------//
static AdminTheme theme = AdminTheme(
leftBarTheme: LeftBarTheme.lightLeftBarTheme,
topBarTheme: TopBarTheme.lightTopBarTheme,
rightBarTheme: RightBarTheme.lightRightBarTheme,
contentTheme: ContentTheme.lightContentTheme);
leftBarTheme: LeftBarTheme.lightLeftBarTheme,
topBarTheme: TopBarTheme.lightTopBarTheme,
rightBarTheme: RightBarTheme.lightRightBarTheme,
contentTheme: ContentTheme.withColorTheme(ColorThemeType.purple, mode: ThemeMode.light),
);
static void setTheme() {
final themeMode = ThemeCustomizer.instance.theme;
final colorTheme = ThemeCustomizer.instance.colorTheme;
theme = AdminTheme(
leftBarTheme: ThemeCustomizer.instance.theme == ThemeMode.dark
? LeftBarTheme.darkLeftBarTheme
: LeftBarTheme.lightLeftBarTheme,
topBarTheme: ThemeCustomizer.instance.theme == ThemeMode.dark
? TopBarTheme.darkTopBarTheme
: TopBarTheme.lightTopBarTheme,
rightBarTheme: ThemeCustomizer.instance.theme == ThemeMode.dark
? RightBarTheme.darkRightBarTheme
: RightBarTheme.lightRightBarTheme,
contentTheme: ThemeCustomizer.instance.theme == ThemeMode.dark
? ContentTheme.darkContentTheme
: ContentTheme.lightContentTheme);
leftBarTheme: themeMode == ThemeMode.dark ? LeftBarTheme.darkLeftBarTheme : LeftBarTheme.lightLeftBarTheme,
topBarTheme: themeMode == ThemeMode.dark ? TopBarTheme.darkTopBarTheme : TopBarTheme.lightTopBarTheme,
rightBarTheme: themeMode == ThemeMode.dark ? RightBarTheme.darkRightBarTheme : RightBarTheme.lightRightBarTheme,
contentTheme: ContentTheme.withColorTheme(colorTheme, mode: themeMode),
);
}
}

View File

@ -24,7 +24,7 @@ class ThemeCustomizer {
ThemeMode leftBarTheme = ThemeMode.light;
ThemeMode rightBarTheme = ThemeMode.light;
ThemeMode topBarTheme = ThemeMode.light;
ColorThemeType colorTheme = ColorThemeType.red;
bool rightBarOpen = false;
bool leftBarCondensed = false;
@ -73,6 +73,11 @@ class ThemeCustomizer {
}
}
/// Public method to trigger theme updates externally
static void applyThemeChange() {
_notify();
}
static void notify() {
for (var value in _notifier) {
value(oldInstance, instance);

View File

@ -0,0 +1,271 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:marco/helpers/widgets/my_text.dart';
import 'package:marco/helpers/widgets/wave_background.dart';
import 'package:marco/helpers/theme/admin_theme.dart';
import 'package:marco/helpers/theme/theme_customizer.dart';
class ThemeOption {
final String label;
final Color primary;
final Color button;
final Color brand;
final ColorThemeType colorThemeType;
ThemeOption(
this.label, this.primary, this.button, this.brand, this.colorThemeType);
}
final List<ThemeOption> themeOptions = [
ThemeOption(
"Theme 1", Colors.red, Colors.red, Colors.red, ColorThemeType.red),
ThemeOption(
"Theme 2",
const Color(0xFF49BF3C),
const Color(0xFF49BF3C),
const Color(0xFF49BF3C),
ColorThemeType.green,
),
ThemeOption(
"Theme 3",
const Color(0xFF3F51B5),
const Color(0xFF3F51B5),
const Color(0xFF3F51B5),
ColorThemeType.blue,
),
ThemeOption(
"Theme 4",
const Color(0xFF663399),
const Color(0xFF663399),
const Color(0xFF663399),
ColorThemeType.purple,
),
];
class ThemeController extends GetxController {
RxInt selectedIndex = 0.obs;
RxBool showApplied = false.obs;
void init() {
final currentPrimary = AdminTheme.theme.contentTheme.primary;
int index = themeOptions
.indexWhere((opt) => opt.primary.value == currentPrimary.value);
selectedIndex.value = index == -1 ? 0 : index;
}
void applyTheme(int index) async {
selectedIndex.value = index;
showApplied.value = true;
ThemeCustomizer.instance.colorTheme = themeOptions[index].colorThemeType;
ThemeCustomizer.applyThemeChange();
await Future.delayed(const Duration(milliseconds: 600));
showApplied.value = false;
}
}
class ThemeEditorWidget extends StatefulWidget {
final VoidCallback onClose;
const ThemeEditorWidget({super.key, required this.onClose});
@override
_ThemeEditorWidgetState createState() => _ThemeEditorWidgetState();
}
class _ThemeEditorWidgetState extends State<ThemeEditorWidget> {
final ThemeController themeController = Get.put(ThemeController());
@override
void initState() {
super.initState();
themeController.init();
}
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Header row with title and close button
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
MyText.bodyLarge("Theme Customization", fontWeight: 600),
IconButton(
icon: const Icon(Icons.close),
onPressed: widget.onClose,
tooltip: "Back",
iconSize: 20,
),
],
),
const SizedBox(height: 12),
// Theme cards wrapped in reactive Obx widget
Center(
child: Obx(
() => Wrap(
spacing: 12,
runSpacing: 12,
alignment: WrapAlignment.center,
children: List.generate(themeOptions.length, (i) {
return ThemeCard(
themeOption: themeOptions[i],
isSelected: themeController.selectedIndex.value == i,
onTap: () => themeController.applyTheme(i),
);
}),
),
),
),
const SizedBox(height: 12),
// Applied indicator reactive widget
Obx(
() => themeController.showApplied.value
? Padding(
padding: const EdgeInsets.only(top: 10),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.check_circle,
color:
themeOptions[themeController.selectedIndex.value]
.brand,
size: 20,
),
const SizedBox(width: 6),
Text(
"Theme Applied!",
style: TextStyle(
color: themeOptions[
themeController.selectedIndex.value]
.brand,
fontWeight: FontWeight.w700,
),
),
],
),
)
: const SizedBox(),
),
const SizedBox(height: 16),
const Text(
"Preview and select a theme. You can change this anytime.",
style: TextStyle(fontSize: 13, color: Colors.black54),
),
],
),
);
}
}
class ThemeCard extends StatelessWidget {
final ThemeOption themeOption;
final bool isSelected;
final VoidCallback onTap;
const ThemeCard({
Key? key,
required this.themeOption,
required this.isSelected,
required this.onTap,
});
@override
Widget build(BuildContext context) {
return SizedBox(
width: 80,
child: Material(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
elevation: isSelected ? 4 : 1,
child: InkWell(
borderRadius: BorderRadius.circular(12),
onTap: onTap,
child: AnimatedContainer(
duration: const Duration(milliseconds: 250),
curve: Curves.easeInOut,
padding: const EdgeInsets.all(6),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: isSelected ? themeOption.brand : Colors.transparent,
width: 2,
),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(
height: 80,
child: ClipRRect(
borderRadius: BorderRadius.circular(10),
child: Stack(
fit: StackFit.expand,
children: [
CustomPaint(
painter: RedWavePainter(themeOption.brand, 0.15)),
Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
"Hello, User!",
textAlign: TextAlign.center,
style: TextStyle(
fontWeight: FontWeight.w600,
color: themeOption.primary,
fontSize: 12,
),
),
const SizedBox(height: 4),
SizedBox(
height: 18,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: themeOption.button,
padding: const EdgeInsets.symmetric(
horizontal: 8, vertical: 0),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(4),
),
elevation: 1,
textStyle: const TextStyle(fontSize: 10),
),
onPressed: () {},
child: const Text("Welcome"),
),
),
],
),
),
],
),
),
),
const SizedBox(height: 6),
Text(
themeOption.label,
style: TextStyle(
fontWeight: FontWeight.w600,
fontSize: 12,
color: Colors.grey[700],
),
),
],
),
),
),
),
);
}
}

View File

@ -1,38 +1,35 @@
import 'package:flutter/material.dart';
class WaveBackground extends StatelessWidget {
final Color color;
class RedWaveBackground extends StatelessWidget {
final Color brandRed;
final double heightFactor;
const WaveBackground({
const RedWaveBackground({
super.key,
required this.color,
this.heightFactor = 0.2,
required this.brandRed,
this.heightFactor = 0.2,
});
@override
Widget build(BuildContext context) {
return CustomPaint(
painter: _WavePainter(color, heightFactor),
painter: RedWavePainter(brandRed, heightFactor),
size: Size.infinite,
);
}
}
class _WavePainter extends CustomPainter {
final Color color;
class RedWavePainter extends CustomPainter {
final Color brandRed;
final double heightFactor;
_WavePainter(this.color, this.heightFactor);
RedWavePainter(this.brandRed, this.heightFactor);
@override
void paint(Canvas canvas, Size size) {
final paint1 = Paint()
..shader = LinearGradient(
colors: [
const Color(0xFF49BF3C),
const Color(0xFF81C784),
],
colors: [brandRed, const Color.fromARGB(255, 97, 22, 22)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
).createShader(Rect.fromLTWH(0, 0, size.width, size.height));
@ -40,20 +37,26 @@ class _WavePainter extends CustomPainter {
final path1 = Path()
..moveTo(0, size.height * heightFactor)
..quadraticBezierTo(
size.width * 0.25, size.height * 0.05, size.width * 0.5, size.height * 0.15)
size.width * 0.25, size.height * 0.05,
size.width * 0.5, size.height * 0.15,
)
..quadraticBezierTo(
size.width * 0.75, size.height * 0.25, size.width, size.height * 0.1)
size.width * 0.75, size.height * 0.25,
size.width, size.height * 0.1,
)
..lineTo(size.width, 0)
..lineTo(0, 0)
..close();
canvas.drawPath(path1, paint1);
// Secondary wave (overlay) with same green but lighter opacity
final paint2 = Paint()..color = const Color(0xFF49BF3C).withOpacity(0.15);
final paint2 = Paint()..color = brandRed.withOpacity(0.15);
final path2 = Path()
..moveTo(0, size.height * (heightFactor + 0.05))
..quadraticBezierTo(size.width * 0.4, size.height * 0.1, size.width, size.height * 0.2)
..quadraticBezierTo(
size.width * 0.4, size.height * 0.1,
size.width, size.height * 0.2,
)
..lineTo(size.width, 0)
..lineTo(0, 0)
..close();

View File

@ -60,7 +60,7 @@ class _ForgotPasswordScreenState extends State<ForgotPasswordScreen>
return Scaffold(
body: Stack(
children: [
WaveBackground(color: contentTheme.brandRed),
RedWaveBackground(brandRed: contentTheme.primary),
SafeArea(
child: Center(
child: Column(
@ -254,7 +254,7 @@ class _ForgotPasswordScreenState extends State<ForgotPasswordScreen>
Widget _buildBackButton() {
return TextButton.icon(
onPressed: () async => await LocalStorage.logout(),
icon: Icon(Icons.arrow_back, size: 18, color: contentTheme.primary),
icon: Icon(Icons.arrow_back, size: 18, color: contentTheme.primary,),
label: MyText.bodyMedium(
'Back to Login',
color: contentTheme.primary,

View File

@ -56,7 +56,7 @@ class _WelcomeScreenState extends State<WelcomeScreen>
context: context,
barrierDismissible: false,
builder: (_) => Dialog(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(5)),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
insetPadding: const EdgeInsets.all(24),
child: SingleChildScrollView(
padding: const EdgeInsets.all(24),
@ -102,7 +102,7 @@ class _WelcomeScreenState extends State<WelcomeScreen>
return Scaffold(
body: Stack(
children: [
WaveBackground(color: contentTheme.brandRed),
RedWaveBackground(brandRed: contentTheme.primary),
SafeArea(
child: Center(
child: SingleChildScrollView(
@ -204,7 +204,7 @@ class _WelcomeScreenState extends State<WelcomeScreen>
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
decoration: BoxDecoration(
color: Colors.orangeAccent,
borderRadius: BorderRadius.circular(5),
borderRadius: BorderRadius.circular(6),
),
child: MyText(
'BETA',
@ -235,9 +235,10 @@ class _WelcomeScreenState extends State<WelcomeScreen>
),
),
style: ElevatedButton.styleFrom(
backgroundColor: contentTheme.brandGreen,
backgroundColor: contentTheme.primary,
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(5)),
shape:
RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)),
elevation: 4,
shadowColor: Colors.black26,
),

View File

@ -193,7 +193,7 @@ class _LoginScreenState extends State<LoginScreen> with UIMixin {
elevation: 2,
padding: MySpacing.xy(24, 16),
borderRadiusAll: 5,
backgroundColor:contentTheme.brandGreen,
backgroundColor:contentTheme.primary,
child: MyText.labelMedium(
'Login',
fontWeight: 600,

View File

@ -10,6 +10,7 @@ import 'package:marco/helpers/services/api_endpoints.dart';
import 'package:marco/helpers/utils/mixins/ui_mixin.dart';
import 'package:marco/helpers/widgets/wave_background.dart';
class MPINAuthScreen extends StatefulWidget {
const MPINAuthScreen({super.key});
@ -52,7 +53,7 @@ class _MPINAuthScreenState extends State<MPINAuthScreen>
return Scaffold(
body: Stack(
children: [
WaveBackground(color: contentTheme.brandRed),
RedWaveBackground(brandRed: contentTheme.primary),
SafeArea(
child: Center(
child: Column(
@ -111,7 +112,7 @@ class _MPINAuthScreenState extends State<MPINAuthScreen>
horizontal: 10, vertical: 4),
decoration: BoxDecoration(
color: Colors.orangeAccent,
borderRadius: BorderRadius.circular(5),
borderRadius: BorderRadius.circular(6),
),
child: MyText(
'BETA',
@ -146,7 +147,7 @@ class _MPINAuthScreenState extends State<MPINAuthScreen>
padding: const EdgeInsets.all(24),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(5),
borderRadius: BorderRadius.circular(20),
boxShadow: const [
BoxShadow(
color: Colors.black12,
@ -265,7 +266,7 @@ class _MPINAuthScreenState extends State<MPINAuthScreen>
filled: true,
fillColor: Colors.grey.shade100,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(5),
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide.none,
),
),
@ -280,7 +281,7 @@ class _MPINAuthScreenState extends State<MPINAuthScreen>
onPressed: controller.isLoading.value ? null : controller.onSubmitMPIN,
elevation: 2,
padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 16),
borderRadiusAll: 5,
borderRadiusAll: 10,
backgroundColor: controller.isLoading.value
? contentTheme.primary.withOpacity(0.6)
: contentTheme.primary,
@ -316,11 +317,11 @@ class _MPINAuthScreenState extends State<MPINAuthScreen>
if (isNewUser || isChangeMpin)
TextButton.icon(
onPressed: () => Get.toNamed('/dashboard'),
icon: const Icon(Icons.arrow_back,
size: 18, color: Colors.redAccent),
icon: Icon(Icons.arrow_back,
size: 18, color: contentTheme.primary),
label: MyText.bodyMedium(
'Back to Home Page',
color: contentTheme.brandRed,
color: contentTheme.primary,
fontWeight: 600,
fontSize: 14,
),
@ -332,7 +333,7 @@ class _MPINAuthScreenState extends State<MPINAuthScreen>
size: 18, color: Colors.redAccent),
label: MyText.bodyMedium(
'Go back to Login Screen',
color: contentTheme.brandRed,
color: contentTheme.primary,
fontWeight: 600,
fontSize: 14,
),

View File

@ -170,7 +170,7 @@ class _OTPLoginScreenState extends State<OTPLoginScreen> with UIMixin {
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: contentTheme.brandRed, width: 2),
borderSide: BorderSide(color: contentTheme.primary, width: 2),
),
),
),
@ -220,7 +220,7 @@ class _OTPLoginScreenState extends State<OTPLoginScreen> with UIMixin {
elevation: 2,
padding: MySpacing.xy(24, 16),
borderRadiusAll: 10,
backgroundColor: contentTheme.brandRed,
backgroundColor: contentTheme.primary,
child: MyText.labelMedium(
'Verify OTP',
fontWeight: 600,

View File

@ -4,6 +4,7 @@ import 'package:marco/helpers/services/auth_service.dart';
import 'package:marco/helpers/widgets/my_snackbar.dart';
import 'package:marco/helpers/utils/mixins/ui_mixin.dart';
import 'package:marco/helpers/widgets/my_text.dart';
import 'package:marco/helpers/widgets/my_spacing.dart';
class OrganizationFormBottomSheet {
static void show(BuildContext context) {
@ -81,169 +82,203 @@ class _OrganizationFormState extends State<_OrganizationForm> with UIMixin {
@override
Widget build(BuildContext context) {
return Container(
decoration: const BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
),
padding: const EdgeInsets.fromLTRB(20, 20, 20, 40),
child: SingleChildScrollView(
controller: widget.scrollController,
child: Form(
key: validator.formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Center(
child: Container(
width: 40,
height: 5,
margin: const EdgeInsets.only(bottom: 20),
decoration: BoxDecoration(
color: Colors.grey[300],
borderRadius: BorderRadius.circular(10),
),
),
),
Center(
child: Column(
children: [
MyText.titleLarge(
'Adventure starts here 🚀',
fontWeight: 600,
color: Colors.black87,
),
const SizedBox(height: 4),
MyText.bodySmall(
"Make your app management easy and fun!",
color: Colors.grey,
),
],
),
),
const SizedBox(height: 20),
_sectionHeader('Organization Info'),
_buildTextField('organizationName', 'Organization Name'),
_buildTextField('email', 'Email',
keyboardType: TextInputType.emailAddress),
_buildTextField('about', 'About Organization'),
_sectionHeader('Contact Details'),
_buildTextField('contactPerson', 'Contact Person'),
_buildTextField('contactNumber', 'Contact Number',
keyboardType: TextInputType.phone),
_buildTextField('address', 'Current Address'),
_sectionHeader('Additional Details'),
_buildPopupMenuField(
'Organization Size',
_sizes,
_selectedSize,
(val) => setState(() => _selectedSize = val),
'Please select organization size',
),
_buildPopupMenuField(
'Industry',
_industries.map((e) => e['name'] as String).toList(),
_selectedIndustryId != null
? _industries.firstWhere(
(e) => e['id'] == _selectedIndustryId)['name']
: null,
(val) {
setState(() {
final selectedIndustry = _industries.firstWhere(
(element) => element['name'] == val,
orElse: () => {},
);
_selectedIndustryId = selectedIndustry['id'];
});
},
'Please select industry',
),
const SizedBox(height: 12),
Row(
children: [
Checkbox(
value: _agreed,
onChanged: (val) => setState(() => _agreed = val ?? false),
fillColor: MaterialStateProperty.resolveWith((states) =>
states.contains(MaterialState.selected)
? contentTheme.primary
: Colors.white),
checkColor: Colors.white,
),
Row(
children: [
MyText(
'I agree to the ',
color: Colors.black87,
),
MyText(
'privacy policy & terms',
color: contentTheme.primary,
fontWeight: 600,
),
],
)
],
),
const SizedBox(height: 20),
Row(
children: [
Expanded(
child: ElevatedButton.icon(
onPressed: () => Navigator.pop(context),
icon: const Icon(Icons.close, color: Colors.white),
label: MyText.bodyMedium(
"Cancel",
color: Colors.white,
fontWeight: 600,
),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.grey,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
padding: const EdgeInsets.symmetric(vertical: 8),
),
),
),
const SizedBox(width: 12),
Expanded(
child: ElevatedButton.icon(
onPressed: _loading ? null : _submitForm,
icon:
Icon(Icons.check_circle_outline, color: Colors.white),
label: MyText.bodyMedium(
_loading ? "Submitting..." : "Submit",
color: Colors.white,
fontWeight: 600,
),
style: ElevatedButton.styleFrom(
backgroundColor: contentTheme
.primary,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
padding: const EdgeInsets.symmetric(vertical: 8),
),
),
),
],
),
const SizedBox(height: 8),
Center(
child: TextButton.icon(
onPressed: () => Navigator.pop(context),
icon:
Icon(Icons.arrow_back, size: 18, color: contentTheme.primary),
label: MyText.bodySmall(
'Back to log in',
fontWeight: 600,
color: contentTheme.primary,
),
),
return SingleChildScrollView(
padding: MediaQuery.of(context).viewInsets,
child: Padding(
padding: const EdgeInsets.only(top: 60),
child: Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: const BorderRadius.vertical(top: Radius.circular(24)),
boxShadow: const [
BoxShadow(
color: Colors.black12,
blurRadius: 12,
offset: Offset(0, -2),
),
],
),
child: SafeArea(
top: false,
child: Padding(
padding: const EdgeInsets.fromLTRB(20, 16, 20, 32),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Center(
child: Container(
width: 40,
height: 5,
decoration: BoxDecoration(
color: Colors.grey.shade300,
borderRadius: BorderRadius.circular(10),
),
),
),
MySpacing.height(12),
Center(
child: MyText.titleLarge(
'Adventure starts here 🚀',
fontWeight: 700,
textAlign: TextAlign.center,
),
),
MySpacing.height(4),
Center(
child: MyText.bodySmall(
"Make your app management easy and fun!",
color: Colors.grey[700],
),
),
MySpacing.height(12),
Form(
key: validator.formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_sectionHeader('Organization Info'),
_buildTextField('organizationName', 'Organization Name'),
_buildTextField('email', 'Email',
keyboardType: TextInputType.emailAddress),
_buildTextField('about', 'About Organization'),
_sectionHeader('Contact Details'),
_buildTextField('contactPerson', 'Contact Person'),
_buildTextField('contactNumber', 'Contact Number',
keyboardType: TextInputType.phone),
_buildTextField('address', 'Current Address'),
_sectionHeader('Additional Details'),
_buildPopupMenuField(
'Organization Size',
_sizes,
_selectedSize,
(val) => setState(() => _selectedSize = val),
'Please select organization size',
),
_buildPopupMenuField(
'Industry',
_industries.map((e) => e['name'] as String).toList(),
_selectedIndustryId != null
? _industries.firstWhere(
(e) => e['id'] == _selectedIndustryId)['name']
: null,
(val) {
setState(() {
final selectedIndustry = _industries.firstWhere(
(element) => element['name'] == val,
orElse: () => {},
);
_selectedIndustryId = selectedIndustry['id'];
});
},
'Please select industry',
),
const SizedBox(height: 12),
Row(
children: [
Checkbox(
value: _agreed,
onChanged: (val) =>
setState(() => _agreed = val ?? false),
fillColor: MaterialStateProperty.resolveWith(
(states) => states.contains(MaterialState.selected)
? contentTheme.primary
: Colors.white),
checkColor: Colors.white,
side: BorderSide(color: contentTheme.primary, width: 2),
),
Flexible(
child: Wrap(
children: [
MyText(
'I agree to the ',
color: Colors.black87,
),
MyText(
'privacy policy & terms',
color: contentTheme.primary,
fontWeight: 600,
),
],
),
),
],
),
],
),
),
MySpacing.height(12),
Row(
children: [
Expanded(
child: ElevatedButton.icon(
onPressed: () => Navigator.pop(context),
icon: const Icon(Icons.close, color: Colors.white),
label: MyText.bodyMedium(
"Cancel",
color: Colors.white,
fontWeight: 600,
),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.grey,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
padding: const EdgeInsets.symmetric(vertical: 12),
),
),
),
const SizedBox(width: 12),
Expanded(
child: ElevatedButton.icon(
onPressed: _loading ? null : _submitForm,
icon: _loading
? SizedBox(
width: 18,
height: 18,
child: CircularProgressIndicator(
color: Colors.white,
strokeWidth: 2,
),
)
: const Icon(Icons.check_circle_outline,
color: Colors.white),
label: MyText.bodyMedium(
_loading ? "Submitting..." : "Submit",
color: Colors.white,
fontWeight: 600,
),
style: ElevatedButton.styleFrom(
backgroundColor: contentTheme.primary,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
padding: const EdgeInsets.symmetric(vertical: 12),
),
),
),
],
),
MySpacing.height(12),
Center(
child: TextButton.icon(
onPressed: () => Navigator.pop(context),
icon: Icon(
Icons.arrow_back,
size: 18,
color: contentTheme.primary,
),
label: MyText.bodySmall(
'Back to log in',
fontWeight: 600,
color: contentTheme.primary,
),
),
),
],
),
),
),
),
),
);
@ -290,19 +325,19 @@ class _OrganizationFormState extends State<_OrganizationForm> with UIMixin {
contentPadding:
const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(5),
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: Colors.grey[400]!),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(5),
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: Colors.grey[300]!),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(5),
borderSide: BorderSide(color: contentTheme.brandRed, width: 1.5),
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: contentTheme.primary, width: 1.5),
),
errorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(5),
borderRadius: BorderRadius.circular(12),
borderSide: const BorderSide(color: Colors.red),
),
),
@ -366,17 +401,17 @@ class _OrganizationFormState extends State<_OrganizationForm> with UIMixin {
contentPadding: const EdgeInsets.symmetric(
horizontal: 16, vertical: 16),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(5),
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: Colors.grey[400]!),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(5),
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: Colors.grey[300]!),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(5),
borderRadius: BorderRadius.circular(12),
borderSide:
BorderSide(color: contentTheme.brandRed, width: 1.5),
BorderSide(color: contentTheme.primary, width: 1.5),
),
errorText: fieldState.errorText,
),
@ -385,8 +420,7 @@ class _OrganizationFormState extends State<_OrganizationForm> with UIMixin {
children: [
MyText.bodyMedium(
selectedValue ?? 'Select $label',
color:
selectedValue == null ? Colors.grey : Colors.black,
color: selectedValue == null ? Colors.grey : Colors.black,
),
const Icon(Icons.arrow_drop_down, color: Colors.grey),
],

View File

@ -134,7 +134,7 @@ class _NotesViewState extends State<NotesView> with UIMixin {
),
if (note.isActive) ...[
IconButton(
icon: Icon(isEditing ? Icons.close : Icons.edit_outlined,
icon: Icon(isEditing ? Icons.close : Icons.edit ,
color: Colors.indigo, size: 18),
splashRadius: 18,
onPressed: () {
@ -143,7 +143,7 @@ class _NotesViewState extends State<NotesView> with UIMixin {
},
),
IconButton(
icon: const Icon(Icons.delete_outline,
icon: const Icon(Icons.delete,
size: 18, color: Colors.red),
splashRadius: 18,
onPressed: () async {
@ -216,7 +216,7 @@ class _NotesViewState extends State<NotesView> with UIMixin {
await controller.updateNote(updated);
controller.editingNoteId.value = null;
},
icon: const Icon(Icons.check_circle_outline,
icon: const Icon(Icons.check_circle,
color: Colors.white),
label: MyText.bodyMedium(
"Save",

View File

@ -9,6 +9,7 @@ import 'package:marco/helpers/services/api_endpoints.dart';
import 'package:marco/images.dart';
import 'package:marco/controller/project_controller.dart';
import 'package:marco/view/layouts/user_profile_right_bar.dart';
import 'package:marco/helpers/utils/mixins/ui_mixin.dart';
class Layout extends StatefulWidget {
final Widget? child;
@ -20,7 +21,7 @@ class Layout extends StatefulWidget {
State<Layout> createState() => _LayoutState();
}
class _LayoutState extends State<Layout> {
class _LayoutState extends State<Layout> with UIMixin {
final LayoutController controller = LayoutController();
final EmployeeInfo? employeeInfo = LocalStorage.getEmployeeInfo();
final bool isBetaEnvironment = ApiEndpoints.baseUrl.contains("stage");
@ -409,13 +410,13 @@ class _LayoutState extends State<Layout> {
style: TextStyle(
fontWeight:
isSelected ? FontWeight.bold : FontWeight.normal,
color: isSelected ? Colors.blueAccent : Colors.black87,
color: isSelected ? contentTheme.primary : Colors.black87,
),
),
contentPadding: const EdgeInsets.symmetric(horizontal: 0),
activeColor: Colors.blueAccent,
activeColor: contentTheme.primary,
tileColor: isSelected
? Colors.blueAccent.withOpacity(0.1)
? contentTheme.primary.withOpacity(0.1)
: Colors.transparent,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(5),

View File

@ -39,7 +39,7 @@ class _OfflineScreenState extends State<OfflineScreen>
return Scaffold(
body: Stack(
children: [
_RedWaveBackground(brandRed: contentTheme.brandRed),
_RedWaveBackground(primary: contentTheme.primary),
SafeArea(
child: Center(
child: Padding(
@ -101,28 +101,28 @@ class _OfflineScreenState extends State<OfflineScreen>
}
class _RedWaveBackground extends StatelessWidget {
final Color brandRed;
const _RedWaveBackground({required this.brandRed});
final Color primary;
const _RedWaveBackground({required this.primary});
@override
Widget build(BuildContext context) {
return CustomPaint(
painter: _WavePainter(brandRed),
painter: _WavePainter(primary),
size: Size.infinite,
);
}
}
class _WavePainter extends CustomPainter {
final Color brandRed;
final Color primary;
_WavePainter(this.brandRed);
_WavePainter(this.primary);
@override
void paint(Canvas canvas, Size size) {
final paint1 = Paint()
..shader = LinearGradient(
colors: [brandRed, const Color.fromARGB(255, 97, 22, 22)],
colors: [primary, const Color.fromARGB(255, 97, 22, 22)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
).createShader(Rect.fromLTWH(0, 0, size.width, size.height));

View File

@ -10,8 +10,8 @@ import 'package:marco/helpers/widgets/avatar.dart';
import 'package:marco/model/employees/employee_info.dart';
import 'package:marco/controller/auth/mpin_controller.dart';
import 'package:marco/view/employees/employee_profile_screen.dart';
import 'package:marco/view/support/support_screen.dart';
import 'package:marco/view/faq/faq_screen.dart';
import 'package:marco/helpers/theme/theme_editor_widget.dart';
class UserProfileBar extends StatefulWidget {
final bool isCondensed;
@ -26,6 +26,7 @@ class _UserProfileBarState extends State<UserProfileBar>
late EmployeeInfo employeeInfo;
bool _isLoading = true;
bool hasMpin = true;
bool _isThemeEditorVisible = false;
@override
void initState() {
@ -45,7 +46,7 @@ class _UserProfileBarState extends State<UserProfileBar>
return Padding(
padding: const EdgeInsets.only(left: 14),
child: ClipRRect(
borderRadius: BorderRadius.circular(5),
borderRadius: BorderRadius.circular(22),
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 18, sigmaY: 18),
child: AnimatedContainer(
@ -55,59 +56,72 @@ class _UserProfileBarState extends State<UserProfileBar>
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
Colors.white.withValues(alpha: 0.95),
Colors.white.withValues(alpha: 0.85),
Colors.white.withOpacity(0.95),
Colors.white.withOpacity(0.85),
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(5),
borderRadius: BorderRadius.circular(22),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.06),
color: Colors.black.withOpacity(0.06),
blurRadius: 18,
offset: const Offset(0, 8),
)
],
border: Border.all(
color: Colors.grey.withValues(alpha: 0.25),
color: Colors.grey.withOpacity(0.25),
width: 1,
),
),
child: SafeArea(
bottom: true,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_isLoading
? const _LoadingSection()
: _userProfileSection(isCondensed),
MySpacing.height(12),
Divider(
indent: 18,
endIndent: 18,
thickness: 0.7,
color: Colors.grey.withValues(alpha: 0.25),
),
MySpacing.height(12),
_supportAndSettingsMenu(isCondensed),
const Spacer(),
Divider(
indent: 18,
endIndent: 18,
thickness: 0.35,
color: Colors.grey.withValues(alpha: 0.18),
),
_logoutButton(isCondensed),
],
),
),
bottom: true,
child: Stack(
children: [
Offstage(
offstage: _isThemeEditorVisible,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_isLoading
? const _LoadingSection()
: _userProfileSection(isCondensed),
MySpacing.height(12),
Divider(
indent: 18,
endIndent: 18,
thickness: 0.7,
color: Colors.grey.withOpacity(0.25),
),
MySpacing.height(12),
_supportAndSettingsMenu(isCondensed),
const Spacer(),
Divider(
indent: 18,
endIndent: 18,
thickness: 0.35,
color: Colors.grey.withOpacity(0.18),
),
_logoutButton(isCondensed),
],
),
),
Offstage(
offstage: !_isThemeEditorVisible,
child: ThemeEditorWidget(
onClose: () {
setState(() => _isThemeEditorVisible = false);
},
),
),
],
)),
),
),
),
);
}
Widget _userProfileSection(bool condensed) {
final padding = MySpacing.fromLTRB(
condensed ? 16 : 26,
@ -126,7 +140,7 @@ class _UserProfileBarState extends State<UserProfileBar>
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: Theme.of(context).primaryColor.withValues(alpha: 0.15),
color: Theme.of(context).primaryColor.withOpacity(0.15),
blurRadius: 10,
spreadRadius: 1,
),
@ -180,22 +194,23 @@ class _UserProfileBarState extends State<UserProfileBar>
),
SizedBox(height: spacingHeight),
_menuItemRow(
icon: LucideIcons.badge_help,
label: 'Support',
onTap: _onSupportTap,
icon: LucideIcons.settings,
label: 'Settings',
onTap: () {
setState(() {
_isThemeEditorVisible = true;
});
},
),
SizedBox(height: spacingHeight),
_menuItemRow(
icon: LucideIcons.info,
label: 'FAQ', // <-- New FAQ menu item
onTap: _onFaqTap, // <-- Handle tap
icon: LucideIcons.badge_alert,
label: 'Support',
),
SizedBox(height: spacingHeight),
_menuItemRow(
icon: LucideIcons.lock,
label: hasMpin ? 'Change MPIN' : 'Set MPIN',
iconColor: contentTheme.primary,
textColor: contentTheme.primary,
onTap: _onMpinTap,
),
],
@ -203,14 +218,6 @@ class _UserProfileBarState extends State<UserProfileBar>
);
}
void _onFaqTap() {
Get.to(() => const FAQScreen());
}
void _onSupportTap() {
Get.to(() => const SupportScreen());
}
Widget _menuItemRow({
required IconData icon,
required String label,
@ -220,12 +227,12 @@ class _UserProfileBarState extends State<UserProfileBar>
}) {
return InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(5),
borderRadius: BorderRadius.circular(12),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.9),
borderRadius: BorderRadius.circular(5),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.grey.withOpacity(0.2), width: 1),
),
child: Row(
@ -277,15 +284,15 @@ class _UserProfileBarState extends State<UserProfileBar>
fontWeight: 700,
),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red.shade600,
backgroundColor: contentTheme.primary,
foregroundColor: Colors.white,
shadowColor: Colors.red.shade200,
shadowColor: contentTheme.primary,
padding: EdgeInsets.symmetric(
vertical: condensed ? 9 : 12,
horizontal: condensed ? 6 : 16,
vertical: condensed ? 14 : 18,
horizontal: condensed ? 14 : 22,
),
shape:
RoundedRectangleBorder(borderRadius: BorderRadius.circular(5)),
RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
),
),
),
@ -301,54 +308,71 @@ class _UserProfileBarState extends State<UserProfileBar>
}
Widget _buildLogoutDialog(BuildContext context) {
final theme = Theme.of(context);
final primaryColor = contentTheme.primary;
return Dialog(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(5)),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(24)),
elevation: 10,
backgroundColor: Colors.white,
backgroundColor: theme.cardColor,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 28, vertical: 34),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(LucideIcons.log_out, size: 56, color: Colors.red.shade700),
const SizedBox(height: 18),
const Text(
// Top icon
Icon(LucideIcons.log_out, size: 56, color: primaryColor),
MySpacing.height(18),
// Title
MyText.titleLarge(
"Logout Confirmation",
style: TextStyle(
fontSize: 22,
fontWeight: FontWeight.w700,
color: Colors.black87),
),
const SizedBox(height: 14),
const Text(
"Are you sure you want to logout?\nYou will need to login again to continue.",
fontWeight: 700,
textAlign: TextAlign.center,
style: TextStyle(fontSize: 16, color: Colors.black54),
),
const SizedBox(height: 30),
MySpacing.height(14),
// Subtitle
MyText.bodyMedium(
"Are you sure you want to logout?\nYou will need to login again to continue.",
color: Colors.grey[700],
textAlign: TextAlign.center,
),
MySpacing.height(30),
// Buttons
Row(
children: [
Expanded(
child: TextButton(
child: ElevatedButton(
onPressed: () => Navigator.pop(context, false),
style: TextButton.styleFrom(
foregroundColor: Colors.grey.shade700,
padding: const EdgeInsets.symmetric(vertical: 12)),
child: const Text("Cancel"),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.grey,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 14),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(14)),
),
child: MyText.bodyMedium(
"Cancel",
color: Colors.white,
fontWeight: 600,
),
),
),
const SizedBox(width: 18),
MySpacing.width(18),
Expanded(
child: ElevatedButton(
onPressed: () => Navigator.pop(context, true),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red.shade700,
backgroundColor: primaryColor,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 14),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(5)),
borderRadius: BorderRadius.circular(14)),
),
child: MyText.bodyMedium(
"Logout",
color: Colors.white,
fontWeight: 600,
),
child: const Text("Logout"),
),
),
],