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

View File

@ -24,7 +24,7 @@ class ThemeCustomizer {
ThemeMode leftBarTheme = ThemeMode.light; ThemeMode leftBarTheme = ThemeMode.light;
ThemeMode rightBarTheme = ThemeMode.light; ThemeMode rightBarTheme = ThemeMode.light;
ThemeMode topBarTheme = ThemeMode.light; ThemeMode topBarTheme = ThemeMode.light;
ColorThemeType colorTheme = ColorThemeType.red;
bool rightBarOpen = false; bool rightBarOpen = false;
bool leftBarCondensed = false; bool leftBarCondensed = false;
@ -73,6 +73,11 @@ class ThemeCustomizer {
} }
} }
/// Public method to trigger theme updates externally
static void applyThemeChange() {
_notify();
}
static void notify() { static void notify() {
for (var value in _notifier) { for (var value in _notifier) {
value(oldInstance, instance); 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'; import 'package:flutter/material.dart';
class WaveBackground extends StatelessWidget { class RedWaveBackground extends StatelessWidget {
final Color color; final Color brandRed;
final double heightFactor; final double heightFactor;
const WaveBackground({ const RedWaveBackground({
super.key, super.key,
required this.color, required this.brandRed,
this.heightFactor = 0.2, this.heightFactor = 0.2,
}); });
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return CustomPaint( return CustomPaint(
painter: _WavePainter(color, heightFactor), painter: RedWavePainter(brandRed, heightFactor),
size: Size.infinite, size: Size.infinite,
); );
} }
} }
class _WavePainter extends CustomPainter { class RedWavePainter extends CustomPainter {
final Color color; final Color brandRed;
final double heightFactor; final double heightFactor;
_WavePainter(this.color, this.heightFactor); RedWavePainter(this.brandRed, this.heightFactor);
@override @override
void paint(Canvas canvas, Size size) { void paint(Canvas canvas, Size size) {
final paint1 = Paint() final paint1 = Paint()
..shader = LinearGradient( ..shader = LinearGradient(
colors: [ colors: [brandRed, const Color.fromARGB(255, 97, 22, 22)],
const Color(0xFF49BF3C),
const Color(0xFF81C784),
],
begin: Alignment.topLeft, begin: Alignment.topLeft,
end: Alignment.bottomRight, end: Alignment.bottomRight,
).createShader(Rect.fromLTWH(0, 0, size.width, size.height)); ).createShader(Rect.fromLTWH(0, 0, size.width, size.height));
@ -40,20 +37,26 @@ class _WavePainter extends CustomPainter {
final path1 = Path() final path1 = Path()
..moveTo(0, size.height * heightFactor) ..moveTo(0, size.height * heightFactor)
..quadraticBezierTo( ..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( ..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(size.width, 0)
..lineTo(0, 0) ..lineTo(0, 0)
..close(); ..close();
canvas.drawPath(path1, paint1); canvas.drawPath(path1, paint1);
// Secondary wave (overlay) with same green but lighter opacity final paint2 = Paint()..color = brandRed.withOpacity(0.15);
final paint2 = Paint()..color = const Color(0xFF49BF3C).withOpacity(0.15);
final path2 = Path() final path2 = Path()
..moveTo(0, size.height * (heightFactor + 0.05)) ..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(size.width, 0)
..lineTo(0, 0) ..lineTo(0, 0)
..close(); ..close();

View File

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

View File

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

View File

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

View File

@ -170,7 +170,7 @@ class _OTPLoginScreenState extends State<OTPLoginScreen> with UIMixin {
), ),
focusedBorder: OutlineInputBorder( focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12), 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, elevation: 2,
padding: MySpacing.xy(24, 16), padding: MySpacing.xy(24, 16),
borderRadiusAll: 10, borderRadiusAll: 10,
backgroundColor: contentTheme.brandRed, backgroundColor: contentTheme.primary,
child: MyText.labelMedium( child: MyText.labelMedium(
'Verify OTP', 'Verify OTP',
fontWeight: 600, 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/widgets/my_snackbar.dart';
import 'package:marco/helpers/utils/mixins/ui_mixin.dart'; import 'package:marco/helpers/utils/mixins/ui_mixin.dart';
import 'package:marco/helpers/widgets/my_text.dart'; import 'package:marco/helpers/widgets/my_text.dart';
import 'package:marco/helpers/widgets/my_spacing.dart';
class OrganizationFormBottomSheet { class OrganizationFormBottomSheet {
static void show(BuildContext context) { static void show(BuildContext context) {
@ -81,169 +82,203 @@ class _OrganizationFormState extends State<_OrganizationForm> with UIMixin {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Container( return SingleChildScrollView(
decoration: const BoxDecoration( padding: MediaQuery.of(context).viewInsets,
color: Colors.white, child: Padding(
borderRadius: BorderRadius.vertical(top: Radius.circular(20)), padding: const EdgeInsets.only(top: 60),
), child: Container(
padding: const EdgeInsets.fromLTRB(20, 20, 20, 40), decoration: BoxDecoration(
child: SingleChildScrollView( color: Colors.white,
controller: widget.scrollController, borderRadius: const BorderRadius.vertical(top: Radius.circular(24)),
child: Form( boxShadow: const [
key: validator.formKey, BoxShadow(
child: Column( color: Colors.black12,
crossAxisAlignment: CrossAxisAlignment.start, blurRadius: 12,
children: [ offset: Offset(0, -2),
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,
),
),
), ),
], ],
), ),
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: contentPadding:
const EdgeInsets.symmetric(horizontal: 16, vertical: 16), const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
border: OutlineInputBorder( border: OutlineInputBorder(
borderRadius: BorderRadius.circular(5), borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: Colors.grey[400]!), borderSide: BorderSide(color: Colors.grey[400]!),
), ),
enabledBorder: OutlineInputBorder( enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(5), borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: Colors.grey[300]!), borderSide: BorderSide(color: Colors.grey[300]!),
), ),
focusedBorder: OutlineInputBorder( focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(5), borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: contentTheme.brandRed, width: 1.5), borderSide: BorderSide(color: contentTheme.primary, width: 1.5),
), ),
errorBorder: OutlineInputBorder( errorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(5), borderRadius: BorderRadius.circular(12),
borderSide: const BorderSide(color: Colors.red), borderSide: const BorderSide(color: Colors.red),
), ),
), ),
@ -366,17 +401,17 @@ class _OrganizationFormState extends State<_OrganizationForm> with UIMixin {
contentPadding: const EdgeInsets.symmetric( contentPadding: const EdgeInsets.symmetric(
horizontal: 16, vertical: 16), horizontal: 16, vertical: 16),
border: OutlineInputBorder( border: OutlineInputBorder(
borderRadius: BorderRadius.circular(5), borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: Colors.grey[400]!), borderSide: BorderSide(color: Colors.grey[400]!),
), ),
enabledBorder: OutlineInputBorder( enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(5), borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: Colors.grey[300]!), borderSide: BorderSide(color: Colors.grey[300]!),
), ),
focusedBorder: OutlineInputBorder( focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(5), borderRadius: BorderRadius.circular(12),
borderSide: borderSide:
BorderSide(color: contentTheme.brandRed, width: 1.5), BorderSide(color: contentTheme.primary, width: 1.5),
), ),
errorText: fieldState.errorText, errorText: fieldState.errorText,
), ),
@ -385,8 +420,7 @@ class _OrganizationFormState extends State<_OrganizationForm> with UIMixin {
children: [ children: [
MyText.bodyMedium( MyText.bodyMedium(
selectedValue ?? 'Select $label', selectedValue ?? 'Select $label',
color: color: selectedValue == null ? Colors.grey : Colors.black,
selectedValue == null ? Colors.grey : Colors.black,
), ),
const Icon(Icons.arrow_drop_down, color: Colors.grey), 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) ...[ if (note.isActive) ...[
IconButton( IconButton(
icon: Icon(isEditing ? Icons.close : Icons.edit_outlined, icon: Icon(isEditing ? Icons.close : Icons.edit ,
color: Colors.indigo, size: 18), color: Colors.indigo, size: 18),
splashRadius: 18, splashRadius: 18,
onPressed: () { onPressed: () {
@ -143,7 +143,7 @@ class _NotesViewState extends State<NotesView> with UIMixin {
}, },
), ),
IconButton( IconButton(
icon: const Icon(Icons.delete_outline, icon: const Icon(Icons.delete,
size: 18, color: Colors.red), size: 18, color: Colors.red),
splashRadius: 18, splashRadius: 18,
onPressed: () async { onPressed: () async {
@ -216,7 +216,7 @@ class _NotesViewState extends State<NotesView> with UIMixin {
await controller.updateNote(updated); await controller.updateNote(updated);
controller.editingNoteId.value = null; controller.editingNoteId.value = null;
}, },
icon: const Icon(Icons.check_circle_outline, icon: const Icon(Icons.check_circle,
color: Colors.white), color: Colors.white),
label: MyText.bodyMedium( label: MyText.bodyMedium(
"Save", "Save",

View File

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

View File

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