Refactor onboarding and authentication UI components
- Updated password field to use a consistent dark color. - Enhanced onboarding controller with a new text page controller for synchronized page views. - Improved location warning screen to utilize a centralized error snackbar. - Refined onboarding screen layout for better responsiveness and added text synchronization. - Revamped role selection screen with improved UI and dynamic role card rendering. - Updated welcome screen text for clarity on location services. - Adjusted custom text field to align with new color scheme. - Simplified color constants for a more cohesive theme. - Introduced a new circular full-screen loader for better loading experience.
This commit is contained in:
parent
1a6eefe6e3
commit
6a4813c15e
|
@ -138,7 +138,7 @@ class AuthenticationRepository extends GetxController {
|
|||
bool biometricSuccess = await attemptBiometricLogin();
|
||||
if (!biometricSuccess) {
|
||||
if (isFirstTime) {
|
||||
_navigateToRoute(AppRoutes.signIn);
|
||||
_navigateToRoute(AppRoutes.onboarding);
|
||||
} else {
|
||||
_navigateToRoute(AppRoutes.onboarding);
|
||||
}
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_tabler_icons/flutter_tabler_icons.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:sigap/src/features/auth/presentasion/controllers/signin/signin_controller.dart';
|
||||
|
@ -9,6 +8,7 @@ import 'package:sigap/src/features/auth/presentasion/widgets/auth_header.dart';
|
|||
import 'package:sigap/src/features/auth/presentasion/widgets/password_field.dart';
|
||||
import 'package:sigap/src/features/auth/presentasion/widgets/social_button.dart';
|
||||
import 'package:sigap/src/shared/widgets/text/custom_text_field.dart';
|
||||
import 'package:sigap/src/utils/helpers/helper_functions.dart';
|
||||
import 'package:sigap/src/utils/validators/validation.dart';
|
||||
|
||||
class SignInScreen extends StatelessWidget {
|
||||
|
@ -20,23 +20,13 @@ class SignInScreen extends StatelessWidget {
|
|||
final formKey = GlobalKey<FormState>();
|
||||
|
||||
// Get the controller - use Get.put to ensure it's initialized
|
||||
final controller = Get.put(SignInController());
|
||||
final controller = Get.find<SignInController>();
|
||||
|
||||
// Check if dark mode is enabled
|
||||
final isDarkMode = Theme.of(context).brightness == Brightness.dark;
|
||||
|
||||
// Set system overlay style based on theme
|
||||
SystemChrome.setSystemUIOverlayStyle(
|
||||
SystemUiOverlayStyle(
|
||||
statusBarColor: Colors.transparent,
|
||||
statusBarIconBrightness:
|
||||
isDarkMode ? Brightness.light : Brightness.dark,
|
||||
),
|
||||
);
|
||||
final isDarkMode = THelperFunctions.isDarkMode(context);
|
||||
|
||||
return Scaffold(
|
||||
// Use dynamic background color from theme
|
||||
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
||||
body: SafeArea(
|
||||
child: SingleChildScrollView(
|
||||
child: Padding(
|
||||
|
|
|
@ -33,7 +33,7 @@ class FormRegistrationScreen extends StatelessWidget {
|
|||
);
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: dark ? TColors.darkContainer : TColors.lightContainer,
|
||||
backgroundColor: dark ? TColors.dark : TColors.lightContainer,
|
||||
appBar: _buildAppBar(context, dark),
|
||||
body: Obx(() {
|
||||
// Show loading state while controller initializes
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_svg/svg.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
@ -25,14 +24,6 @@ class SignupWithRoleScreen extends StatelessWidget {
|
|||
final theme = Theme.of(context);
|
||||
final isDark = THelperFunctions.isDarkMode(context);
|
||||
|
||||
// Set system overlay style
|
||||
SystemChrome.setSystemUIOverlayStyle(
|
||||
SystemUiOverlayStyle(
|
||||
statusBarColor: Colors.transparent,
|
||||
statusBarIconBrightness: isDark ? Brightness.light : Brightness.dark,
|
||||
),
|
||||
);
|
||||
|
||||
return Scaffold(
|
||||
body: Obx(
|
||||
() => NestedScrollView(
|
||||
|
@ -223,7 +214,7 @@ class SignupWithRoleScreen extends StatelessWidget {
|
|||
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: isDark ? TColors.darkContainer : TColors.lightContainer,
|
||||
color: isDark ? TColors.dark : TColors.lightContainer,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.05),
|
||||
|
@ -245,7 +236,7 @@ class SignupWithRoleScreen extends StatelessWidget {
|
|||
child: Container(
|
||||
height: 60,
|
||||
decoration: BoxDecoration(
|
||||
color: isDark ? TColors.darkContainer : TColors.lightContainer,
|
||||
color: isDark ? TColors.dark : TColors.lightContainer,
|
||||
borderRadius: BorderRadius.circular(TSizes.borderRadiusLg),
|
||||
),
|
||||
child: Row(
|
||||
|
@ -511,7 +502,7 @@ class SignupWithRoleScreen extends StatelessWidget {
|
|||
text: 'Google',
|
||||
iconImage: TImages.googleIcon,
|
||||
onPressed: () => controller.signInWithGoogle(),
|
||||
backgroundColor: isDark ? TColors.darkContainer : Colors.white,
|
||||
backgroundColor: isDark ? TColors.dark : Colors.white,
|
||||
foregroundColor: isDark ? TColors.white : TColors.dark,
|
||||
borderColor: isDark ? Colors.transparent : Colors.grey.shade300,
|
||||
),
|
||||
|
@ -523,7 +514,7 @@ class SignupWithRoleScreen extends StatelessWidget {
|
|||
text: 'Facebook',
|
||||
iconImage: TImages.facebookIcon,
|
||||
onPressed: () => controller.signInWithFacebook(),
|
||||
backgroundColor: isDark ? TColors.darkContainer : Colors.white,
|
||||
backgroundColor: isDark ? TColors.dark : Colors.white,
|
||||
foregroundColor: isDark ? TColors.white : TColors.dark,
|
||||
borderColor: isDark ? Colors.transparent : Colors.grey.shade300,
|
||||
),
|
||||
|
|
|
@ -82,7 +82,7 @@ class PasswordField extends StatelessWidget {
|
|||
),
|
||||
filled: true,
|
||||
fillColor:
|
||||
isDark ? TColors.darkContainer : TColors.lightContainer,
|
||||
isDark ? TColors.dark : TColors.lightContainer,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(TSizes.inputFieldRadius),
|
||||
borderSide: BorderSide(color: TColors.borderPrimary, width: 1),
|
||||
|
|
|
@ -1,13 +1,10 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:get_storage/get_storage.dart';
|
||||
import 'package:logger/logger.dart';
|
||||
import 'package:sigap/src/cores/services/location_service.dart';
|
||||
import 'package:sigap/src/features/onboarding/data/dummy/onboarding_dummy.dart';
|
||||
import 'package:sigap/src/utils/constants/app_routes.dart';
|
||||
import 'package:sigap/src/utils/constants/image_strings.dart';
|
||||
import 'package:sigap/src/utils/popups/full_screen_loader.dart';
|
||||
import 'package:sigap/src/utils/popups/loaders.dart';
|
||||
import 'package:sigap/src/utils/popups/circular_full_screen_loader.dart';
|
||||
|
||||
class OnboardingController extends GetxController
|
||||
with GetSingleTickerProviderStateMixin {
|
||||
|
@ -23,6 +20,8 @@ class OnboardingController extends GetxController
|
|||
// Observable variables
|
||||
final RxInt currentIndex = 0.obs;
|
||||
final PageController pageController = PageController(initialPage: 0);
|
||||
// Tambahkan controller untuk PageView teks
|
||||
final PageController textPageController = PageController(initialPage: 0);
|
||||
final RxBool isLocationChecking = false.obs;
|
||||
|
||||
// Animation controllers
|
||||
|
@ -57,6 +56,7 @@ class OnboardingController extends GetxController
|
|||
@override
|
||||
void onClose() {
|
||||
pageController.dispose();
|
||||
textPageController.dispose(); // dispose controller baru
|
||||
animationController.dispose();
|
||||
super.onClose();
|
||||
}
|
||||
|
@ -66,6 +66,7 @@ class OnboardingController extends GetxController
|
|||
currentIndex.value = index;
|
||||
animationController.reset();
|
||||
animationController.forward();
|
||||
// Sinkronisasi textPageController dilakukan di widget, tidak perlu di sini
|
||||
}
|
||||
|
||||
// Method to go to next page
|
||||
|
@ -97,53 +98,40 @@ class OnboardingController extends GetxController
|
|||
isLocationChecking.value = true;
|
||||
|
||||
try {
|
||||
|
||||
TFullScreenLoader.openLoadingDialog(
|
||||
'Checking location...',
|
||||
TImages.loader,
|
||||
);
|
||||
TCircularFullScreenLoader.openLoadingDialog(text: 'Checking location...');
|
||||
|
||||
// Verify location is valid (in Jember and not mocked)
|
||||
final isLocationValid =
|
||||
await _locationService.isLocationValidForFeature();
|
||||
|
||||
TFullScreenLoader.stopLoading();
|
||||
|
||||
Logger().i('isFirstTime before: ${storage.read('isFirstTime')}');
|
||||
TCircularFullScreenLoader.stopLoading();
|
||||
|
||||
storage.write('isFirstTime', false);
|
||||
|
||||
Logger().i('isFirstTime after: ${storage.read('isFirstTime')}');
|
||||
|
||||
if (isLocationValid) {
|
||||
// If location is valid, proceed to role selection
|
||||
|
||||
Get.offAllNamed(AppRoutes.signupWithRole);
|
||||
|
||||
// TLoaders.successSnackBar(
|
||||
// title: 'Location Valid',
|
||||
// message: 'Checking location was successful',
|
||||
// );
|
||||
Get.offAllNamed(AppRoutes.roleSelection);
|
||||
|
||||
// Store isfirstTime to false in storage
|
||||
} else {
|
||||
// If location is invalid, show warning screen
|
||||
// TLoaders.errorSnackBar(
|
||||
// title: 'Location Invalid',
|
||||
// message:
|
||||
// 'Please enable location services or ensure you are in Jember area',
|
||||
// );
|
||||
|
||||
Get.offAllNamed(AppRoutes.locationWarning);
|
||||
|
||||
TLoaders.errorSnackBar(
|
||||
title: 'Location Invalid',
|
||||
message:
|
||||
'Please enable location services or ensure you are in Jember area',
|
||||
);
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
// If there's an error, show the location warning screen
|
||||
// TFullScreenLoader.stopLoading();
|
||||
Get.offAllNamed(AppRoutes.locationWarning);
|
||||
} finally {
|
||||
isLocationChecking.value = false;
|
||||
// TFullScreenLoader.stopLoading();
|
||||
// TCircularFullScreenLoader.stopLoading();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -5,6 +5,7 @@ import 'package:sigap/src/cores/services/location_service.dart';
|
|||
import 'package:sigap/src/utils/constants/app_routes.dart';
|
||||
import 'package:sigap/src/utils/constants/image_strings.dart';
|
||||
import 'package:sigap/src/utils/constants/sizes.dart';
|
||||
import 'package:sigap/src/utils/popups/loaders.dart';
|
||||
|
||||
class LocationWarningScreen extends StatelessWidget {
|
||||
const LocationWarningScreen({super.key});
|
||||
|
@ -76,14 +77,12 @@ class LocationWarningScreen extends StatelessWidget {
|
|||
if (isValid) {
|
||||
Get.offAllNamed(AppRoutes.roleSelection);
|
||||
} else {
|
||||
Get.snackbar(
|
||||
'Location Issue',
|
||||
'Your location is still not valid. Please ensure you are in Jember with location services enabled.',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
backgroundColor: theme.colorScheme.error.withOpacity(
|
||||
0.1,
|
||||
),
|
||||
colorText: theme.colorScheme.error,
|
||||
TLoaders.errorSnackBar(
|
||||
title: 'Location Verification Failed',
|
||||
message:
|
||||
isMocked
|
||||
? 'Please disable mock location apps to continue.'
|
||||
: 'Ensure you are within Jember region and location services are enabled.',
|
||||
);
|
||||
}
|
||||
},
|
||||
|
|
|
@ -14,6 +14,9 @@ class OnboardingScreen extends StatelessWidget {
|
|||
// Get the controller
|
||||
final controller = Get.find<OnboardingController>();
|
||||
|
||||
// Tambahkan controller untuk PageView teks
|
||||
final textPageController = controller.textPageController;
|
||||
|
||||
// Get screen dimensions for responsive design
|
||||
final size = MediaQuery.of(context).size;
|
||||
final isSmallScreen = size.height < 700;
|
||||
|
@ -22,105 +25,22 @@ class OnboardingScreen extends StatelessWidget {
|
|||
bool isDark = THelperFunctions.isDarkMode(context);
|
||||
|
||||
return Scaffold(
|
||||
body: SafeArea(
|
||||
child: Column(
|
||||
body: Stack(
|
||||
children: [
|
||||
// Skip button
|
||||
Align(
|
||||
alignment: Alignment.topRight,
|
||||
|
||||
// Top bar: indicator & skip
|
||||
SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(TSizes.md),
|
||||
child: TextButton(
|
||||
onPressed: controller.skipOnboarding,
|
||||
child: Text(
|
||||
'Skip',
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: TSizes.md,
|
||||
vertical: TSizes.md,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Page view for slides
|
||||
Expanded(
|
||||
flex: 3,
|
||||
child: PageView.builder(
|
||||
controller: controller.pageController,
|
||||
itemCount: contents.length,
|
||||
onPageChanged: controller.onPageChanged,
|
||||
itemBuilder: (_, i) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(TSizes.lg),
|
||||
child: Column(
|
||||
children: [
|
||||
// Animated image
|
||||
Expanded(
|
||||
flex: isSmallScreen ? 3 : 4,
|
||||
child: FadeTransition(
|
||||
opacity: controller.fadeAnimation,
|
||||
child: SlideTransition(
|
||||
position: controller.slideAnimation,
|
||||
child: SvgPicture.asset(
|
||||
contents[i].image,
|
||||
fit: BoxFit.contain,
|
||||
height: isSmallScreen ? 200 : 300,
|
||||
// Make SVG adapt to dark Theme.of(context) if needed
|
||||
colorFilter:
|
||||
isDark
|
||||
? const ColorFilter.mode(
|
||||
Colors.white,
|
||||
BlendMode.srcIn,
|
||||
)
|
||||
: null,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
SizedBox(height: isSmallScreen ? TSizes.md : TSizes.xl),
|
||||
|
||||
// Animated title
|
||||
FadeTransition(
|
||||
opacity: controller.fadeAnimation,
|
||||
child: SlideTransition(
|
||||
position: controller.slideAnimation,
|
||||
child: Text(
|
||||
contents[i].title,
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.headlineMedium,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
SizedBox(height: isSmallScreen ? TSizes.sm : TSizes.lg),
|
||||
|
||||
// Animated description
|
||||
FadeTransition(
|
||||
opacity: controller.fadeAnimation,
|
||||
child: SlideTransition(
|
||||
position: controller.slideAnimation,
|
||||
child: Text(
|
||||
contents[i].description,
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
// Indicator and buttons
|
||||
Expanded(
|
||||
flex: 1,
|
||||
child: Column(
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
// Dot indicators
|
||||
Obx(
|
||||
() => Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: List.generate(
|
||||
contents.length,
|
||||
(index) => buildDot(
|
||||
|
@ -132,27 +52,145 @@ class OnboardingScreen extends StatelessWidget {
|
|||
),
|
||||
),
|
||||
),
|
||||
|
||||
const Spacer(),
|
||||
|
||||
// Next button
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: TSizes.lg,
|
||||
vertical: TSizes.md,
|
||||
TextButton(
|
||||
onPressed: controller.skipOnboarding,
|
||||
child: Text(
|
||||
'Skip',
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
child: Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: Container(
|
||||
margin: const EdgeInsets.only(right: TSizes.sm),
|
||||
child: FloatingActionButton(
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
// Main content
|
||||
Column(
|
||||
children: [
|
||||
// Spacer for top bar
|
||||
SizedBox(height: size.height * 0.08),
|
||||
// Image in the center of gray area
|
||||
Expanded(
|
||||
flex: 5,
|
||||
child: PageView.builder(
|
||||
controller: controller.pageController,
|
||||
itemCount: contents.length,
|
||||
onPageChanged: (i) {
|
||||
controller.onPageChanged(i);
|
||||
// Sinkronkan page pada textPageController
|
||||
textPageController.jumpToPage(i);
|
||||
},
|
||||
itemBuilder: (_, i) {
|
||||
return Center(
|
||||
child: FadeTransition(
|
||||
opacity: controller.fadeAnimation,
|
||||
child: SlideTransition(
|
||||
position: controller.slideAnimation,
|
||||
child: SvgPicture.asset(
|
||||
contents[i].image,
|
||||
fit: BoxFit.contain,
|
||||
height: isSmallScreen ? 200 : 300,
|
||||
colorFilter:
|
||||
isDark
|
||||
? ColorFilter.mode(
|
||||
Theme.of(context).colorScheme.onSurface,
|
||||
BlendMode.srcIn,
|
||||
)
|
||||
: null,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
// White rounded bottom sheet tanpa tombol Next
|
||||
Container(
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
borderRadius: const BorderRadius.only(
|
||||
topLeft: Radius.circular(32),
|
||||
topRight: Radius.circular(32),
|
||||
),
|
||||
),
|
||||
padding: const EdgeInsets.fromLTRB(24, 32, 24, 32),
|
||||
child: SizedBox(
|
||||
height: isSmallScreen ? 172 : 212, // dikurangi tinggi tombol
|
||||
child: PageView.builder(
|
||||
|
||||
controller: textPageController,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemCount: contents.length,
|
||||
itemBuilder: (_, i) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// Title
|
||||
FadeTransition(
|
||||
opacity: controller.fadeAnimation,
|
||||
child: SlideTransition(
|
||||
position: controller.slideAnimation,
|
||||
child: Text(
|
||||
contents[i].title,
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.headlineMedium
|
||||
?.copyWith(fontWeight: FontWeight.w700),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
// Description
|
||||
FadeTransition(
|
||||
opacity: controller.fadeAnimation,
|
||||
child: SlideTransition(
|
||||
position: controller.slideAnimation,
|
||||
child: Text(
|
||||
contents[i].description,
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
),
|
||||
),
|
||||
// Hapus SizedBox(height: 28) dan tombol Next dari sini
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
// Tombol Next di posisi paling bawah
|
||||
Positioned(
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(
|
||||
24,
|
||||
0,
|
||||
24,
|
||||
16,
|
||||
), // kurangi bottom padding agar tidak terlalu mepet
|
||||
child: SafeArea(
|
||||
top: false,
|
||||
minimum: const EdgeInsets.only(
|
||||
bottom: 8,
|
||||
), // beri ruang ekstra agar tidak tertutup gesture bar
|
||||
child: SizedBox(
|
||||
width: double.infinity,
|
||||
height: 56, // tambah tinggi agar text tidak terpotong
|
||||
child: ElevatedButton(
|
||||
onPressed: controller.nextPage,
|
||||
backgroundColor: Theme.of(context).primaryColor,
|
||||
elevation: TSizes.cardElevation,
|
||||
child: Icon(
|
||||
Icons.chevron_right,
|
||||
color: Theme.of(context).colorScheme.onPrimary,
|
||||
size: TSizes.iconLg - 2,
|
||||
style: ElevatedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
textStyle: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
child: const Text('Next', overflow: TextOverflow.visible),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -160,10 +198,6 @@ class OnboardingScreen extends StatelessWidget {
|
|||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -176,12 +210,12 @@ class OnboardingScreen extends StatelessWidget {
|
|||
) {
|
||||
return AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
margin: const EdgeInsets.only(right: TSizes.sm),
|
||||
height: TSizes.xs * 2,
|
||||
width: currentIndex == index ? TSizes.md + 8 : TSizes.xs * 2,
|
||||
margin: const EdgeInsets.only(right: 6),
|
||||
height: 6,
|
||||
width: currentIndex == index ? 22 : 10,
|
||||
decoration: BoxDecoration(
|
||||
color: currentIndex == index ? activeColor : inactiveColor,
|
||||
borderRadius: BorderRadius.circular(TSizes.xs),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:flutter_tabler_icons/flutter_tabler_icons.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:sigap/src/features/auth/presentasion/widgets/auth_button.dart';
|
||||
import 'package:sigap/src/features/auth/presentasion/widgets/auth_header.dart';
|
||||
import 'package:sigap/src/features/onboarding/presentasion/controllers/role_selection_controller.dart';
|
||||
import 'package:sigap/src/features/onboarding/presentasion/pages/role-selection/widgets/role_card.dart';
|
||||
import 'package:sigap/src/utils/constants/image_strings.dart';
|
||||
import 'package:sigap/src/utils/constants/sizes.dart';
|
||||
import 'package:sigap/src/utils/helpers/helper_functions.dart';
|
||||
import 'package:sigap/src/utils/loaders/shimmer.dart';
|
||||
|
||||
class RoleSelectionScreen extends StatelessWidget {
|
||||
|
@ -13,142 +14,475 @@ class RoleSelectionScreen extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Get the controller
|
||||
final controller = Get.find<RoleSelectionController>();
|
||||
final isDark = THelperFunctions.isDarkMode(Get.context!);
|
||||
|
||||
// Get theme
|
||||
final theme = Theme.of(context);
|
||||
|
||||
// Set system overlay style
|
||||
SystemChrome.setSystemUIOverlayStyle(
|
||||
const SystemUiOverlayStyle(
|
||||
SystemUiOverlayStyle(
|
||||
statusBarColor: Colors.transparent,
|
||||
statusBarIconBrightness: Brightness.dark,
|
||||
statusBarIconBrightness: isDark ? Brightness.light : Brightness.dark,
|
||||
),
|
||||
);
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: theme.scaffoldBackgroundColor,
|
||||
appBar: AppBar(
|
||||
backgroundColor: theme.appBarTheme.backgroundColor,
|
||||
elevation: 0,
|
||||
title: Text(
|
||||
'Choose Role',
|
||||
style: Theme.of(context).appBarTheme.titleTextStyle,
|
||||
),
|
||||
centerTitle: true,
|
||||
),
|
||||
backgroundColor:
|
||||
isDark ? const Color(0xFF18191A) : const Color(0xFFF7F7F5),
|
||||
body: SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(TSizes.defaultSpace),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Header
|
||||
const AuthHeader(
|
||||
title: 'Select Your Role',
|
||||
subtitle: 'Choose the role that best describes your position',
|
||||
),
|
||||
|
||||
// Role list
|
||||
Expanded(
|
||||
child: Obx(() {
|
||||
// Check if roles are loading
|
||||
if (controller.isRolesLoading.value) {
|
||||
// Show shimmer placeholder cards
|
||||
return ListView.builder(
|
||||
itemCount: 2,
|
||||
itemBuilder:
|
||||
(_, __) => Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
bottom: TSizes.spaceBtwItems,
|
||||
),
|
||||
child: Container(
|
||||
margin: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(
|
||||
TSizes.borderRadiusMd,
|
||||
color: isDark ? const Color(0xFF232425) : Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color:
|
||||
isDark
|
||||
? const Color(0xFF343536)
|
||||
: const Color(0xFFE5E5E5),
|
||||
width: 1,
|
||||
),
|
||||
border: Border.all(color: theme.dividerColor),
|
||||
boxShadow:
|
||||
isDark
|
||||
? []
|
||||
: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.04),
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(TSizes.md),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Icon shimmer
|
||||
const TShimmerEffect(
|
||||
width: 40,
|
||||
height: 40,
|
||||
radius: TSizes.borderRadiusSm,
|
||||
),
|
||||
const SizedBox(width: TSizes.md),
|
||||
// Text shimmer
|
||||
Expanded(
|
||||
padding: const EdgeInsets.all(32),
|
||||
child: Column(
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Role title shimmer
|
||||
const TShimmerEffect(
|
||||
width: 150,
|
||||
height: 24,
|
||||
radius: TSizes.borderRadiusXs,
|
||||
),
|
||||
const SizedBox(height: TSizes.sm),
|
||||
|
||||
// Role description shimmer
|
||||
const TShimmerEffect(
|
||||
width: double.infinity,
|
||||
height: 16,
|
||||
radius: TSizes.borderRadiusXs,
|
||||
),
|
||||
const SizedBox(height: TSizes.xs),
|
||||
const TShimmerEffect(
|
||||
width: 200,
|
||||
height: 16,
|
||||
radius: TSizes.borderRadiusXs,
|
||||
),
|
||||
],
|
||||
// Top illustration that changes based on selection
|
||||
Expanded(
|
||||
flex: 3,
|
||||
child: Obx(
|
||||
() => _buildTopIllustration(controller, isDark),
|
||||
),
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 40),
|
||||
Text(
|
||||
'Choose your role',
|
||||
style: TextStyle(
|
||||
fontSize: 32,
|
||||
fontWeight: FontWeight.w700,
|
||||
color:
|
||||
isDark ? Colors.white : const Color(0xFF2F2F2F),
|
||||
letterSpacing: -0.5,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Obx(
|
||||
() => Text(
|
||||
_getSubtitleText(controller, isDark),
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color:
|
||||
isDark
|
||||
? Colors.white70
|
||||
: const Color(0xFF6B6B6B),
|
||||
height: 1.5,
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
const SizedBox(height: 48),
|
||||
Obx(() {
|
||||
if (controller.isRolesLoading.value) {
|
||||
return _buildShimmerCards(isDark);
|
||||
} else {
|
||||
// Show dynamic cards from database
|
||||
return ListView.builder(
|
||||
itemCount: controller.roles.length,
|
||||
itemBuilder: (context, index) {
|
||||
final role = controller.roles[index];
|
||||
return Obx(
|
||||
() => RoleCard(
|
||||
role: role,
|
||||
isSelected:
|
||||
controller.selectedRole.value?.id == role.id,
|
||||
onTap: () => controller.selectRole(role),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
return _buildRoleCards(controller, isDark);
|
||||
}
|
||||
}),
|
||||
),
|
||||
|
||||
// Continue button
|
||||
const SizedBox(height: 48),
|
||||
Obx(
|
||||
() => AuthButton(
|
||||
text: 'Continue',
|
||||
onPressed: controller.continueWithRole,
|
||||
isLoading: controller.isLoading.value,
|
||||
() => SizedBox(
|
||||
width: double.infinity,
|
||||
height: 48,
|
||||
child: ElevatedButton(
|
||||
onPressed:
|
||||
controller.selectedRole.value != null
|
||||
? controller.continueWithRole
|
||||
: null,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor:
|
||||
isDark
|
||||
? Colors.white
|
||||
: const Color(0xFF2F2F2F),
|
||||
foregroundColor:
|
||||
isDark ? Colors.black : Colors.white,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
elevation: 0,
|
||||
disabledBackgroundColor:
|
||||
isDark
|
||||
? const Color(0xFF343536)
|
||||
: const Color(0xFFF1F1F1),
|
||||
disabledForegroundColor: const Color(0xFFB0B0B0),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 0,
|
||||
horizontal: 0,
|
||||
), // reset padding
|
||||
minimumSize: const Size(0, 48), // ensure height
|
||||
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
),
|
||||
child:
|
||||
controller.isLoading.value
|
||||
? SizedBox(
|
||||
child: CircularProgressIndicator(
|
||||
color:
|
||||
isDark
|
||||
? Colors.black
|
||||
: Colors.white,
|
||||
strokeWidth: 2,
|
||||
),
|
||||
)
|
||||
: FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
child: Text(
|
||||
'Get started',
|
||||
style: TextStyle(
|
||||
fontSize: TSizes.fontSizeMd,
|
||||
fontWeight: FontWeight.w500,
|
||||
letterSpacing: 0.1,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTopIllustration(
|
||||
RoleSelectionController controller,
|
||||
bool isDark,
|
||||
) {
|
||||
// Default illustration when no role is selected
|
||||
if (controller.selectedRole.value == null) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: isDark ? const Color(0xFF232425) : const Color(0xFFFAFAFA),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color: isDark ? const Color(0xFF343536) : const Color(0xFFEEEEEE),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: SvgPicture.asset(
|
||||
TImages.homeOffice,
|
||||
width: double.infinity,
|
||||
height: double.infinity,
|
||||
fit: BoxFit.cover,
|
||||
colorFilter:
|
||||
isDark
|
||||
? const ColorFilter.mode(Colors.white70, BlendMode.srcIn)
|
||||
: null,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Change illustration based on selected role (viewer/officer only)
|
||||
final name = controller.selectedRole.value?.name.toLowerCase() ?? '';
|
||||
String asset;
|
||||
bool isSvg = false;
|
||||
|
||||
if (name.contains('officer')) {
|
||||
asset = isDark ? TImages.customerSupportDark : TImages.customerSupport;
|
||||
} else {
|
||||
asset = isDark ? TImages.communication : TImages.communication;
|
||||
}
|
||||
|
||||
if (asset.toLowerCase().endsWith('.svg')) {
|
||||
isSvg = true;
|
||||
}
|
||||
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: isDark ? const Color(0xFF232425) : const Color(0xFFFAFAFA),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color: isDark ? const Color(0xFF343536) : const Color(0xFFEEEEEE),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child:
|
||||
isSvg
|
||||
? SvgPicture.asset(
|
||||
asset,
|
||||
width: double.infinity,
|
||||
height: double.infinity,
|
||||
fit: BoxFit.cover,
|
||||
colorFilter:
|
||||
isDark
|
||||
? const ColorFilter.mode(
|
||||
Colors.white70,
|
||||
BlendMode.srcIn,
|
||||
)
|
||||
: null,
|
||||
)
|
||||
: Image.asset(
|
||||
asset,
|
||||
width: double.infinity,
|
||||
height: double.infinity,
|
||||
fit: BoxFit.cover,
|
||||
color: isDark ? Colors.white70 : null,
|
||||
colorBlendMode: isDark ? BlendMode.srcIn : null,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String _getSubtitleText(RoleSelectionController controller, bool isDark) {
|
||||
if (controller.selectedRole.value == null) {
|
||||
return isDark
|
||||
? 'Select your role to start your journey.\nYou can always change it later.'
|
||||
: 'Select your role to get started with\nyour learning journey.';
|
||||
}
|
||||
|
||||
final selectedRole = controller.selectedRole.value!.name.toLowerCase();
|
||||
if (selectedRole.contains('officer')) {
|
||||
return 'You will manage crime related tasks, handle reports, and ensure the safety of the community.';
|
||||
} else {
|
||||
return 'You have access to panic button for our early warning system. ';
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildRoleCards(RoleSelectionController controller, bool isDark) {
|
||||
return Row(
|
||||
children: [
|
||||
// First role card
|
||||
Expanded(
|
||||
child: Obx(
|
||||
() => _buildRoleCard(
|
||||
title:
|
||||
controller.roles.isNotEmpty
|
||||
? controller.roles[0].name
|
||||
: 'Officer',
|
||||
isSelected:
|
||||
controller.selectedRole.value?.id ==
|
||||
(controller.roles.isNotEmpty ? controller.roles[0].id : null),
|
||||
onTap:
|
||||
() =>
|
||||
controller.roles.isNotEmpty
|
||||
? controller.selectRole(controller.roles[0])
|
||||
: null,
|
||||
illustration: _buildCardOfficerIllustration(isDark),
|
||||
isDark: isDark,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
// Second role card
|
||||
Expanded(
|
||||
child: Obx(
|
||||
() => _buildRoleCard(
|
||||
title:
|
||||
controller.roles.length > 1
|
||||
? controller.roles[1].name
|
||||
: 'Viewer',
|
||||
isSelected:
|
||||
controller.selectedRole.value?.id ==
|
||||
(controller.roles.length > 1 ? controller.roles[1].id : null),
|
||||
onTap:
|
||||
() =>
|
||||
controller.roles.length > 1
|
||||
? controller.selectRole(controller.roles[1])
|
||||
: null,
|
||||
illustration: _buildCardViewerIllustration(isDark),
|
||||
isDark: isDark,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildRoleCard({
|
||||
required String title,
|
||||
required bool isSelected,
|
||||
required VoidCallback? onTap,
|
||||
required Widget illustration,
|
||||
required bool isDark,
|
||||
}) {
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
height: 140,
|
||||
decoration: BoxDecoration(
|
||||
color:
|
||||
isSelected
|
||||
? (isDark ? const Color(0xFF232425) : const Color(0xFFF8F9FA))
|
||||
: (isDark ? const Color(0xFF18191A) : Colors.white),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(
|
||||
color:
|
||||
isSelected
|
||||
? (isDark ? Colors.white : const Color(0xFF2F2F2F))
|
||||
: (isDark
|
||||
? const Color(0xFF343536)
|
||||
: const Color(0xFFE5E5E5)),
|
||||
width: isSelected ? 2 : 1,
|
||||
),
|
||||
boxShadow:
|
||||
isSelected && !isDark
|
||||
? [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.08),
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
]
|
||||
: null,
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
illustration,
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: isDark ? Colors.white : const Color(0xFF2F2F2F),
|
||||
letterSpacing: 0.1,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCardViewerIllustration(bool isDark) {
|
||||
return Container(
|
||||
width: 50,
|
||||
height: 50,
|
||||
decoration: BoxDecoration(
|
||||
color: isDark ? const Color(0xFF232425) : const Color(0xFFF5F5F5),
|
||||
borderRadius: BorderRadius.circular(25),
|
||||
border: Border.all(
|
||||
color: isDark ? const Color(0xFF343536) : const Color(0xFFE0E0E0),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: Center(
|
||||
child: Container(
|
||||
width: 24,
|
||||
height: 24,
|
||||
decoration: BoxDecoration(
|
||||
color: isDark ? Colors.white : const Color(0xFF2F2F2F),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Icon(
|
||||
TablerIcons.user_star,
|
||||
color: isDark ? Colors.black : Colors.white,
|
||||
size: 14,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCardOfficerIllustration(bool isDark) {
|
||||
return Container(
|
||||
width: 50,
|
||||
height: 50,
|
||||
decoration: BoxDecoration(
|
||||
color: isDark ? const Color(0xFF232425) : const Color(0xFFF5F5F5),
|
||||
borderRadius: BorderRadius.circular(25),
|
||||
border: Border.all(
|
||||
color: isDark ? const Color(0xFF343536) : const Color(0xFFE0E0E0),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: Center(
|
||||
child: Container(
|
||||
width: 24,
|
||||
height: 24,
|
||||
decoration: BoxDecoration(
|
||||
color: isDark ? Colors.white : const Color(0xFF2F2F2F),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Icon(
|
||||
TablerIcons.user,
|
||||
color: isDark ? Colors.black : Colors.white,
|
||||
size: 14,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildShimmerCards(bool isDark) {
|
||||
return Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Container(
|
||||
height: 140,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
color: isDark ? const Color(0xFF232425) : const Color(0xFFF5F5F5),
|
||||
border: Border.all(
|
||||
color:
|
||||
isDark ? const Color(0xFF343536) : const Color(0xFFE5E5E5),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: const TShimmerEffect(
|
||||
width: double.infinity,
|
||||
height: 140,
|
||||
radius: 8,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Container(
|
||||
height: 140,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
color: isDark ? const Color(0xFF232425) : const Color(0xFFF5F5F5),
|
||||
border: Border.all(
|
||||
color:
|
||||
isDark ? const Color(0xFF343536) : const Color(0xFFE5E5E5),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: const TShimmerEffect(
|
||||
width: double.infinity,
|
||||
height: 140,
|
||||
radius: 8,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -87,18 +87,12 @@ class WelcomeScreen extends StatelessWidget {
|
|||
child: ElevatedButton(
|
||||
onPressed: controller.getStarted,
|
||||
style: theme.elevatedButtonTheme.style,
|
||||
child: Text(
|
||||
'Get Started',
|
||||
style: theme.textTheme.titleMedium?.copyWith(
|
||||
color: theme.colorScheme.onPrimary,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
child: Text('Get Started'),
|
||||
),
|
||||
),
|
||||
SizedBox(height: isSmallScreen ? TSizes.lg : TSizes.xl),
|
||||
Text(
|
||||
'By continuing, you agree to our Terms of Service and Privacy Policy',
|
||||
'By continuing, We will check your location for our security purposes. Please ensure your location services are enabled.',
|
||||
textAlign: TextAlign.center,
|
||||
style: theme.textTheme.labelMedium,
|
||||
),
|
||||
|
|
|
@ -55,7 +55,7 @@ class CustomTextField extends StatelessWidget {
|
|||
|
||||
// Determine the effective fill color
|
||||
final Color effectiveFillColor =
|
||||
fillColor ?? (isDark ? TColors.darkContainer : TColors.lightContainer);
|
||||
fillColor ?? (isDark ? TColors.dark : TColors.lightContainer);
|
||||
|
||||
// Get the common input decoration for both cases
|
||||
final inputDecoration = _getInputDecoration(
|
||||
|
|
|
@ -1,66 +1,38 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
class TColors {
|
||||
// App theme colors
|
||||
static const Color primary = Color(
|
||||
0xFF3B82F6,
|
||||
); // From lightThemeColors primary
|
||||
static const Color secondary = Color(
|
||||
0xFFF1F5F9,
|
||||
); // From lightThemeColors secondary
|
||||
static const Color accent = Color(0xFFF1F5F9); // From lightThemeColors accent
|
||||
// Monochrome Notion-like theme colors
|
||||
static const Color primary = Color(0xFF2F2F2F); // Notion dark text
|
||||
static const Color secondary = Color(0xFFF5F5F5); // Notion light bg
|
||||
static const Color accent = Color(0xFFFAFAFA);
|
||||
|
||||
// Text colors
|
||||
static const Color textPrimary = Color(
|
||||
0xFF0B0C1E,
|
||||
); // From lightThemeColors foreground
|
||||
static const Color textSecondary = Color(
|
||||
0xFF707C91,
|
||||
); // From lightThemeColors mutedForeground
|
||||
static const Color textPrimary = Color(0xFF2F2F2F);
|
||||
static const Color textSecondary = Color(0xFF6B6B6B);
|
||||
static const Color textWhite = Colors.white;
|
||||
|
||||
// Background colors
|
||||
static const Color light = Color(
|
||||
0xFFFFFFFF,
|
||||
); // From lightThemeColors background
|
||||
static const Color dark = Color(
|
||||
0xFF0B0C1E,
|
||||
); // From darkThemeColors background
|
||||
static const Color primaryBackground = Color(
|
||||
0xFFFFFFFF,
|
||||
); // From lightThemeColors background
|
||||
static const Color light = Color(0xFFF7F7F5); // Notion off-white
|
||||
static const Color dark = Color(0xFF18191A); // Notion dark mode bg
|
||||
static const Color primaryBackground = Color(0xFFF7F7F5);
|
||||
|
||||
// Background Container colors
|
||||
static const Color lightContainer = Color(
|
||||
0xFFFFFFFF,
|
||||
); // From lightThemeColors card
|
||||
static Color darkContainer = Color(0xFF0B0C1E); // From darkThemeColors card
|
||||
static const Color lightContainer = Color(0xFFFFFFFF);
|
||||
static const Color darkContainer = Color(0xFF232425);
|
||||
|
||||
// Button colors
|
||||
static const Color buttonPrimary = Color(
|
||||
0xFF3B82F6,
|
||||
); // From lightThemeColors primary
|
||||
static const Color buttonSecondary = Color(
|
||||
0xFFF1F5F9,
|
||||
); // From lightThemeColors secondary
|
||||
static const Color buttonDisabled = Color(
|
||||
0xFF707C91,
|
||||
); // From lightThemeColors mutedForeground
|
||||
static const Color buttonPrimary = Color(0xFF2F2F2F);
|
||||
static const Color buttonSecondary = Color(0xFFF5F5F5);
|
||||
static const Color buttonDisabled = Color(0xFFB0B0B0);
|
||||
|
||||
// Border colors
|
||||
static const Color borderPrimary = Color(
|
||||
0xFFE2E8F0,
|
||||
); // From lightThemeColors border
|
||||
static const Color borderSecondary = Color(
|
||||
0xFFE2E8F0,
|
||||
); // From lightThemeColors input
|
||||
static const Color borderPrimary = Color(0xFFE5E5E5);
|
||||
static const Color borderSecondary = Color(0xFFE5E5E5);
|
||||
|
||||
// Error and validation colors
|
||||
static const Color error = Color(
|
||||
0xFFEF4444,
|
||||
); // From lightThemeColors destructive
|
||||
static const Color success = Color(0xFF38B2AC); // From darkThemeColors chart2
|
||||
static const Color warning = Color(0xFFF59E0B); // From darkThemeColors chart3
|
||||
static const Color error = Color(0xFFEF4444);
|
||||
static const Color success = Color(0xFF38B2AC);
|
||||
static const Color warning = Color(0xFFF59E0B);
|
||||
|
||||
// Neutral Shades
|
||||
static const Color black = Color(0xFF232323);
|
||||
|
@ -74,58 +46,34 @@ class TColors {
|
|||
// Additional colors
|
||||
static const Color transparent = Colors.transparent;
|
||||
|
||||
// Theme color maps for reference
|
||||
// Theme color maps for reference (monochrome)
|
||||
static const Map<String, Color> darkThemeColors = {
|
||||
'background': Color(0xFF0B0C1E),
|
||||
'foreground': Color(0xFFF8FAFC),
|
||||
'card': Color(0xFF0B0C1E),
|
||||
'cardForeground': Color(0xFFF8FAFC),
|
||||
'popover': Color(0xFF0B0C1E),
|
||||
'popoverForeground': Color(0xFFF8FAFC),
|
||||
'primary': Color(0xFF2563EB),
|
||||
'primaryForeground': Color(0xFF121826),
|
||||
'secondary': Color(0xFF1E293B),
|
||||
'secondaryForeground': Color(0xFFF8FAFC),
|
||||
'muted': Color(0xFF1E293B),
|
||||
'mutedForeground': Color(0xFFA0AEC0),
|
||||
'accent': Color(0xFF1E293B),
|
||||
'accentForeground': Color(0xFFF8FAFC),
|
||||
'destructive': Color(0xFF752727),
|
||||
'destructiveForeground': Color(0xFFF8FAFC),
|
||||
'border': Color(0xFF1E293B),
|
||||
'input': Color(0xFF1E293B),
|
||||
'ring': Color(0xFF3B5FE8),
|
||||
'chart1': Color(0xFF3B82F6),
|
||||
'chart2': Color(0xFF38B2AC),
|
||||
'chart3': Color(0xFFF59E0B),
|
||||
'chart4': Color(0xFFC084FC),
|
||||
'chart5': Color(0xFFEF476F),
|
||||
'background': Color(0xFF18191A),
|
||||
'foreground': Colors.white,
|
||||
'card': Color(0xFF232425),
|
||||
'cardForeground': Colors.white,
|
||||
'primary': Color(0xFF2F2F2F),
|
||||
'secondary': Color(0xFF232425),
|
||||
'muted': Color(0xFF343536),
|
||||
'mutedForeground': Color(0xFFB0B0B0),
|
||||
'accent': Color(0xFF343536),
|
||||
'destructive': Color(0xFFEF4444),
|
||||
'border': Color(0xFF343536),
|
||||
'input': Color(0xFF343536),
|
||||
};
|
||||
|
||||
static const Map<String, Color> lightThemeColors = {
|
||||
'background': Color(0xFFFFFFFF),
|
||||
'foreground': Color(0xFF0B0C1E),
|
||||
'background': Color(0xFFF7F7F5),
|
||||
'foreground': Color(0xFF2F2F2F),
|
||||
'card': Color(0xFFFFFFFF),
|
||||
'cardForeground': Color(0xFF0B0C1E),
|
||||
'popover': Color(0xFFFFFFFF),
|
||||
'popoverForeground': Color(0xFF0B0C1E),
|
||||
'primary': Color(0xFF3B82F6),
|
||||
'primaryForeground': Color(0xFFF8FAFC),
|
||||
'secondary': Color(0xFFF1F5F9),
|
||||
'secondaryForeground': Color(0xFF121826),
|
||||
'muted': Color(0xFFF1F5F9),
|
||||
'mutedForeground': Color(0xFF707C91),
|
||||
'accent': Color(0xFFF1F5F9),
|
||||
'accentForeground': Color(0xFF121826),
|
||||
'cardForeground': Color(0xFF2F2F2F),
|
||||
'primary': Color(0xFF2F2F2F),
|
||||
'secondary': Color(0xFFF5F5F5),
|
||||
'muted': Color(0xFFF1F1F1),
|
||||
'mutedForeground': Color(0xFFB0B0B0),
|
||||
'accent': Color(0xFFFAFAFA),
|
||||
'destructive': Color(0xFFEF4444),
|
||||
'destructiveForeground': Color(0xFFF8FAFC),
|
||||
'border': Color(0xFFE2E8F0),
|
||||
'input': Color(0xFFE2E8F0),
|
||||
'ring': Color(0xFF3B82F6),
|
||||
'chart1': Color(0xFFED6A43),
|
||||
'chart2': Color(0xFF2A9D8F),
|
||||
'chart3': Color(0xFF2A495B),
|
||||
'chart4': Color(0xFFF4C15D),
|
||||
'chart5': Color(0xFFF98F3D),
|
||||
'border': Color(0xFFE5E5E5),
|
||||
'input': Color(0xFFE5E5E5),
|
||||
};
|
||||
}
|
||||
|
|
|
@ -0,0 +1,68 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:sigap/src/utils/loaders/circular_loader.dart';
|
||||
|
||||
import '../constants/colors.dart';
|
||||
import '../helpers/helper_functions.dart';
|
||||
|
||||
/// A utility class for managing a full-screen loading dialog.
|
||||
class TCircularFullScreenLoader {
|
||||
/// Show a full-screen loader with optional [text] below the spinner.
|
||||
static void openLoadingDialog({String? text}) {
|
||||
final isDark = THelperFunctions.isDarkMode(Get.context!);
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
showDialog(
|
||||
context: Get.overlayContext!,
|
||||
barrierDismissible: false,
|
||||
builder:
|
||||
(_) => PopScope(
|
||||
canPop: false,
|
||||
child: Container(
|
||||
color: isDark ? TColors.black : TColors.white,
|
||||
width: double.infinity,
|
||||
height: double.infinity,
|
||||
child: Center(
|
||||
child: IntrinsicWidth(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
TCircularLoader(
|
||||
foregroundColor: TColors.primary,
|
||||
backgroundColor:
|
||||
isDark ? TColors.black : TColors.white,
|
||||
),
|
||||
if (text != null && text.isNotEmpty) ...[
|
||||
const SizedBox(height: 24),
|
||||
ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 220),
|
||||
child: Text(
|
||||
text,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: isDark ? Colors.white : Colors.black,
|
||||
fontWeight: FontWeight.w500,
|
||||
decoration:
|
||||
TextDecoration.none, // Hapus underline
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
softWrap: true,
|
||||
overflow: TextOverflow.visible,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/// Stop the currently open loading dialog.
|
||||
static stopLoading() {
|
||||
Navigator.of(Get.overlayContext!).pop();
|
||||
}
|
||||
}
|
|
@ -1,5 +1,6 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import '../constants/colors.dart';
|
||||
import '../helpers/helper_functions.dart';
|
||||
import '../loaders/animation_loader.dart';
|
||||
|
|
Loading…
Reference in New Issue