feat: Refactor code structure and optimize performance across multiple modules

This commit is contained in:
Achintha Isuru
2025-11-17 23:29:28 -05:00
parent 831570f2e0
commit a64cbd9edf
1508 changed files with 105319 additions and 0 deletions

View File

@@ -0,0 +1,29 @@
import 'package:flutter/material.dart';
import 'package:krow/core/presentation/styles/theme.dart';
abstract class KwBoxDecorations {
static BoxDecoration primaryLight8 = BoxDecoration(
color: AppColors.grayPrimaryFrame,
borderRadius: BorderRadius.circular(8),
);
static BoxDecoration primaryLight12 = BoxDecoration(
color: AppColors.grayPrimaryFrame,
borderRadius: BorderRadius.circular(12),
);
static BoxDecoration white24 = BoxDecoration(
color: AppColors.grayWhite,
borderRadius: BorderRadius.circular(24),
);
static BoxDecoration white12 = BoxDecoration(
color: AppColors.grayWhite,
borderRadius: BorderRadius.circular(12),
);
static BoxDecoration white8 = BoxDecoration(
color: AppColors.grayWhite,
borderRadius: BorderRadius.circular(12),
);
}

View File

@@ -0,0 +1,125 @@
import 'package:flutter/material.dart';
import 'package:krow/core/presentation/gen/fonts.gen.dart';
import 'package:krow/core/presentation/styles/theme.dart';
abstract class AppTextStyles {
static const TextStyle headingH0 = TextStyle(
fontFamily: FontFamily.poppins,
fontWeight: FontWeight.w600, // SemiBold
fontSize: 48,
color: AppColors.blackBlack);
static const TextStyle headingH1 = TextStyle(
fontFamily: FontFamily.poppins,
fontWeight: FontWeight.w600, // SemiBold
fontSize: 28,
height: 1,
color: AppColors.blackBlack);
static const TextStyle headingH2 = TextStyle(
fontFamily: FontFamily.poppins,
fontWeight: FontWeight.w500, // Medium
fontSize: 20,
color: AppColors.blackBlack);
static const TextStyle headingH3 = TextStyle(
fontFamily: FontFamily.poppins,
fontWeight: FontWeight.w500, // Medium
fontSize: 18,
height: 1,
color: AppColors.blackBlack);
static const TextStyle bodyLargeReg = TextStyle(
fontFamily: FontFamily.poppins,
fontWeight: FontWeight.w400,
// Regular
fontSize: 16,
letterSpacing: -0.5,
color: AppColors.blackBlack);
static const TextStyle bodyLargeMed = TextStyle(
fontFamily: FontFamily.poppins,
fontWeight: FontWeight.w500,
// Medium
fontSize: 16,
letterSpacing: -0.5,
color: AppColors.blackBlack);
static const TextStyle bodyMediumReg = TextStyle(
fontFamily: FontFamily.poppins,
fontWeight: FontWeight.w400,
// Regular
fontSize: 14,
letterSpacing: -0.5,
height: 1.2,
color: AppColors.blackBlack);
static const TextStyle bodyMediumMed = TextStyle(
fontFamily: FontFamily.poppins,
fontWeight: FontWeight.w500,
// Medium
fontSize: 14,
letterSpacing: -0.5,
height: 1.2,
color: AppColors.blackBlack);
static const TextStyle bodyMediumSmb = TextStyle(
fontFamily: FontFamily.poppins,
fontWeight: FontWeight.w600,
// SemiBold
letterSpacing: -0.5,
fontSize: 14,
height: 1.2,
color: AppColors.blackBlack);
static const TextStyle bodySmallReg = TextStyle(
fontFamily: FontFamily.poppins,
fontWeight: FontWeight.w400,
// Regular
fontSize: 12,
letterSpacing: -0.5,
height: 1.3,
color: AppColors.blackBlack);
static const TextStyle bodySmallMed = TextStyle(
fontFamily: FontFamily.poppins,
fontWeight: FontWeight.w500,
// Medium
fontSize: 12,
letterSpacing: -0.5,
color: AppColors.blackBlack);
static const TextStyle bodyTinyReg = TextStyle(
fontFamily: FontFamily.poppins,
fontWeight: FontWeight.w400,
// Regular
fontSize: 10,
height: 1.2,
color: AppColors.blackBlack);
static const TextStyle bodyTinyMed = TextStyle(
fontFamily: FontFamily.poppins,
fontWeight: FontWeight.w500,
// Medium
fontSize: 10,
height: 1.2,
color: AppColors.blackBlack);
static const TextStyle badgeRegular = TextStyle(
fontFamily: FontFamily.poppins,
fontWeight: FontWeight.w400, // Regular
fontSize: 12,
color: AppColors.blackBlack);
static const TextStyle captionReg = TextStyle(
fontFamily: FontFamily.poppins,
fontWeight: FontWeight.w400, // Regular
fontSize: 10,
color: AppColors.blackBlack);
static const TextStyle captionBold = TextStyle(
fontFamily: FontFamily.poppins,
fontWeight: FontWeight.w600, // SemiBold
fontSize: 10,
color: AppColors.blackBlack);
}

View File

@@ -0,0 +1,65 @@
import 'package:flutter/material.dart';
abstract class AppColors {
static const Color bgColorLight = Color(0xFFF0F2FF);
static const Color bgColorDark = Color(0xFF040B45);
static const Color blackBlack = Color(0xFF02071D);
static const Color blackGray = Color(0xFF656773);
static const Color blackCaptionText = Color(0xFFAEAEB1);
static const Color blackCaptionBlue = Color(0xFF8485A4);
static const Color primaryYellow = Color(0xFFFFF3A8);
static const Color primaryYellowDark = Color(0xFFEEE39F);
static const Color primaryYolk = Color(0xFFFFF321);
static const Color primaryBlue = Color(0xFF002AE8);
static const Color primaryMint = Color(0xFFDFEDE3);
static const Color grayWhite = Color(0xFFFFFFFF);
static const Color grayStroke = Color(0xFFA8AABD);
static const Color grayDisable = Color(0xFFD8D9E0);
static const Color grayPrimaryFrame = Color(0xFFFAFAFF);
static const Color grayTintStroke = Color(0xFFE1E2E8);
static const Color statusError = Color(0xFFF45E5E);
static const Color statusSuccess = Color(0xFF14A858);
static const Color statusWarning = Color(0xFFED7021);
static const Color statusWarningBody = Color(0xFF906F07);
static const Color statusRate = Color(0xFFF7CE39);
static const Color tintGreen = Color(0xFFF3FCF7);
static const Color tintDarkGreen = Color(0xFFC8EFDB);
static const Color tintRed = Color(0xFFFEF2F2);
static const Color tintYellow = Color(0xFFFEF7E0);
static const Color tintBlue = Color(0xFFF0F3FF);
static const Color tintDarkBlue = Color(0xFF7A88BE);
static const Color tintGray = Color(0xFFEBEBEB);
static const Color tintDarkRed = Color(0xFFFDB9B9);
static const Color tintDropDownButton = Color(0xFFBEC5FE);
static const Color tintOrange = Color(0xFFFAEBE3);
static const Color navBarDisabled = Color(0xFF5C6081);
static const Color darkBgBgElements = Color(0xFF1B1F41);
static const Color darkBgActiveButtonState = Color(0xFF252A5A);
static const Color darkBgStroke = Color(0xFF4E537E);
static const Color darkBgInactive = Color(0xFF7A7FA9);
static const Color buttonPrimaryYellowDrop = Color(0xFFFFEB6B);
static const Color buttonPrimaryYellowActive = Color(0xFFFFF7C7);
static const Color buttonPrimaryYellowActiveDrop = Color(0xFFFFF2A3);
static const Color buttonOutline = Color(0xFFBEC5FE);
static const Color buttonTertiaryActive = Color(0xFFEBEDFF);
static const Color bgProfileCard = Color(0xff405FED);
}
class KWTheme {
static ThemeData get lightTheme {
return ThemeData(
useMaterial3: true,
scaffoldBackgroundColor: AppColors.bgColorLight,
progressIndicatorTheme:
const ProgressIndicatorThemeData(color: AppColors.bgColorDark),
fontFamily: 'Poppins',
//unused
appBarTheme: const AppBarTheme(
backgroundColor: AppColors.bgColorLight,
),
colorScheme: ColorScheme.fromSwatch().copyWith(
secondary: AppColors.statusSuccess,
),
);
}
}

View File

@@ -0,0 +1,172 @@
import 'package:auto_route/auto_route.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:krow/core/application/common/str_extensions.dart';
import 'package:krow/core/application/routing/routes.gr.dart';
import 'package:krow/core/data/models/staff/pivot.dart';
import 'package:krow/core/entity/event_entity.dart';
import 'package:krow/core/entity/staff_contact_entity.dart';
import 'package:krow/core/presentation/gen/assets.gen.dart';
import 'package:krow/core/presentation/styles/kw_box_decorations.dart';
import 'package:krow/core/presentation/styles/kw_text_styles.dart';
import 'package:krow/core/presentation/styles/theme.dart';
import 'package:krow/core/presentation/widgets/staff_contact_info_popup.dart';
class AssignedStaffItemWidget extends StatelessWidget {
final StaffContact staffContact;
final String department;
const AssignedStaffItemWidget(
{super.key, required this.staffContact, required this.department});
@override
Widget build(BuildContext context) {
var showRating =
staffContact.parentPosition?.parentShift?.parentEvent?.status ==
EventStatus.completed;
return GestureDetector(
onTap: () {
StaffContactInfoPopup.show(context, staffContact, department);
},
child: Container(
height: 62,
padding: const EdgeInsets.all(12),
margin: const EdgeInsets.only(bottom: 8),
decoration: KwBoxDecorations.white8,
child: Row(
mainAxisAlignment: showRating
? MainAxisAlignment.spaceBetween
: MainAxisAlignment.start,
children: [
Flexible(
child: Row(
children: [
CachedNetworkImage(
useOldImageOnUrlChange: true,
fadeOutDuration: Duration.zero,
placeholderFadeInDuration: Duration.zero,
fadeInDuration: Duration.zero,
imageUrl: staffContact.photoUrl ?? '',
imageBuilder: (context, imageProvider) => Container(
width: 36,
height: 36,
decoration: BoxDecoration(
shape: BoxShape.circle,
image: DecorationImage(
image: imageProvider,
fit: BoxFit.cover,
),
),
),
errorWidget: (context, url, error) =>
const Icon(Icons.error)),
const Gap(12),
Flexible(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'${staffContact.firstName} ${staffContact.lastName}',
overflow: TextOverflow.ellipsis,
style: AppTextStyles.bodyMediumMed,
),
const Gap(4),
Text(
staffContact.phoneNumber ?? '',
style: AppTextStyles.bodySmallReg
.copyWith(color: AppColors.blackGray),
),
],
),
),
const Gap(8),
],
),
),
showRating ? _buildRating(context) : buildStatus()
],
),
),
);
}
Widget _buildRating(BuildContext context) {
return ValueListenableBuilder<double>(
valueListenable: staffContact.rating,
builder: (context, rating, child) {
return rating == 0
? GestureDetector(
onTap: () {
context.pushRoute(RateStaffRoute(
staff: staffContact,
));
},
child: Container(
height: 36,
alignment: Alignment.center,
padding: const EdgeInsets.symmetric(horizontal: 20),
decoration: BoxDecoration(
color: AppColors.tintBlue,
borderRadius: BorderRadius.circular(56),
border: Border.all(
color: AppColors.tintDropDownButton, width: 1),
),
child: Text('Rate',
style: AppTextStyles.bodyMediumMed
.copyWith(color: AppColors.primaryBlue)),
),
)
: Container(
height: 36,
alignment: Alignment.topRight,
child: Row(
children: [
Assets.images.icons.ratingStar.star
.svg(height: 16, width: 16),
Gap(4),
Text(rating.toStringAsFixed(1),
style: AppTextStyles.bodyMediumMed),
],
),
);
});
}
SizedBox buildStatus() {
return SizedBox(
height: 40,
child: Align(
alignment: Alignment.topCenter,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 4),
decoration: BoxDecoration(
border: Border.all(
color: staffContact.status.getStatusBorderColor(),
),
borderRadius: BorderRadius.circular(10),
),
child: Row(
children: [
Container(
width: 6,
height: 6,
padding: const EdgeInsets.all(3),
decoration: BoxDecoration(
color: staffContact.status.getStatusTextColor(),
borderRadius: BorderRadius.circular(3),
),
),
const Gap(2),
Text(
staffContact.status.formattedName.capitalize(),
style: AppTextStyles.bodyTinyMed
.copyWith(color: staffContact.status.getStatusTextColor()),
)
],
),
),
),
);
}
}

View File

@@ -0,0 +1,54 @@
import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:krow/core/presentation/styles/kw_text_styles.dart';
import 'package:krow/core/presentation/styles/theme.dart';
class IconRowInfoWidget extends StatelessWidget {
final Widget icon;
final String title;
final String value;
const IconRowInfoWidget(
{super.key,
required this.icon,
required this.title,
required this.value});
@override
Widget build(BuildContext context) {
return SizedBox(
height: 36,
child: Row(
children: [
Container(
height: 36,
width: 36,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(6),
border: Border.all(color: AppColors.grayTintStroke, width: 1),
),
child: Center(child: icon),
),
const Gap(12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(title,
style: AppTextStyles.bodySmallReg
.copyWith(color: AppColors.blackGray)),
const Gap(2),
Text(
value,
style: AppTextStyles.bodyMediumMed,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
),
)
],
),
);
}
}

View File

@@ -0,0 +1,117 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:krow/core/presentation/gen/assets.gen.dart';
import 'package:krow/core/presentation/styles/kw_text_styles.dart';
import 'package:krow/core/presentation/styles/theme.dart';
import 'package:krow/features/home/presentation/home_screen.dart';
class KwTimeSlotInput extends StatefulWidget {
const KwTimeSlotInput({
super.key,
required this.label,
required this.onChange,
required this.initialValue,
});
static final _timeFormat = DateFormat('h:mma');
final String label;
final Function(DateTime value) onChange;
final DateTime initialValue;
@override
State<KwTimeSlotInput> createState() => _KwTimeSlotInputState();
}
class _KwTimeSlotInputState extends State<KwTimeSlotInput> {
late DateTime _currentValue = widget.initialValue;
@override
void didChangeDependencies() {
_currentValue = widget.initialValue;
super.didChangeDependencies();
}
@override
void didUpdateWidget(covariant KwTimeSlotInput oldWidget) {
_currentValue = widget.initialValue;
super.didUpdateWidget(oldWidget);
}
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Padding(
padding: const EdgeInsets.only(left: 16, bottom: 4),
child: Text(
widget.label,
style: AppTextStyles.bodyTinyReg.copyWith(
color: AppColors.blackGray,
),
),
),
Material(
color: Colors.transparent,
child: InkWell(
borderRadius: const BorderRadius.all(
Radius.circular(24),
),
onTap: () {
showModalBottomSheet(
context: homeContext!,
isScrollControlled: false,
builder: (context) {
return Container(
alignment: Alignment.topCenter,
height: 216 + 48, //_kPickerHeight + 24top+24bot
child: CupertinoDatePicker(
mode: CupertinoDatePickerMode.time,
initialDateTime: _currentValue,
minuteInterval: 10,
itemExtent: 36,
onDateTimeChanged: (DateTime value) {
setState(() => _currentValue = value);
widget.onChange.call(value);
},
),
);
},
);
},
child: Container(
height: 48,
alignment: Alignment.centerLeft,
decoration: BoxDecoration(
border: Border.all(color: AppColors.grayStroke),
borderRadius: BorderRadius.circular(24),
),
child: Padding(
padding: const EdgeInsets.only(left: 16, right: 16),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
KwTimeSlotInput._timeFormat.format(_currentValue),
style: AppTextStyles.bodyMediumReg,
),
Assets.images.icons.caretDown.svg(
height: 16,
width: 16,
colorFilter: const ColorFilter.mode(
AppColors.blackBlack,
BlendMode.srcIn,
),
)
],
),
),
),
),
),
],
);
}
}

View File

@@ -0,0 +1,146 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';
import 'package:krow/core/presentation/gen/assets.gen.dart';
import 'package:krow/core/presentation/widgets/ui_kit/kw_image_animated_placeholder.dart';
import '../styles/theme.dart';
class ProfileIcon extends StatefulWidget {
const ProfileIcon({
super.key,
required this.onChange,
this.diameter = 64,
this.imagePath,
this.imageUrl,
this.imageQuality = 80,
});
final double diameter;
final String? imagePath;
final String? imageUrl;
final int imageQuality;
final Function(String) onChange;
@override
State<ProfileIcon> createState() => _ProfileIconState();
}
class _ProfileIconState extends State<ProfileIcon> {
String? _imagePath;
String? _imageUrl;
final ImagePicker _picker = ImagePicker();
Future<void> _pickImage([ImageSource source = ImageSource.gallery]) async {
final XFile? image = await _picker.pickImage(
source: source,
imageQuality: widget.imageQuality,
);
if (image != null) {
setState(() => _imagePath = image.path);
widget.onChange(image.path);
}
}
@override
void initState() {
super.initState();
_imagePath = widget.imagePath;
_imageUrl = widget.imageUrl;
}
@override
void didUpdateWidget(covariant ProfileIcon oldWidget) {
_imagePath = widget.imagePath;
_imageUrl = widget.imageUrl;
super.didUpdateWidget(oldWidget);
}
@override
Widget build(BuildContext context) {
final isImageAvailable = _imagePath != null || widget.imageUrl != null;
Widget avatar;
if (_imagePath != null) {
avatar = Image.file(
File(_imagePath!),
fit: BoxFit.cover,
frameBuilder: (_, child, frame, __) {
return frame != null ? child : const KwAnimatedImagePlaceholder();
},
);
} else if (_imageUrl != null) {
avatar = Image.network(
_imageUrl!,
fit: BoxFit.cover,
frameBuilder: (_, child, frame, __) {
return frame != null ? child : const KwAnimatedImagePlaceholder();
},
);
} else {
avatar = DecoratedBox(
decoration: const BoxDecoration(
color: AppColors.grayWhite,
),
child: Assets.images.icons.person.svg(
height: widget.diameter/2,
width: widget.diameter/2,
fit: BoxFit.scaleDown,
),
);
}
return GestureDetector(
onTap: _pickImage,
child: Stack(
clipBehavior: Clip.none,
children: [
ClipOval(
child: SizedBox(
height: widget.diameter,
width: widget.diameter,
child: avatar,
),
),
PositionedDirectional(
bottom: 0,
end: -5,
child: Container(
height: widget.diameter/4+10,
width: widget.diameter/4+10,
alignment: Alignment.center,
decoration: BoxDecoration(
color: isImageAvailable
? AppColors.grayWhite
: AppColors.bgColorDark,
shape: BoxShape.circle,
border: Border.all(
color: AppColors.bgColorLight,
width: 2,
),
),
child: isImageAvailable
? Assets.images.icons.edit.svg(
width: widget.diameter/4,
height: widget.diameter/4,
)
: Assets.images.icons.add.svg(
width: widget.diameter/4,
height: widget.diameter/4,
fit: BoxFit.scaleDown,
colorFilter: const ColorFilter.mode(
AppColors.grayWhite,
BlendMode.srcIn,
),
),
),
),
],
),
);
}
}

View File

@@ -0,0 +1,60 @@
import 'package:flutter/material.dart';
/// Helps to create a scrollable layout with two widgets in a scrollable column
/// and space between them
class ScrollLayoutHelper extends StatelessWidget {
final Widget upperWidget;
final Widget lowerWidget;
final EdgeInsets padding;
final ScrollController? controller;
final Future Function()? onRefresh;
const ScrollLayoutHelper(
{super.key,
required this.upperWidget,
required this.lowerWidget,
this.padding = const EdgeInsets.symmetric(horizontal: 16),
this.controller,
this.onRefresh});
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
Widget content = SingleChildScrollView(
physics:
onRefresh != null ? const AlwaysScrollableScrollPhysics() : null,
controller: controller,
child: ConstrainedBox(
constraints: BoxConstraints(
minHeight: constraints.maxHeight,
),
child: SafeArea(
child: Padding(
padding: padding,
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
upperWidget,
lowerWidget,
],
),
),
),
),
);
if (onRefresh != null) {
content = RefreshIndicator(
onRefresh: () => onRefresh!.call(),
child: content,
);
}
return content;
},
);
}
}

View File

@@ -0,0 +1,288 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:gap/gap.dart';
import 'package:geolocator/geolocator.dart';
import 'package:krow/core/data/models/staff/pivot.dart';
import 'package:krow/core/entity/event_entity.dart';
import 'package:krow/core/entity/staff_contact_entity.dart';
import 'package:krow/core/presentation/gen/assets.gen.dart';
import 'package:krow/core/presentation/styles/kw_box_decorations.dart';
import 'package:krow/core/presentation/styles/theme.dart';
import 'package:krow/core/presentation/widgets/staff_position_details_widget.dart';
import 'package:krow/core/presentation/widgets/ui_kit/dialogs/kw_dialog.dart';
import 'package:krow/core/presentation/widgets/ui_kit/kw_button.dart';
import 'package:krow/core/presentation/widgets/ui_kit/kw_input.dart';
import 'package:krow/core/presentation/widgets/ui_kit/kw_popup_button.dart';
import 'package:krow/features/events/domain/blocs/details/event_details_bloc.dart';
import 'package:modal_progress_hud_nsn/modal_progress_hud_nsn.dart';
class StaffContactInfoPopup {
static Future<void> show(
BuildContext context,
StaffContact staff,
String department,
) async {
var bloc = (BlocProvider.of<EventDetailsBloc>(context));
return showDialog<void>(
context: context,
builder: (context) {
return Center(
child: _StaffPopupWidget(
staff,
bloc: bloc,
department: department,
),
);
});
}
}
class _StaffPopupWidget extends StatelessWidget {
final StaffContact staff;
final EventDetailsBloc bloc;
final String department;
const _StaffPopupWidget(this.staff,
{required this.bloc, required this.department});
@override
Widget build(BuildContext context) {
return BlocConsumer<EventDetailsBloc, EventDetailsState>(
listener: (context, state) {
geoFencingServiceDialog(context, state);
},
bloc: bloc,
builder: (context, state) {
return ModalProgressHUD(
inAsyncCall: state.inLoading,
child: Center(
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 12),
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
// margin: const EdgeInsets.symmetric(horizontal: 12),
decoration: KwBoxDecorations.white24,
child: Column(
mainAxisSize: MainAxisSize.min,
children: _ongoingBtn(context),
),
),
),
);
},
);
}
List<Widget> _ongoingBtn(BuildContext context) {
return [
const Gap(32),
StaffPositionAvatar(
imageUrl: staff.photoUrl,
userName: '${staff.firstName} ${staff.lastName}',
status: staff.status,
),
const Gap(16),
StaffContactsWidget(staff: staff),
StaffPositionDetailsWidget(staff: staff),
if (staff.status == PivotStatus.confirmed &&
staff.parentPosition?.parentShift?.parentEvent?.status ==
EventStatus.active)
..._confirmedBtn(context),
if (staff.status == PivotStatus.confirmed &&
staff.parentPosition?.parentShift?.parentEvent?.status ==
EventStatus.confirmed)
_buildReplaceStaffButton(context),
if (staff.status == PivotStatus.ongoing) ...[
KwButton.primary(
onPressed: () {
_clockOutDialog(context);
},
label: 'Clock Out',
),
],
const Gap(12),
];
}
List<Widget> _confirmedBtn(BuildContext context) {
return [
KwButton.primary(
onPressed: () {
_clockinDialog(context);
},
label: 'Clock In',
),
const Gap(8),
_buildReplaceStaffButton(context),
];
}
Widget _buildReplaceStaffButton(BuildContext context) {
return KwPopUpButton(
label: 'Staff Didnt Show Up',
colorPallet: KwPopupButtonColorPallet.transparent(),
withBorder: true,
popUpPadding: 28,
items: [
KwPopUpButtonItem(
title: 'Replace Staff',
onTap: () {
// Navigator.of(context).pop();
_replaceStaff(context);
},
color: AppColors.statusError),
KwPopUpButtonItem(
title: 'Do Nothing',
onTap: () {
bloc.add(NotShowedPositionStaffEvent(staff.id));
Navigator.of(context).pop();
})
],
);
}
void _replaceStaff(BuildContext context) async {
var controller = TextEditingController();
StateSetter? setStateInDialog;
var inputError = false;
var result = await KwDialog.show(
context: context,
icon: Assets.images.icons.profileDelete,
title: 'Request Staff Replacement',
message:
'Please provide a reason for the staff replacement request in the text area below:',
state: KwDialogState.negative,
child: StatefulBuilder(builder: (context, setDialogState) {
setStateInDialog = setDialogState;
return KwTextInput(
controller: controller,
maxLength: 300,
showCounter: true,
showError: inputError,
minHeight: 144,
hintText: 'Enter your reason here...',
title: 'Reason',
);
}),
primaryButtonLabel: 'Submit Request',
onPrimaryButtonPressed: (dialogContext) {
if (controller.text.isEmpty) {
setStateInDialog?.call(() {
inputError = true;
});
return;
}
bloc.add(ReplacePositionStaffEvent(staff.id, controller.text));
Navigator.of(dialogContext).pop(true);
},
secondaryButtonLabel: 'Cancel',
);
if (result && context.mounted) {
await KwDialog.show(
context: context,
state: KwDialogState.negative,
icon: Assets.images.icons.documentUpload,
title: 'Request is Under Review',
message:
'Thank you! Your request for staff replacement is now under review. You will be notified of the outcome shortly.',
primaryButtonLabel: 'Back to Event',
onPrimaryButtonPressed: (dialogContext) {
Navigator.of(dialogContext).pop();
Navigator.of(context).maybePop();
},
);
}
}
void geoFencingServiceDialog(BuildContext context, EventDetailsState state) {
switch (state.geofencingDialogState) {
case GeofencingDialogState.tooFar:
KwDialog.show(
context: context,
icon: Assets.images.icons.alertTriangle,
state: KwDialogState.warning,
title: "You're too far",
message: 'Please move closer to the designated location.',
primaryButtonLabel: 'OK',
);
break;
case GeofencingDialogState.locationDisabled:
KwDialog.show(
context: context,
icon: Assets.images.icons.alertTriangle,
state: KwDialogState.negative,
title: 'Location Disabled',
message: 'Please enable location services to continue.',
primaryButtonLabel: 'Go to Settings',
onPrimaryButtonPressed: (dialogContext) {
Geolocator.openLocationSettings();
Navigator.of(dialogContext).pop();
},
);
break;
case GeofencingDialogState.goToSettings:
KwDialog.show(
context: context,
icon: Assets.images.icons.alertTriangle,
state: KwDialogState.info,
title: 'Permission Required',
message: 'You need to allow location access in settings.',
primaryButtonLabel: 'Open Settings',
onPrimaryButtonPressed: (dialogContext) {
Geolocator.openAppSettings();
Navigator.of(dialogContext).pop();
},
);
break;
case GeofencingDialogState.permissionDenied:
KwDialog.show(
context: context,
icon: Assets.images.icons.alertTriangle,
state: KwDialogState.negative,
title: 'Permission Denied',
message: 'You have denied location access. Please allow it manually.',
primaryButtonLabel: 'OK',
);
break;
default:
break;
}
}
Future<dynamic> _clockinDialog(BuildContext context) {
return KwDialog.show(
context: context,
icon: Assets.images.icons.profileAdd,
state: KwDialogState.warning,
title: 'Do you want to Clock In this Staff member?',
primaryButtonLabel: 'Yes',
onPrimaryButtonPressed: (dialogContext) async {
bloc.add(TrackClientClockin(staff));
await Navigator.of(dialogContext).maybePop();
Navigator.of(context).maybePop();
},
secondaryButtonLabel: 'No',
);
}
Future<dynamic> _clockOutDialog(BuildContext context) {
return KwDialog.show(
context: context,
icon: Assets.images.icons.profileDelete,
state: KwDialogState.warning,
title: 'Do you want to Clock Out this Staff member?',
primaryButtonLabel: 'Yes',
onPrimaryButtonPressed: (dialogContext) async {
bloc.add(TrackClientClockout(staff));
await Navigator.of(dialogContext).maybePop();
Navigator.of(context).maybePop();
},
secondaryButtonLabel: 'No',
);
}
}

View File

@@ -0,0 +1,224 @@
import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:intl/intl.dart';
import 'package:krow/core/application/common/str_extensions.dart';
import 'package:krow/core/data/models/staff/pivot.dart';
import 'package:krow/core/entity/staff_contact_entity.dart';
import 'package:krow/core/presentation/gen/assets.gen.dart';
import 'package:krow/core/presentation/styles/kw_box_decorations.dart';
import 'package:krow/core/presentation/styles/kw_text_styles.dart';
import 'package:krow/core/presentation/styles/theme.dart';
import 'package:url_launcher/url_launcher_string.dart';
class StaffPositionDetailsWidget extends StatelessWidget {
final StaffContact staff;
const StaffPositionDetailsWidget({
super.key,
required this.staff,
});
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
margin: const EdgeInsets.all(12),
decoration: KwBoxDecorations.primaryLight8,
child: Column(
children: [
_textRow('Role', staff.skillName),
_textRow('Department', staff.parentPosition?.department?.name ?? ''),
_textRow(
'Start Time',
DateFormat('hh:mm a').format(
DateFormat('yyyy-MM-dd hh:mm:ss').parse(staff.startAt))),
_textRow(
'End Time',
DateFormat('hh:mm a').format(
DateFormat('yyyy-MM-dd hh:mm:ss').parse(staff.endAt))),
_textRow('Cost', '\$${staff.rate}/h'),
],
),
);
}
Widget _textRow(String title, String value) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
title,
style: AppTextStyles.bodySmallReg.copyWith(
color: AppColors.blackGray,
),
),
Text(
value,
style: AppTextStyles.bodySmallMed,
),
],
),
);
}
}
class StaffPositionAvatar extends StatelessWidget {
final String? imageUrl;
final String? userName;
final PivotStatus? status;
const StaffPositionAvatar(
{super.key, this.imageUrl, this.userName, this.status});
@override
Widget build(BuildContext context) {
return Stack(
alignment: Alignment.center,
clipBehavior: Clip.none,
children: [
Container(
width: 96,
height: 96,
decoration: const BoxDecoration(
shape: BoxShape.circle,
color: AppColors.darkBgActiveButtonState,
),
child: imageUrl == null || imageUrl!.isEmpty
? Center(
child: Text(
getInitials(userName),
style: AppTextStyles.headingH1.copyWith(
color: Colors.white,
),
),
)
: ClipOval(
child: Image.network(
imageUrl ?? '',
fit: BoxFit.cover,
width: 96,
height: 96,
),
),
),
if (status != null) Positioned(bottom: -7, child: buildStatus(status!)),
],
);
}
Widget buildStatus(PivotStatus status) {
return Container(
height: 20,
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 4),
decoration: BoxDecoration(
color: AppColors.grayWhite,
border: Border.all(color: status.getStatusBorderColor()),
borderRadius: BorderRadius.circular(10),
),
child: Row(
children: [
Container(
width: 6,
height: 6,
padding: const EdgeInsets.all(3),
decoration: BoxDecoration(
color: status.getStatusTextColor(),
borderRadius: BorderRadius.circular(3),
),
),
const Gap(2),
Text(
status.formattedName.capitalize(),
style: AppTextStyles.bodyTinyMed
.copyWith(color: status.getStatusTextColor()),
)
],
),
);
}
String getInitials(String? name) {
try {
if (name == null || name.isEmpty) return 'X';
List<String> nameParts = name.split(' ');
if (nameParts.length == 1) {
return nameParts[0].substring(0, 1).toUpperCase();
}
return (nameParts[0][0] + nameParts[1][0]).toUpperCase();
} catch (e) {
return 'X';
}
}
}
class StaffContactsWidget extends StatelessWidget {
final StaffContact staff;
const StaffContactsWidget({super.key, required this.staff});
@override
Widget build(BuildContext context) {
return Column(
children: [
Text(
'${staff.firstName} ${staff.lastName}',
style: AppTextStyles.headingH3,
textAlign: TextAlign.center,
),
if (staff.email != null && (staff.email?.isNotEmpty ?? false)) ...[
const Gap(8),
GestureDetector(
onDoubleTap: () {
launchUrlString('mailto:${staff.email}');
},
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Assets.images.icons.userProfile.sms.svg(
colorFilter: const ColorFilter.mode(
AppColors.blackGray,
BlendMode.srcIn,
),
),
const Gap(4),
Text(
staff.email ?? '',
style: AppTextStyles.bodyMediumReg
.copyWith(color: AppColors.blackGray),
)
],
),
),
],
if (staff.phoneNumber != null &&
(staff.phoneNumber?.isNotEmpty ?? false)) ...[
const Gap(8),
GestureDetector(
onTap: () {
launchUrlString('tel:${staff.phoneNumber}');
},
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Assets.images.icons.userProfile.call.svg(
colorFilter: const ColorFilter.mode(
AppColors.blackGray,
BlendMode.srcIn,
),
),
const Gap(4),
Text(
staff.phoneNumber ?? '',
style: AppTextStyles.bodyMediumReg
.copyWith(color: AppColors.blackGray),
)
],
),
),
]
],
);
}
}

View File

@@ -0,0 +1,447 @@
import 'dart:io';
import 'dart:math' as math;
import 'package:flutter/material.dart';
enum PressType {
longPress,
singleClick,
}
enum PreferredPosition {
top,
bottom,
}
class CustomPopupMenuController extends ChangeNotifier {
bool menuIsShowing = false;
void setState() {
notifyListeners();
}
void showMenu() {
menuIsShowing = true;
notifyListeners();
}
void hideMenu() {
menuIsShowing = false;
notifyListeners();
}
void toggleMenu() {
menuIsShowing = !menuIsShowing;
notifyListeners();
}
}
Rect _menuRect = Rect.zero;
class CustomPopupMenu extends StatefulWidget {
const CustomPopupMenu({super.key,
required this.child,
required this.menuBuilder,
required this.pressType,
this.controller,
this.arrowColor = const Color(0xFF4C4C4C),
this.showArrow = true,
this.barrierColor = Colors.black12,
this.arrowSize = 10.0,
this.horizontalMargin = 10.0,
this.verticalMargin = 10.0,
this.position,
this.menuOnChange,
this.enablePassEvent = true,
});
final Widget child;
final PressType pressType;
final bool showArrow;
final Color arrowColor;
final Color barrierColor;
final double horizontalMargin;
final double verticalMargin;
final double arrowSize;
final CustomPopupMenuController? controller;
final Widget Function()? menuBuilder;
final PreferredPosition? position;
final void Function(bool)? menuOnChange;
/// Pass tap event to the widgets below the mask.
/// It only works when [barrierColor] is transparent.
final bool enablePassEvent;
@override
_CustomPopupMenuState createState() => _CustomPopupMenuState();
}
class _CustomPopupMenuState extends State<CustomPopupMenu> {
RenderBox? _childBox;
RenderBox? _parentBox;
static OverlayEntry? overlayEntry;
CustomPopupMenuController? _controller;
bool _canResponse = true;
_showMenu() {
if (widget.menuBuilder == null) {
_hideMenu();
return;
}
Widget arrow = ClipPath(
clipper: _ArrowClipper(),
child: Container(
width: widget.arrowSize,
height: widget.arrowSize,
color: widget.arrowColor,
),
);
if(overlayEntry!=null){
overlayEntry?.remove();
}
overlayEntry = OverlayEntry(
builder: (context) {
Widget menu = Center(
child: Container(
constraints: BoxConstraints(
maxWidth: _parentBox!.size.width - 2 * widget.horizontalMargin,
minWidth: 0,
),
child: CustomMultiChildLayout(
delegate: _MenuLayoutDelegate(
anchorSize: _childBox!.size,
anchorOffset: _childBox!.localToGlobal(
Offset(-widget.horizontalMargin, 0),
),
verticalMargin: widget.verticalMargin,
position: widget.position,
),
children: <Widget>[
if (widget.showArrow)
LayoutId(
id: _MenuLayoutId.arrow,
child: arrow,
),
if (widget.showArrow)
LayoutId(
id: _MenuLayoutId.downArrow,
child: Transform.rotate(
angle: math.pi,
child: arrow,
),
),
LayoutId(
id: _MenuLayoutId.content,
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Material(
color: Colors.transparent,
child: widget.menuBuilder?.call() ?? Container(),
),
],
),
),
],
),
),
);
return Listener(
behavior: widget.enablePassEvent
? HitTestBehavior.translucent
: HitTestBehavior.opaque,
onPointerDown: (PointerDownEvent event) {
Offset offset = event.localPosition;
// If tap position in menu
if (_menuRect.contains(
Offset(offset.dx - widget.horizontalMargin, offset.dy))) {
return;
}
_controller?.hideMenu();
// When [enablePassEvent] works and we tap the [child] to [hideMenu],
// but the passed event would trigger [showMenu] again.
// So, we use time threshold to solve this bug.
_canResponse = false;
Future.delayed(const Duration(milliseconds: 300))
.then((_) => _canResponse = true);
},
child: widget.barrierColor == Colors.transparent
? menu
: Container(
color: widget.barrierColor,
child: menu,
),
);
},
);
if (overlayEntry != null) {
try {
overlayEntry?.remove();
} catch (e) {
print(e);
}
Overlay.of(context).insert(overlayEntry!);
}
}
_hideMenu() {
if (overlayEntry != null) {
overlayEntry?.remove();
overlayEntry = null;
}
}
_updateView() {
bool menuIsShowing = _controller?.menuIsShowing ?? false;
widget.menuOnChange?.call(menuIsShowing);
if (menuIsShowing) {
_showMenu();
} else {
_hideMenu();
}
}
@override
void initState() {
super.initState();
_controller = widget.controller;
_controller ??= CustomPopupMenuController();
_controller?.addListener(_updateView);
WidgetsBinding.instance.addPostFrameCallback((call) {
if (mounted) {
_childBox = context.findRenderObject() as RenderBox?;
_parentBox =
Overlay.of(context).context.findRenderObject() as RenderBox?;
}
});
}
@override
void dispose() {
_hideMenu();
_controller?.removeListener(_updateView);
super.dispose();
}
@override
Widget build(BuildContext context) {
var child = Material(
color: Colors.transparent,
child: InkWell(
hoverColor: Colors.transparent,
focusColor: Colors.transparent,
splashColor: Colors.transparent,
highlightColor: Colors.transparent,
child: widget.child,
onTap: () {
if (widget.pressType == PressType.singleClick && _canResponse) {
_controller?.showMenu();
}
},
onLongPress: () {
if (widget.pressType == PressType.longPress && _canResponse) {
_controller?.showMenu();
}
},
),
);
if (Platform.isIOS) {
return child;
} else {
return WillPopScope(
onWillPop: () {
_hideMenu();
return Future.value(true);
},
child: child,
);
}
}
}
enum _MenuLayoutId {
arrow,
downArrow,
content,
}
enum _MenuPosition {
bottomLeft,
bottomCenter,
bottomRight,
topLeft,
topCenter,
topRight,
}
class _MenuLayoutDelegate extends MultiChildLayoutDelegate {
_MenuLayoutDelegate({
required this.anchorSize,
required this.anchorOffset,
required this.verticalMargin,
this.position,
});
final Size anchorSize;
final Offset anchorOffset;
final double verticalMargin;
final PreferredPosition? position;
@override
void performLayout(Size size) {
Size contentSize = Size.zero;
Size arrowSize = Size.zero;
Offset contentOffset = const Offset(0, 0);
Offset arrowOffset = const Offset(0, 0);
double anchorCenterX = anchorOffset.dx + anchorSize.width / 2;
double anchorTopY = anchorOffset.dy;
double anchorBottomY = anchorTopY + anchorSize.height;
_MenuPosition menuPosition = _MenuPosition.bottomCenter;
if (hasChild(_MenuLayoutId.content)) {
contentSize = layoutChild(
_MenuLayoutId.content,
BoxConstraints.loose(size),
);
}
if (hasChild(_MenuLayoutId.arrow)) {
arrowSize = layoutChild(
_MenuLayoutId.arrow,
BoxConstraints.loose(size),
);
}
if (hasChild(_MenuLayoutId.downArrow)) {
layoutChild(
_MenuLayoutId.downArrow,
BoxConstraints.loose(size),
);
}
bool isTop = false;
if (position == null) {
// auto calculate position
isTop = anchorBottomY > size.height / 2;
} else {
isTop = position == PreferredPosition.top;
}
if (anchorCenterX - contentSize.width / 2 < 0) {
menuPosition = isTop ? _MenuPosition.topLeft : _MenuPosition.bottomLeft;
} else if (anchorCenterX + contentSize.width / 2 > size.width) {
menuPosition = isTop ? _MenuPosition.topRight : _MenuPosition.bottomRight;
} else {
menuPosition =
isTop ? _MenuPosition.topCenter : _MenuPosition.bottomCenter;
}
switch (menuPosition) {
case _MenuPosition.bottomCenter:
arrowOffset = Offset(
anchorCenterX - arrowSize.width / 2,
anchorBottomY + verticalMargin,
);
contentOffset = Offset(
anchorCenterX - contentSize.width / 2,
anchorBottomY + verticalMargin + arrowSize.height,
);
break;
case _MenuPosition.bottomLeft:
arrowOffset = Offset(anchorCenterX - arrowSize.width / 2,
anchorBottomY + verticalMargin);
contentOffset = Offset(
0,
anchorBottomY + verticalMargin + arrowSize.height,
);
break;
case _MenuPosition.bottomRight:
arrowOffset = Offset(anchorCenterX - arrowSize.width / 2,
anchorBottomY + verticalMargin);
contentOffset = Offset(
size.width - contentSize.width,
anchorBottomY + verticalMargin + arrowSize.height,
);
break;
case _MenuPosition.topCenter:
arrowOffset = Offset(
anchorCenterX - arrowSize.width / 2,
anchorTopY - verticalMargin - arrowSize.height,
);
contentOffset = Offset(
anchorCenterX - contentSize.width / 2,
anchorTopY - verticalMargin - arrowSize.height - contentSize.height,
);
break;
case _MenuPosition.topLeft:
arrowOffset = Offset(
anchorCenterX - arrowSize.width / 2,
anchorTopY - verticalMargin - arrowSize.height,
);
contentOffset = Offset(
0,
anchorTopY - verticalMargin - arrowSize.height - contentSize.height,
);
break;
case _MenuPosition.topRight:
arrowOffset = Offset(
anchorCenterX - arrowSize.width / 2,
anchorTopY - verticalMargin - arrowSize.height,
);
contentOffset = Offset(
size.width - contentSize.width,
anchorTopY - verticalMargin - arrowSize.height - contentSize.height,
);
break;
}
if (hasChild(_MenuLayoutId.content)) {
positionChild(_MenuLayoutId.content, contentOffset);
}
_menuRect = Rect.fromLTWH(
contentOffset.dx,
contentOffset.dy,
contentSize.width,
contentSize.height,
);
bool isBottom = false;
if (_MenuPosition.values.indexOf(menuPosition) < 3) {
// bottom
isBottom = true;
}
if (hasChild(_MenuLayoutId.arrow)) {
positionChild(
_MenuLayoutId.arrow,
isBottom
? Offset(arrowOffset.dx, arrowOffset.dy + 0.1)
: const Offset(-100, 0),
);
}
if (hasChild(_MenuLayoutId.downArrow)) {
positionChild(
_MenuLayoutId.downArrow,
!isBottom
? Offset(arrowOffset.dx, arrowOffset.dy - 0.1)
: const Offset(-100, 0),
);
}
}
@override
bool shouldRelayout(MultiChildLayoutDelegate oldDelegate) => false;
}
class _ArrowClipper extends CustomClipper<Path> {
@override
Path getClip(Size size) {
Path path = Path();
path.moveTo(0, size.height);
path.lineTo(size.width / 2, size.height / 2);
path.lineTo(size.width, size.height);
return path;
}
@override
bool shouldReclip(CustomClipper<Path> oldClipper) {
return true;
}
}

View File

@@ -0,0 +1,60 @@
import 'package:flutter/material.dart';
import 'package:krow/core/presentation/gen/assets.gen.dart';
import 'package:krow/core/presentation/styles/theme.dart';
enum CheckBoxStyle {
green,
black,
red,
}
class KWCheckBox extends StatelessWidget {
final bool value;
final CheckBoxStyle? style;
const KWCheckBox({
super.key,
required this.value,
this.style = CheckBoxStyle.green,
});
Color get _color {
switch (style!) {
case CheckBoxStyle.green:
return value ? AppColors.statusSuccess : Colors.white;
case CheckBoxStyle.black:
return value ? AppColors.bgColorDark : Colors.white;
case CheckBoxStyle.red:
return value ? AppColors.statusError : Colors.white;
}
}
Widget get _icon {
switch (style!) {
case CheckBoxStyle.green:
case CheckBoxStyle.black:
return Assets.images.icons.checkBox.check.svg();
case CheckBoxStyle.red:
return Assets.images.icons.checkBox.x.svg();
}
}
@override
Widget build(BuildContext context) {
return AnimatedContainer(
duration: const Duration(milliseconds: 200),
width: 16,
height: 16,
decoration: BoxDecoration(
color: _color,
borderRadius: BorderRadius.circular(4),
border: value
? null
: Border.all(color: AppColors.grayTintStroke, width: 1),
),
child: Center(
child: value ? _icon : null,
),
);
}
}

View File

@@ -0,0 +1,90 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:krow/core/presentation/gen/assets.gen.dart';
import 'package:krow/core/presentation/styles/kw_text_styles.dart';
import 'package:krow/core/presentation/styles/theme.dart';
class ImagePreviewDialog extends StatelessWidget {
final String title;
final String imageUrl;
const ImagePreviewDialog({
super.key,
required this.title,
required this.imageUrl,
});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Center(
child: Container(
decoration: BoxDecoration(
color: AppColors.grayWhite,
borderRadius: BorderRadius.circular(16),
),
child: Padding(
padding: const EdgeInsets.all(24.0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
title,
style: AppTextStyles.bodyLargeMed,
),
GestureDetector(
onTap: () {
Navigator.of(context).pop();
},
child: Assets.images.icons.x.svg(),
),
],
),
const SizedBox(height: 32),
//rounded corner image
ClipRRect(
borderRadius: BorderRadius.circular(16),
child: _buildImage(context),
),
],
),
),
),
),
);
}
Widget _buildImage(context) {
if (Uri.parse(imageUrl).isAbsolute) {
return Image.network(
imageUrl,
width: MediaQuery.of(context).size.width - 80,
fit: BoxFit.cover,
);
} else {
return Image.file(
File(imageUrl),
width: MediaQuery.of(context).size.width - 80,
fit: BoxFit.cover,
);
}
}
static void show(BuildContext context, String title, String imageUrl) {
showDialog(
context: context,
builder: (BuildContext context) {
return ImagePreviewDialog(
title: title,
imageUrl: imageUrl,
);
},
);
}
}

View File

@@ -0,0 +1,194 @@
import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:krow/core/presentation/gen/assets.gen.dart';
import 'package:krow/core/presentation/styles/kw_box_decorations.dart';
import 'package:krow/core/presentation/styles/kw_text_styles.dart';
import 'package:krow/core/presentation/styles/theme.dart';
import 'package:krow/core/presentation/widgets/ui_kit/kw_button.dart';
enum KwDialogState { neutral, positive, negative, warning, info }
class KwDialog extends StatefulWidget {
final SvgGenImage icon;
final KwDialogState state;
final String title;
final String? message;
final String? primaryButtonLabel;
final String? secondaryButtonLabel;
final void Function(BuildContext dialogContext)? onPrimaryButtonPressed;
final void Function(BuildContext dialogContext)? onSecondaryButtonPressed;
final Widget? child;
const KwDialog(
{super.key,
required this.icon,
required this.state,
required this.title,
this.message,
this.primaryButtonLabel,
this.secondaryButtonLabel,
this.onPrimaryButtonPressed,
this.onSecondaryButtonPressed,
this.child});
@override
State<KwDialog> createState() => _KwDialogState();
static Future<R?> show<R>(
{required BuildContext context,
required SvgGenImage icon,
KwDialogState state = KwDialogState.neutral,
required String title,
String? message,
String? primaryButtonLabel,
String? secondaryButtonLabel,
void Function(BuildContext dialogContext)? onPrimaryButtonPressed,
void Function(BuildContext dialogContext)? onSecondaryButtonPressed,
Widget? child}) async {
return showDialog<R>(
context: context,
builder: (context) => KwDialog(
icon: icon,
state: state,
title: title,
message: message,
primaryButtonLabel: primaryButtonLabel,
secondaryButtonLabel: secondaryButtonLabel,
onPrimaryButtonPressed: onPrimaryButtonPressed,
onSecondaryButtonPressed: onSecondaryButtonPressed,
child: child,
),
);
}
}
class _KwDialogState extends State<KwDialog> {
Color get _iconColor {
switch (widget.state) {
case KwDialogState.neutral:
return AppColors.blackBlack;
case KwDialogState.positive:
return AppColors.statusSuccess;
case KwDialogState.negative:
return AppColors.statusError;
case KwDialogState.warning:
return AppColors.statusWarning;
case KwDialogState.info:
return AppColors.primaryBlue;
}
}
Color get _iconBgColor {
switch (widget.state) {
case KwDialogState.neutral:
return AppColors.tintGray;
case KwDialogState.positive:
return AppColors.tintGreen;
case KwDialogState.negative:
return AppColors.tintRed;
case KwDialogState.warning:
return AppColors.tintYellow;
case KwDialogState.info:
return AppColors.tintBlue;
}
}
@override
Widget build(BuildContext context) {
return Center(
child: AnimatedSize(
duration: const Duration(milliseconds: 300),
child: Container(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
decoration: KwBoxDecorations.white24,
child: RawScrollbar(
thumbVisibility: true,
thumbColor: AppColors.blackCaptionText,
radius: const Radius.circular(20),
crossAxisMargin: 4,
mainAxisMargin: 24,
thickness: 6,
child: SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 56),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
AnimatedContainer(
duration: const Duration(milliseconds: 300),
height: 64,
width: 64,
decoration: BoxDecoration(
color: _iconBgColor,
shape: BoxShape.circle,
),
child: Center(
child: widget.icon.svg(
width: 32,
height: 32,
colorFilter:
ColorFilter.mode(_iconColor, BlendMode.srcIn),
),
),
),
const Gap(32),
Text(
widget.title,
style: AppTextStyles.headingH1.copyWith(height: 1),
textAlign: TextAlign.center,
),
if (widget.message != null) ...[
const Gap(8),
Text(
widget.message ?? '',
style: AppTextStyles.bodyMediumReg
.copyWith(color: AppColors.blackGray),
textAlign: TextAlign.center,
),
],
if (widget.child != null) ...[
const Gap(8),
widget.child!,
],
const Gap(24),
..._buttonGroup(),
],
),
),
),
),
),
);
}
List<Widget> _buttonGroup() {
return [
if (widget.primaryButtonLabel != null)
KwButton.primary(
label: widget.primaryButtonLabel ?? '',
onPressed: () {
if (widget.onPrimaryButtonPressed != null) {
widget.onPrimaryButtonPressed?.call(context);
} else {
Navigator.of(context).pop();
}
},
),
if (widget.primaryButtonLabel != null &&
widget.secondaryButtonLabel != null)
const Gap(8),
if (widget.secondaryButtonLabel != null)
KwButton.outlinedPrimary(
label: widget.secondaryButtonLabel ?? '',
onPressed: () {
if (widget.onSecondaryButtonPressed != null) {
widget.onSecondaryButtonPressed?.call(context);
} else {
Navigator.of(context).pop();
}
},
),
];
}
}

View File

@@ -0,0 +1,98 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:krow/core/presentation/gen/assets.gen.dart';
import 'package:krow/core/presentation/styles/kw_text_styles.dart';
import 'package:krow/core/presentation/styles/theme.dart';
enum AppBarIconColorStyle { normal, inverted }
class KwAppBar extends AppBar {
final bool showNotification;
final Color? contentColor;
final AppBarIconColorStyle iconColorStyle;
final String? titleText;
KwAppBar({
this.titleText,
this.showNotification = false,
this.contentColor,
bool centerTitle = true,
this.iconColorStyle = AppBarIconColorStyle.normal,
super.key,
super.backgroundColor,
}) : super(
leadingWidth: centerTitle ? null : 0,
elevation: 0,
centerTitle: centerTitle,
);
@override
List<Widget> get actions {
return [
if (showNotification)
Container(
margin: const EdgeInsets.only(right: 16),
height: 48,
width: 48,
color: Colors.transparent,
child: Center(
child: Assets.images.icons.appBar.notification.svg(
colorFilter: ColorFilter.mode(
iconColorStyle == AppBarIconColorStyle.normal
? AppColors.blackBlack
: AppColors.grayWhite,
BlendMode.srcIn)),
),
),
];
}
@override
Widget? get title {
return titleText != null
? Text(
titleText!,
style: _titleTextStyle(contentColor),
)
: Assets.images.logo.svg(
colorFilter: ColorFilter.mode(
contentColor ?? AppColors.bgColorDark, BlendMode.srcIn));
}
@override
Widget? get leading {
return Builder(builder: (context) {
return AutoRouter.of(context).canPop()
? GestureDetector(
onTap: () {
AutoRouter.of(context).maybePop();
},
child: Padding(
padding: const EdgeInsets.only(left: 20.0),
child: SizedBox(
height: 40,
width: 40,
child: Center(
child: Assets.images.icons.appBar.appbarLeading.svg(
colorFilter: ColorFilter.mode(
iconColorStyle == AppBarIconColorStyle.normal
? AppColors.blackBlack
: Colors.white,
BlendMode.srcIn,
),
),
),
),
),
)
: const SizedBox.shrink();
});
}
static TextStyle _titleTextStyle(contentColor) {
return AppTextStyles.headingH2.copyWith(
color: contentColor ?? AppColors.blackBlack,
);
}
}

View File

@@ -0,0 +1,316 @@
import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:krow/core/presentation/gen/assets.gen.dart';
import 'package:krow/core/presentation/styles/kw_text_styles.dart';
import 'package:krow/core/presentation/styles/theme.dart';
///The [KwButtonFit] enum defines two possible values for the button fit:
/// *[expanded]: The button will expand to fill the available space.
/// *[shrinkWrap]: The button will wrap its content, taking up only as much space as needed.
enum KwButtonFit { expanded, shrinkWrap, circular }
class KwButton extends StatefulWidget {
final String? label;
final SvgGenImage? leftIcon;
final SvgGenImage? rightIcon;
final bool disabled;
final Color color;
final Color pressedColor;
final Color disabledColor;
final Color borderColor;
final Color? textColors;
final bool isOutlined;
final bool isFilledOutlined;
final VoidCallback onPressed;
final KwButtonFit? fit;
final double height;
final bool originalIconsColors;
final double? iconSize;
const KwButton._({
required this.onPressed,
required this.color,
required this.isOutlined,
required this.pressedColor,
required this.disabledColor,
required this.borderColor,
this.disabled = false,
this.label,
this.leftIcon,
this.rightIcon,
this.textColors,
this.height = 52,
this.fit = KwButtonFit.expanded,
this.originalIconsColors = false,
this.isFilledOutlined = false,
this.iconSize,
}) : assert(label != null || leftIcon != null || rightIcon != null,
'title or icon must be provided');
/// Creates a standard dark button.
const KwButton.primary(
{super.key,
this.label,
this.leftIcon,
this.rightIcon,
this.disabled = false,
this.height = 52,
this.fit = KwButtonFit.expanded,
required this.onPressed,
this.iconSize})
: color = AppColors.bgColorDark,
borderColor = AppColors.bgColorDark,
pressedColor = AppColors.darkBgActiveButtonState,
disabledColor = AppColors.grayDisable,
textColors = Colors.white,
originalIconsColors = true,
isFilledOutlined = false,
isOutlined = false;
// /// Creates a white button.
const KwButton.secondary(
{super.key,
this.label,
this.leftIcon,
this.rightIcon,
this.disabled = false,
this.height = 52,
this.fit = KwButtonFit.expanded,
required this.onPressed,
this.iconSize})
: color = Colors.white,
borderColor = Colors.white,
pressedColor = AppColors.buttonTertiaryActive,
disabledColor = Colors.white,
textColors = disabled ? AppColors.grayDisable : AppColors.blackBlack,
originalIconsColors = true,
isFilledOutlined = false,
isOutlined = false;
/// Creates a yellow button.
const KwButton.accent(
{super.key,
this.label,
this.leftIcon,
this.rightIcon,
this.disabled = false,
this.height = 52,
this.fit = KwButtonFit.expanded,
required this.onPressed,
this.iconSize})
: color = AppColors.primaryYellow,
borderColor = AppColors.primaryYellow,
pressedColor = AppColors.primaryYellowDark,
disabledColor = AppColors.navBarDisabled,
textColors = disabled ? AppColors.darkBgInactive : AppColors.blackBlack,
originalIconsColors = true,
isFilledOutlined = false,
isOutlined = false;
const KwButton.outlinedPrimary(
{super.key,
this.label,
this.leftIcon,
this.rightIcon,
this.disabled = false,
this.height = 52,
this.fit = KwButtonFit.expanded,
required this.onPressed,
this.iconSize})
: color = AppColors.bgColorDark,
borderColor = AppColors.bgColorDark,
pressedColor = AppColors.darkBgActiveButtonState,
disabledColor = AppColors.grayDisable,
isOutlined = true,
originalIconsColors = true,
isFilledOutlined = false,
textColors = null;
const KwButton.outlinedAccent(
{super.key,
this.label,
this.leftIcon,
this.rightIcon,
this.disabled = false,
this.height = 52,
this.fit = KwButtonFit.expanded,
required this.onPressed,
this.iconSize})
: color = AppColors.primaryYellow,
borderColor = AppColors.primaryYellow,
pressedColor = AppColors.darkBgActiveButtonState,
disabledColor = AppColors.navBarDisabled,
isOutlined = true,
originalIconsColors = true,
isFilledOutlined = false,
textColors = null;
KwButton copyWith({
String? label,
SvgGenImage? icon,
bool? disabled,
Color? color,
Color? pressedColor,
Color? disabledColor,
Color? textColors,
Color? borderColor,
bool? isOutlined,
VoidCallback? onPressed,
KwButtonFit? fit,
double? height,
double? iconSize,
bool? originalIconsColors,
bool? isFilledOutlined,
}) {
return KwButton._(
label: label ?? this.label,
leftIcon: icon ?? leftIcon,
rightIcon: icon ?? rightIcon,
disabled: disabled ?? this.disabled,
color: color ?? this.color,
pressedColor: pressedColor ?? this.pressedColor,
disabledColor: disabledColor ?? this.disabledColor,
textColors: textColors ?? this.textColors,
borderColor: borderColor ?? this.borderColor,
isOutlined: isOutlined ?? this.isOutlined,
onPressed: onPressed ?? this.onPressed,
fit: fit ?? this.fit,
height: height ?? this.height,
iconSize: iconSize ?? this.iconSize,
isFilledOutlined: isFilledOutlined ?? this.isFilledOutlined,
originalIconsColors: originalIconsColors ?? this.originalIconsColors,
);
}
@override
State<KwButton> createState() => _KwButtonState();
}
class _KwButtonState extends State<KwButton> {
bool pressed = false;
@override
Widget build(BuildContext context) {
return widget.fit == KwButtonFit.shrinkWrap
? Row(children: [_buildButton(context)])
: _buildButton(context);
}
Widget _buildButton(BuildContext context) {
return AnimatedContainer(
duration: const Duration(milliseconds: 200),
height: widget.height,
width: widget.fit == KwButtonFit.circular ? widget.height : null,
decoration: BoxDecoration(
color: _getColor(),
border: _getBorder(),
borderRadius: BorderRadius.circular(widget.height / 2),
),
child: Material(
color: Colors.transparent,
child: InkWell(
onTapDown: widget.disabled ? null : _onTapDown,
onTapCancel: widget.disabled ? null : _onTapCancel,
onTapUp: widget.disabled ? null : _onTapUp,
borderRadius: BorderRadius.circular(widget.height / 2),
highlightColor:
( widget.isOutlined && !widget.isFilledOutlined) ? Colors.transparent : widget.pressedColor,
splashColor:
( widget.isOutlined && !widget.isFilledOutlined) ? Colors.transparent : widget.pressedColor,
child: _buildButtonContent(context),
),
),
);
}
Center _buildButtonContent(BuildContext context) {
return Center(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
_buildHorizontalPadding(),
if (widget.leftIcon != null)
Center(
child: widget.leftIcon!.svg(
height: widget.iconSize ?? 16,
width: widget.iconSize ?? 16,
colorFilter: widget.originalIconsColors
? null
: ColorFilter.mode(_getTextColor(), BlendMode.srcIn)),
),
if (widget.leftIcon != null && widget.label != null)
const SizedBox(width: 4),
if (widget.label != null)
Text(
widget.label!,
style: AppTextStyles.bodyMediumMed.copyWith(
color: _getTextColor(),
),
),
if (widget.rightIcon != null && widget.label != null)
const SizedBox(width: 4),
if (widget.rightIcon != null)
Center(
child: widget.rightIcon!.svg(
height: widget.iconSize ?? 16,
width: widget.iconSize ?? 16,
colorFilter: widget.originalIconsColors
? null
: ColorFilter.mode(_getTextColor(), BlendMode.srcIn)),
),
_buildHorizontalPadding()
],
),
);
}
Gap _buildHorizontalPadding() => Gap(
widget.fit == KwButtonFit.circular ? 0 : (widget.height < 40 ? 12 : 20));
void _onTapDown(details) {
setState(() {
pressed = true;
});
}
void _onTapCancel() {
setState(() {
pressed = false;
});
}
void _onTapUp(details) {
Future.delayed(const Duration(milliseconds: 50), _onTapCancel);
widget.onPressed();
}
Border? _getBorder() {
return widget.isOutlined
? Border.all(
color: widget.disabled
? widget.disabledColor
: pressed
? widget.pressedColor
: (widget.borderColor??widget.color),
width: 1)
: null;
}
Color _getColor() {
return widget.isOutlined && !widget.isFilledOutlined
? Colors.transparent
: widget.disabled
? widget.disabledColor
: widget.color;
}
Color _getTextColor() {
return widget.textColors ??
(pressed
? widget.pressedColor
: widget.disabled
? widget.disabledColor
: widget.color);
}
}

View File

@@ -0,0 +1,139 @@
import 'package:flutter/material.dart';
import 'package:krow/core/presentation/gen/assets.gen.dart';
import 'package:krow/core/presentation/styles/kw_text_styles.dart';
import 'package:krow/core/presentation/styles/theme.dart';
import 'package:krow/core/presentation/widgets/ui_kit/kw_popup_menu.dart';
class KwDropdown<R> extends StatefulWidget {
final String? title;
final String hintText;
final KwDropDownItem<R>? selectedItem;
final Iterable<KwDropDownItem<R>> items;
final Function(R item) onSelected;
final double horizontalPadding;
final Color? backgroundColor;
final Color? borderColor;
const KwDropdown(
{super.key,
required this.hintText,
this.horizontalPadding = 0,
required this.items,
required this.onSelected,
this.backgroundColor,
this.borderColor,
this.title,
this.selectedItem});
@override
State<KwDropdown<R>> createState() => _KwDropdownState<R>();
}
class _KwDropdownState<R> extends State<KwDropdown<R>> {
KwDropDownItem<R>? _selectedItem;
@override
didUpdateWidget(KwDropdown<R> oldWidget) {
if (oldWidget.selectedItem != widget.selectedItem) {
_selectedItem = widget.selectedItem;
}
super.didUpdateWidget(oldWidget);
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
_selectedItem ??= widget.selectedItem;
}
@override
void initState() {
_selectedItem = widget.selectedItem;
super.initState();
}
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
if (widget.title != null)
Padding(
padding: const EdgeInsets.only(left: 16,bottom: 4),
child: Text(
widget.title!,
style: AppTextStyles.bodyTinyReg.copyWith(
color: AppColors.blackGray,
),
),
),
IgnorePointer(
ignoring: widget.items.isEmpty,
child: KwPopupMenu(
horizontalPadding: widget.horizontalPadding,
fit: KwPopupMenuFit.expand,
customButtonBuilder: (context, isOpened) {
return _buildMenuButton(isOpened);
},
menuItems: widget.items
.map((item) => KwPopupMenuItem(
title: item.title,
icon: item.icon,
onTap: () {
setState(() {
_selectedItem = item;
});
widget.onSelected(item.data);
}))
.toList()),
),
],
);
}
Container _buildMenuButton(bool isOpened) {
return Container(
height: 48,
padding: const EdgeInsets.symmetric(horizontal: 16),
decoration: BoxDecoration(
color: widget.backgroundColor,
borderRadius: BorderRadius.circular(24),
border: Border.all(
color: isOpened
? AppColors.bgColorDark
: widget.borderColor ?? AppColors.grayStroke,
width: 1),
),
child: Row(
children: [
Expanded(
child: Text(
_selectedItem?.title ?? widget.hintText,
style: AppTextStyles.bodyMediumReg.copyWith(
color: _selectedItem == null
? AppColors.blackGray
: AppColors.blackBlack),
)),
AnimatedRotation(
duration: const Duration(milliseconds: 150),
turns: isOpened ? -0.5 : 0,
child: Assets.images.icons.caretDown.svg(
width: 16,
height: 16,
colorFilter: const ColorFilter.mode(
AppColors.blackGray, BlendMode.srcIn),
),
)
],
),
);
}
}
class KwDropDownItem<R> {
final String title;
final R data;
final Widget? icon;
const KwDropDownItem({required this.data, required this.title, this.icon});
}

View File

@@ -0,0 +1,70 @@
import 'package:flutter/material.dart';
class KwAnimatedImagePlaceholder extends StatefulWidget {
const KwAnimatedImagePlaceholder({
super.key,
this.height = double.maxFinite,
this.width = double.maxFinite,
});
final double height;
final double width;
@override
State<KwAnimatedImagePlaceholder> createState() =>
_KwAnimatedImagePlaceholderState();
}
class _KwAnimatedImagePlaceholderState extends State<KwAnimatedImagePlaceholder>
with TickerProviderStateMixin {
late final AnimationController _controller = AnimationController(
vsync: this,
duration: Durations.long4,
animationBehavior: AnimationBehavior.preserve,
)
..forward()
..addListener(
() {
if (!_controller.isCompleted) return;
if (_controller.value == 0) {
_controller.forward();
} else {
_controller.reverse();
}
},
);
late final Animation<double> _opacity = Tween(
begin: 0.6,
end: 0.2,
).animate(_controller);
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _controller,
child: DecoratedBox(
decoration: const BoxDecoration(
color: Colors.grey,
),
child: SizedBox(
height: widget.height,
width: widget.width,
),
),
builder: (context, child) {
return Opacity(
opacity: _opacity.value,
child: child,
);
},
);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
}

View File

@@ -0,0 +1,250 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:gap/gap.dart';
import 'package:krow/core/presentation/gen/assets.gen.dart';
import 'package:krow/core/presentation/styles/kw_text_styles.dart';
import 'package:krow/core/presentation/styles/theme.dart';
class KwTextInput extends StatefulWidget {
final TextEditingController? controller;
final void Function(String)? onChanged;
final void Function(String)? onFieldSubmitted;
final String? title;
final String? hintText;
final String? helperText;
final bool obscureText;
final bool enabled;
final bool readOnly;
final bool showError;
final TextInputType? keyboardType;
final List<TextInputFormatter>? inputFormatters;
final bool showCounter;
final FocusNode? focusNode;
final TextInputAction? textInputAction;
final double minHeight;
final TextStyle? textStyle;
final Widget? suffixIcon;
final int? minLines;
final int? maxLines;
final int? maxLength;
final double radius;
final Color? borderColor;
final Null Function(bool hasFocus)? onFocusChanged;
const KwTextInput({
super.key,
required this.controller,
this.title,
this.onChanged,
this.onFieldSubmitted,
this.hintText,
this.helperText,
this.minHeight = standardHeight,
this.suffixIcon,
this.obscureText = false,
this.showError = false,
this.enabled = true,
this.readOnly = false,
this.keyboardType,
this.inputFormatters,
this.showCounter = false,
this.focusNode,
this.textInputAction,
this.textStyle,
this.maxLength,
this.minLines,
this.maxLines,
this.radius = 12,
this.borderColor,
this.onFocusChanged,
});
static const standardHeight = 48.0;
@override
State<KwTextInput> createState() => _KwTextInputState();
}
class _KwTextInputState extends State<KwTextInput> {
late FocusNode _focusNode;
@override
initState() {
_focusNode = widget.focusNode ?? FocusNode();
_focusNode.addListener(() {
setState(() {});
widget.onFocusChanged?.call(_focusNode.hasFocus);
});
super.initState();
}
Color _helperTextColor() {
if (widget.showError) {
return AppColors.statusError;
} else {
if (!widget.enabled) {
return AppColors.grayDisable;
}
return AppColors.bgColorDark;
}
}
Color _borderColor() {
if (!widget.enabled) {
return AppColors.grayDisable;
}
if (widget.showError ||
widget.maxLength != null &&
(widget.controller?.text.length ?? 0) > widget.maxLength!) {
return AppColors.statusError;
}
if (_focusNode.hasFocus) {
return AppColors.bgColorDark;
}
return widget.borderColor ?? AppColors.grayStroke;
}
@override
Widget build(BuildContext context) {
return Material(
type: MaterialType.transparency,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (widget.title != null)
Padding(
padding: const EdgeInsets.only(left: 16),
child: Text(
widget.title!,
style: AppTextStyles.bodyTinyReg.copyWith(
color: widget.enabled
? AppColors.blackGray
: AppColors.grayDisable,
),
),
),
if (widget.title != null) const SizedBox(height: 4),
GestureDetector(
onTap: () {
if (widget.enabled && !widget.readOnly) {
_focusNode.requestFocus();
}
},
child: Stack(
children: [
Container(
padding: EdgeInsets.only(bottom: widget.showCounter ? 24 : 0),
alignment: Alignment.topCenter,
constraints: BoxConstraints(
minHeight: widget.minHeight,
),
decoration: BoxDecoration(
color: Colors.transparent,
borderRadius: BorderRadius.all(Radius.circular(
widget.minHeight > KwTextInput.standardHeight
? widget.radius
: widget.minHeight / 2)),
border: Border.all(
color: _borderColor(),
),
),
child: Row(
children: [
Expanded(
child: TextFormField(
focusNode: _focusNode,
inputFormatters: widget.inputFormatters,
keyboardType: widget.keyboardType,
enabled: widget.enabled && !widget.readOnly,
controller: widget.controller,
obscureText: widget.obscureText,
maxLines: widget.maxLines,
minLines: widget.minLines ?? 1,
maxLength: widget.maxLength,
onChanged: widget.onChanged,
textInputAction: widget.textInputAction,
onFieldSubmitted: widget.onFieldSubmitted,
onTapOutside: (_) {
_focusNode.unfocus();
},
style: widget.textStyle ??
AppTextStyles.bodyMediumReg.copyWith(
color: !widget.enabled
? AppColors.grayDisable
: null,
),
decoration: InputDecoration(
counter: const SizedBox.shrink(),
fillColor: Colors.transparent,
contentPadding: const EdgeInsets.symmetric(
horizontal: 16, vertical: 8),
hintText: widget.hintText,
// errorStyle: p2pTextStyles.paragraphSmall(
// color: p2pColors.borderDanger),
hintStyle: widget.textStyle ??
AppTextStyles.bodyMediumReg.copyWith(
color: widget.enabled
? AppColors.blackGray
: AppColors.grayDisable,
),
border: InputBorder.none,
),
),
),
if (widget.suffixIcon != null)
SizedBox(
child: widget.suffixIcon!,
),
],
),
),
if (widget.showCounter)
Positioned(
bottom: 12,
left: 12,
child: Text(
'${widget.controller?.text.length}/'
'${(widget.maxLength ?? 0)}',
style: AppTextStyles.bodySmallReg.copyWith(
color: (widget.controller?.text.length ?? 0) >
(widget.maxLength ?? 0)
? AppColors.statusError
: AppColors.blackGray),
),
),
if (widget.minHeight > KwTextInput.standardHeight)
Positioned(
bottom: 12,
right: 12,
child: Assets.images.icons.textFieldNotches.svg(
height: 12,
width: 12,
),
),
],
),
),
if (widget.helperText != null &&
(widget.helperText?.isNotEmpty ?? false)) ...[
const SizedBox(height: 4),
Row(
children: [
const Gap(16),
Text(
widget.helperText!,
style: AppTextStyles.bodyTinyReg.copyWith(
height: 1,
color: _helperTextColor(),
),
),
],
)
]
],
),
);
}
}

View File

@@ -0,0 +1,78 @@
import 'package:flutter/material.dart';
class KwLoadingOverlay extends StatefulWidget {
const KwLoadingOverlay({
super.key,
required this.child,
this.controller,
this.shouldShowLoading,
});
final Widget child;
final OverlayPortalController? controller;
final bool? shouldShowLoading;
@override
State<KwLoadingOverlay> createState() => _KwLoadingOverlayState();
}
class _KwLoadingOverlayState extends State<KwLoadingOverlay> {
late final OverlayPortalController _controller;
@override
void initState() {
_controller = widget.controller ?? OverlayPortalController();
super.initState();
if (widget.shouldShowLoading ?? false) {
WidgetsBinding.instance.addPostFrameCallback(
(_) => _controller.show(),
);
}
}
@override
void didUpdateWidget(covariant KwLoadingOverlay oldWidget) {
super.didUpdateWidget(oldWidget);
WidgetsBinding.instance.addPostFrameCallback(
(_) {
if (widget.shouldShowLoading == null) return;
if (widget.shouldShowLoading!) {
_controller.show();
} else {
_controller.hide();
}
},
);
}
@override
Widget build(BuildContext context) {
return OverlayPortal(
controller: _controller,
overlayChildBuilder: (context) {
return const SizedBox(
height: double.maxFinite,
width: double.maxFinite,
child: DecoratedBox(
decoration: BoxDecoration(
color: Colors.black38,
),
child: Center(
child: CircularProgressIndicator(),
),
),
);
},
child: widget.child,
);
}
@override
void dispose() {
if (context.mounted) _controller.hide();
super.dispose();
}
}

View File

@@ -0,0 +1,131 @@
import 'package:flutter/material.dart';
import 'package:krow/core/presentation/styles/kw_text_styles.dart';
import 'package:krow/core/presentation/styles/theme.dart';
class KwOptionSelector extends StatelessWidget {
const KwOptionSelector({
super.key,
required this.selectedIndex,
required this.onChanged,
this.title,
required this.items,
this.height = 46,
this.spacer = 4,
this.backgroundColor,
this.selectedColor = AppColors.bgColorDark,
this.itemColor,
this.itemBorder,
this.borderRadius,
this.itemAlign,
this.selectedTextStyle,
this.textStyle,
double? selectorHeight,
}) : _selectorHeight = selectorHeight ?? height;
final int? selectedIndex;
final double height;
final double spacer;
final Function(int index) onChanged;
final String? title;
final List<String> items;
final Color? backgroundColor;
final BorderRadius? borderRadius;
final Color selectedColor;
final Color? itemColor;
final Border? itemBorder;
final double _selectorHeight;
final Alignment? itemAlign;
final TextStyle? textStyle;
final TextStyle? selectedTextStyle;
@override
Widget build(BuildContext context) {
var borderRadius = BorderRadius.all(Radius.circular(height / 2));
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (title != null)
Padding(
padding: const EdgeInsets.only(left: 16, bottom: 4),
child: Text(
title!,
style: AppTextStyles.bodyTinyReg
.copyWith(color: AppColors.blackGray),
),
),
LayoutBuilder(
builder: (builderContext, constraints) {
final itemWidth =
(constraints.maxWidth - spacer * (items.length - 1)) /
items.length;
return Container(
height: height,
decoration: BoxDecoration(
color: backgroundColor,
borderRadius: borderRadius,
),
child: Stack(
children: [
if (selectedIndex != null)
AnimatedAlign(
alignment: Alignment(
selectedIndex! * 2 / (items.length - 1) - 1,
1,
),
duration: Durations.short4,
child: Container(
height: _selectorHeight,
width: itemWidth,
decoration: BoxDecoration(
color: selectedColor,
borderRadius: borderRadius,
),
),
),
Row(
spacing: spacer,
children: [
for (int index = 0; index < items.length; index++)
GestureDetector(
onTap: () {
onChanged(index);
},
child: AnimatedContainer(
height: height,
width: itemWidth,
decoration: BoxDecoration(
color: index == selectedIndex ? null : itemColor,
borderRadius: borderRadius,
border:
index == selectedIndex ? null : itemBorder,
),
duration: Durations.short2,
child: Align(
alignment: itemAlign ?? Alignment.center,
child: AnimatedDefaultTextStyle(
duration: Durations.short4,
style: index == selectedIndex
? (selectedTextStyle ??
AppTextStyles.bodyMediumMed.copyWith(
color: AppColors.grayWhite))
: (textStyle ??
AppTextStyles.bodyMediumReg.copyWith(
color: AppColors.blackGray)),
child: Text(items[index]),
),
),
),
),
],
),
],
),
);
},
)
],
);
}
}

View File

@@ -0,0 +1,177 @@
import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:krow/core/presentation/styles/kw_text_styles.dart';
import 'package:krow/core/presentation/styles/theme.dart';
class KwPhoneInput extends StatefulWidget {
final String? title;
final String? error;
final TextEditingController? controller;
final void Function(String)? onChanged;
final Color? borderColor;
final FocusNode? focusNode;
final bool showError;
final String? helperText;
final bool enabled;
const KwPhoneInput({
super.key,
this.title,
this.error,
this.borderColor ,
this.controller,
this.onChanged,
this.focusNode,
this.showError = false,
this.helperText,
this.enabled = true,
});
@override
State<KwPhoneInput> createState() => _KWPhoneInputState();
}
class _KWPhoneInputState extends State<KwPhoneInput> {
late FocusNode _focusNode;
@override
void initState() {
_focusNode = widget.focusNode ?? FocusNode();
_focusNode.addListener(() {
setState(() {});
});
super.initState();
}
Color _borderColor() {
if (!widget.enabled) {
return AppColors.grayDisable;
}
if (_focusNode.hasFocus) {
return AppColors.bgColorDark;
}
return widget.borderColor ?? AppColors.grayStroke;
}
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if(widget.title != null) ...[
_buildLabel(),
const SizedBox(height: 4),
],
_buildInputRow(),
_buildError()
],
);
}
Container _buildInputRow() {
return Container(
decoration: BoxDecoration(
borderRadius: const BorderRadius.all(
Radius.circular(24),
),
border: Border.all(
color: _borderColor(),
width: 1,
),
color: Colors.transparent,
),
child: Row(
children: [
_buildCountryPicker(),
Expanded(
child: TextField(
focusNode: _focusNode,
controller: widget.controller,
onChanged: widget.onChanged,
decoration: InputDecoration(
hintText: 'Enter your number',
hintStyle: AppTextStyles.bodyMediumReg.copyWith(
color: AppColors.blackGray,
),
border: InputBorder.none,
contentPadding: const EdgeInsets.symmetric(horizontal: 10),
filled: true,
fillColor: Colors.transparent,
),
style: AppTextStyles.bodyMediumReg,
keyboardType: TextInputType.phone,
),
),
],
),
);
}
Padding _buildLabel() {
return Padding(
padding: const EdgeInsets.only(left: 16),
child: Text(
widget.title!,
style: AppTextStyles.bodyTinyReg.copyWith(
color: AppColors.blackGray,
),
),
);
}
Widget _buildCountryPicker() {
return GestureDetector(
onTap: () {
Feedback.forTap(context);
//TODO(Heorhii): Add country selection functionality
},
child: Row(
children: [
const Gap(12),
const CircleAvatar(
radius: 12,
backgroundImage: NetworkImage(
'https://flagcdn.com/w320/us.png',
),
),
//TODO// dont show arrow
// const Gap(6),
// const Icon(
// Icons.keyboard_arrow_down_rounded,
// color: AppColors.blackGray,
// opticalSize: 16,
// ),
const Gap(12),
SizedBox(
height: 48,
child: VerticalDivider(
width: 1,
color: _borderColor(),
),
),
],
),
);
}
_buildError() {
return AnimatedSize(
duration: const Duration(milliseconds: 200),
alignment: Alignment.bottomCenter,
child: Container(
height: widget.error == null ? 0 : 24,
clipBehavior: Clip.none,
padding: const EdgeInsets.only(left: 16),
alignment: Alignment.bottomLeft,
child: Text(
widget.error ?? '',
style: AppTextStyles.bodyTinyMed.copyWith(
color: AppColors.statusError,
),
),
),
);
}
}

View File

@@ -0,0 +1,514 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:krow/core/presentation/styles/kw_text_styles.dart';
import 'package:krow/core/presentation/styles/theme.dart';
class KwPopUpButton extends StatefulWidget {
final double height;
final bool disabled;
final bool withBorder;
final KwPopupButtonColorPallet colorPallet;
final double popUpPadding;
final String label;
final List<KwPopUpButtonItem> items;
KwPopUpButton(
{super.key,
required this.label,
required this.items,
colorPallet,
this.height = 52,
this.withBorder = false,
this.disabled = false,
required this.popUpPadding})
: colorPallet = colorPallet ?? KwPopupButtonColorPallet.dark();
@override
State<KwPopUpButton> createState() => _KwPopUpButtonState();
}
class _KwPopUpButtonState extends State<KwPopUpButton> {
final _layerLink = LayerLink();
double opacity = 0.0;
final _KwButtonListenableOverlayPortalController _controller =
_KwButtonListenableOverlayPortalController();
@override
void initState() {
super.initState();
_controller.addListener(() {
setState(() {});
});
_controller.addOnHideListener(_hide);
_controller.addOnShowListener(_show);
}
@override
Widget build(BuildContext context) {
return _buildButton(context);
}
Widget _buildButton(BuildContext context) {
return _KwButtonPopUpOverlayMenu(
opacity: opacity,
controller: _controller,
layerLink: _layerLink,
popUpPadding: widget.popUpPadding,
items: widget.items,
child: CompositedTransformTarget(
link: _layerLink,
child: AnimatedContainer(
duration: const Duration(milliseconds: 150),
height: widget.height,
decoration: BoxDecoration(
color: _getBgColor(),
border: _getBorder(),
borderRadius: BorderRadius.circular(widget.height / 2),
),
child: Material(
color: Colors.transparent,
child: InkWell(
onTapDown: widget.disabled ? null : _onTapDown,
borderRadius: BorderRadius.circular(widget.height / 2),
highlightColor: widget.colorPallet.pressedBdColor,
splashColor: widget.colorPallet.pressedBdColor,
child: _buildButtonContent(context),
),
),
),
),
);
}
_buildButtonContent(BuildContext context) {
return Center(
child: Row(
children: [
const Gap(52),
Expanded(
child: Center(
child: Text(
widget.label,
style:
AppTextStyles.bodyMediumMed.copyWith(color: _getTextColor()),
),
),
),
AnimatedContainer(
duration: const Duration(milliseconds: 150),
height: 52,
width: 52,
decoration: BoxDecoration(
color: _getDropDownBgColor(),
borderRadius: BorderRadius.only(
topRight: Radius.circular(widget.height / 2),
bottomRight: Radius.circular(widget.height / 2),
),
),
child: AnimatedRotation(
turns: opacity * 0.5,
duration: const Duration(milliseconds: 150),
child: Icon(
Icons.keyboard_arrow_down_rounded,
color: _getIconColor(),
),
),
)
],
),
);
}
void _onTapDown(details) {
if (_controller.isShowing) {
_controller.hide();
} else {
_controller.show();
}
}
Future<void> _hide() async {
setState(() {
opacity = 0.0;
});
await Future.delayed(const Duration(milliseconds: 150), () {});
}
Future<void> _show() async {
WidgetsBinding.instance.addPostFrameCallback((call) {
setState(() {
opacity = 1.0;
});
});
}
Border? _getBorder() {
return widget.withBorder
? Border.all(
color: widget.disabled
? widget.colorPallet.dropDownDisabledBgColor
: _controller.isShowing
? widget.colorPallet.dropDownPressedBgColor
: widget.colorPallet.dropDownBgColor,
width: 1)
: null;
}
Color _getDropDownBgColor() {
return widget.disabled
? widget.colorPallet.dropDownDisabledBgColor
: _controller.isShowing
? widget.colorPallet.dropDownPressedBgColor
: widget.colorPallet.dropDownBgColor;
}
Color _getBgColor() {
return widget.disabled
? widget.colorPallet.disabledBdColor
: widget.colorPallet.bgColor;
}
Color _getTextColor() {
return (_controller.isShowing
? widget.colorPallet.textPressedColor
: widget.disabled
? widget.colorPallet.textDisabledColor
: widget.colorPallet.textColor);
}
Color _getIconColor() {
return (_controller.isShowing
? widget.colorPallet.iconPressedColor
: widget.disabled
? widget.colorPallet.iconDisabledColor
: widget.colorPallet.iconColor);
}
}
class KwPopUpButtonItem {
final String title;
final VoidCallback onTap;
final Color color;
KwPopUpButtonItem(
{required this.title,
required this.onTap,
this.color = AppColors.blackBlack});
}
class KwPopupButtonColorPallet {
final Color textColor;
final Color textPressedColor;
final Color textDisabledColor;
final Color bgColor;
final Color pressedBdColor;
final Color disabledBdColor;
final Color iconColor;
final Color iconPressedColor;
final Color iconDisabledColor;
final Color dropDownBgColor;
final Color dropDownPressedBgColor;
final Color dropDownDisabledBgColor;
const KwPopupButtonColorPallet._(
{required this.textColor,
required this.textPressedColor,
required this.textDisabledColor,
required this.bgColor,
required this.pressedBdColor,
required this.disabledBdColor,
required this.iconColor,
required this.iconPressedColor,
required this.iconDisabledColor,
required this.dropDownBgColor,
required this.dropDownPressedBgColor,
required this.dropDownDisabledBgColor});
factory KwPopupButtonColorPallet.dark() {
return const KwPopupButtonColorPallet._(
textColor: AppColors.grayWhite,
textPressedColor: AppColors.grayWhite,
textDisabledColor: AppColors.grayWhite,
bgColor: AppColors.bgColorDark,
pressedBdColor: AppColors.darkBgActiveButtonState,
disabledBdColor: AppColors.grayDisable,
iconColor: AppColors.grayWhite,
iconPressedColor: AppColors.grayWhite,
iconDisabledColor: AppColors.grayWhite,
dropDownBgColor: AppColors.darkBgBgElements,
dropDownPressedBgColor: AppColors.darkBgStroke,
dropDownDisabledBgColor: AppColors.grayDisable,
);
}
factory KwPopupButtonColorPallet.yellow() {
return const KwPopupButtonColorPallet._(
textColor: AppColors.blackBlack,
textPressedColor: AppColors.blackBlack,
textDisabledColor: AppColors.grayWhite,
bgColor: AppColors.primaryYellow,
pressedBdColor: AppColors.buttonPrimaryYellowActive,
disabledBdColor: AppColors.grayDisable,
iconColor: AppColors.blackBlack,
iconPressedColor: AppColors.blackBlack,
iconDisabledColor: AppColors.grayWhite,
dropDownBgColor: AppColors.buttonPrimaryYellowDrop,
dropDownPressedBgColor: AppColors.buttonPrimaryYellowActiveDrop,
dropDownDisabledBgColor: AppColors.grayDisable,
);
}
factory KwPopupButtonColorPallet.transparent() {
return const KwPopupButtonColorPallet._(
textColor: AppColors.blackBlack,
textPressedColor: AppColors.blackBlack,
textDisabledColor: AppColors.grayDisable,
bgColor: Colors.transparent,
pressedBdColor: Colors.transparent,
disabledBdColor: Colors.transparent,
iconColor: AppColors.blackBlack,
iconPressedColor: AppColors.grayWhite,
iconDisabledColor: AppColors.grayWhite,
dropDownBgColor: AppColors.buttonOutline,
dropDownPressedBgColor: AppColors.darkBgActiveButtonState,
dropDownDisabledBgColor: AppColors.grayDisable,
);
}
KwPopupButtonColorPallet copyWith({
Color? textColor,
Color? textPressedColor,
Color? textDisabledColor,
Color? bgColor,
Color? pressedBdColor,
Color? disabledBdColor,
Color? iconColor,
Color? iconPressedColor,
Color? iconDisabledColor,
Color? dropDownBgColor,
Color? dropDownPressedBgColor,
Color? dropDownDisabledBgColor,
}) {
return KwPopupButtonColorPallet._(
textColor: textColor ?? this.textColor,
textPressedColor: textPressedColor ?? this.textPressedColor,
textDisabledColor: textDisabledColor ?? this.textDisabledColor,
bgColor: bgColor ?? this.bgColor,
pressedBdColor: pressedBdColor ?? this.pressedBdColor,
disabledBdColor: disabledBdColor ?? this.disabledBdColor,
iconColor: iconColor ?? this.iconColor,
iconPressedColor: iconPressedColor ?? this.iconPressedColor,
iconDisabledColor: iconDisabledColor ?? this.iconDisabledColor,
dropDownBgColor: dropDownBgColor ?? this.dropDownBgColor,
dropDownPressedBgColor:
dropDownPressedBgColor ?? this.dropDownPressedBgColor,
dropDownDisabledBgColor:
dropDownDisabledBgColor ?? this.dropDownDisabledBgColor,
);
}
}
class _KwButtonPopUpOverlayMenu extends StatefulWidget {
const _KwButtonPopUpOverlayMenu({
required this.child,
required this.controller,
required this.opacity,
required this.layerLink,
required this.popUpPadding,
required this.items,
});
final Widget child;
final _KwButtonListenableOverlayPortalController controller;
final double opacity;
final LayerLink layerLink;
final double popUpPadding;
final List<KwPopUpButtonItem> items;
@override
State<_KwButtonPopUpOverlayMenu> createState() =>
_KwButtonPopUpOverlayMenuState();
}
class _KwButtonPopUpOverlayMenuState extends State<_KwButtonPopUpOverlayMenu> {
late final _KwButtonListenableOverlayPortalController _controller;
@override
void initState() {
_controller = widget.controller;
_controller.addListener(() {
try {
if (context.mounted) {
setState(() {});
}
} catch (e) {
print(e);
}
});
super.initState();
}
@override
Widget build(BuildContext context) {
return OverlayPortal(
controller: _controller,
overlayChildBuilder: (context) {
return CompositedTransformFollower(
followerAnchor: Alignment.bottomCenter,
targetAnchor: Alignment.topCenter,
offset: const Offset(0, -8),
link: widget.layerLink,
child: GestureDetector(
onTap: () {
_controller.hide();
},
child: Container(
color: Colors.transparent,
child: Padding(
padding:
EdgeInsets.symmetric(horizontal: widget.popUpPadding),
child: AnimatedOpacity(
duration: const Duration(milliseconds: 150),
opacity: widget.opacity,
child: Align(
alignment: Alignment.bottomCenter,
child: Container(
decoration: BoxDecoration(
color: AppColors.grayWhite,
borderRadius: BorderRadius.circular(24),
border: Border.all(
color: AppColors.grayTintStroke, width: 1),
boxShadow: [
BoxShadow(
color:
Colors.black.withAlpha((255 * 0.07).toInt()),
offset: const Offset(0, 8),
blurRadius: 17,
),
BoxShadow(
color:
Colors.black.withAlpha((255 * 0.06).toInt()),
offset: const Offset(0, 30),
blurRadius: 30,
)
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisSize: MainAxisSize.min,
children: widget.items
.expand((e) => [
_buildItem(e.title, e.onTap, e.color),
if (widget.items.last != e)
const Divider(
color: AppColors.grayTintStroke,
height: 0,
),
])
.toList(),
),
),
),
),
),
)),
);
},
child: widget.child,
);
}
Widget _buildItem(String title, VoidCallback onTap, Color color) {
return GestureDetector(
onTap: () {
onTap();
_controller.hide();
},
child: Container(
height: 52,
color: Colors.transparent,
child: Center(
child: Text(
title,
style: AppTextStyles.bodyMediumMed.copyWith(color: color),
),
),
),
);
}
@override
void dispose() {
if (context.mounted) _controller.hide();
super.dispose();
}
}
class _KwButtonListenableOverlayPortalController
extends OverlayPortalController {
List<VoidCallback> listeners = [];
Future<void> Function()? onShow;
Future<void> Function()? onHide;
_KwButtonListenableOverlayPortalController();
addOnShowListener(Future<void> Function() listener) {
onShow = listener;
}
addOnHideListener(Future<void> Function() listener) {
onHide = listener;
}
@override
void show() async {
super.show();
try {
for (var element in listeners) {
element();
}
} catch (e) {
if (kDebugMode) {
print(e);
}
}
if (onShow != null) {
await onShow!();
}
}
@override
void hide() async {
if (onHide != null) {
await onHide!();
}
try {
super.hide();
} catch (e) {
if (kDebugMode) {
print(e);
}
}
for (var element in listeners) {
try {
element();
} catch (e) {
if (kDebugMode) {
print(e);
}
}
}
}
void addListener(VoidCallback listener) {
listeners.add(listener);
}
}

View File

@@ -0,0 +1,169 @@
import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:krow/core/presentation/gen/assets.gen.dart';
import 'package:krow/core/presentation/styles/kw_text_styles.dart';
import 'package:krow/core/presentation/styles/theme.dart';
import 'package:krow/core/presentation/widgets/ui_kit/_custom_popup_menu.dart';
enum KwPopupMenuFit { expand, loose }
class KwPopupMenu extends StatefulWidget {
final Widget Function(BuildContext, bool menuIsShowmn)? customButtonBuilder;
final List<KwPopupMenuItem> menuItems;
final double? horizontalMargin;
final KwPopupMenuFit fit;
final double horizontalPadding;
final CustomPopupMenuController? controller;
const KwPopupMenu({
this.customButtonBuilder,
required this.menuItems,
this.fit = KwPopupMenuFit.loose,
this.horizontalMargin,
this.horizontalPadding = 0,
this.controller,
super.key,
});
@override
State<KwPopupMenu> createState() => _KwPopupMenuState();
}
class _KwPopupMenuState extends State<KwPopupMenu> {
late CustomPopupMenuController _controller;
@override
void initState() {
_controller = widget.controller ?? CustomPopupMenuController();
super.initState();
_controller.addListener(() {
if (mounted) setState(() {});
});
}
@override
Widget build(BuildContext context) {
return CustomPopupMenu(
horizontalMargin: widget.horizontalMargin ?? 0,
controller: _controller,
verticalMargin: 4,
position: PreferredPosition.bottom,
showArrow: false,
enablePassEvent: false,
barrierColor: Colors.transparent,
menuBuilder: widget.menuItems.isEmpty
? null
: () {
return Row(
mainAxisSize: widget.fit == KwPopupMenuFit.expand
? MainAxisSize.max
: MainAxisSize.min,
children: [
widget.fit == KwPopupMenuFit.expand
? Expanded(
child: _buildItem(),
)
: _buildItem()
],
);
},
pressType: PressType.singleClick,
child: widget.customButtonBuilder
?.call(context, _controller.menuIsShowing) ??
AnimatedContainer(
duration: const Duration(milliseconds: 200),
height: 32,
width: 32,
decoration: BoxDecoration(
color: _controller.menuIsShowing
? AppColors.grayTintStroke
: AppColors.grayWhite,
shape: BoxShape.circle,
border: _controller.menuIsShowing
? null
: Border.all(color: AppColors.grayTintStroke, width: 1)),
child: Center(child: Assets.images.icons.more.svg()),
),
);
}
Container _buildItem() {
return Container(
margin: EdgeInsets.symmetric(horizontal: widget.horizontalPadding),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
border: Border.all(color: AppColors.grayTintStroke, width: 1),
color: AppColors.grayWhite,
),
child: Container(
constraints: const BoxConstraints(
maxHeight: 210,
),
child: SingleChildScrollView(
child: IntrinsicWidth(
child: Column(
children: [
for (var i = 0; i < widget.menuItems.length; i++)
Material(
type: MaterialType.transparency,
child: InkWell(
onTap: () {
widget.menuItems[i].onTap();
_controller.hideMenu();
},
child: Container(
height: 42,
padding: const EdgeInsets.symmetric(horizontal: 16),
decoration: i == 0
? null
: const BoxDecoration(
border: Border(
top: BorderSide(
color: AppColors.grayTintStroke,
width: 1,
),
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (widget.menuItems[i].icon != null) ...[
widget.menuItems[i].icon!,
const Gap(4),
],
Expanded(
child: Text(
widget.menuItems[i].title,
style: widget.menuItems[i].textStyle ??
AppTextStyles.bodyMediumReg,
)),
],
),
),
),
),
],
),
),
),
),
);
}
}
class KwPopupMenuItem {
final String title;
final Widget? icon;
final VoidCallback onTap;
final TextStyle? textStyle;
const KwPopupMenuItem({
required this.title,
required this.onTap,
this.icon,
this.textStyle,
});
}

View File

@@ -0,0 +1,133 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:krow/core/presentation/widgets/ui_kit/_custom_popup_menu.dart';
import 'package:krow/core/presentation/widgets/ui_kit/kw_input.dart';
import 'package:krow/core/presentation/widgets/ui_kit/kw_popup_menu.dart';
@immutable
class KwSuggestionInput<R> extends StatefulWidget {
final debounceDuration = const Duration(milliseconds: 400);
final String? title;
final String? hintText;
final Iterable<R> items;
final String Function(R item) itemToStringBuilder;
final Function(R item) onSelected;
final Function(String query) onQueryChanged;
final String? initialText;
final double horizontalPadding;
final Color? backgroundColor;
final Color? borderColor;
const KwSuggestionInput({
super.key,
this.initialText,
required this.hintText,
required this.itemToStringBuilder,
required this.items,
required this.onSelected,
required this.onQueryChanged,
this.horizontalPadding = 0,
this.backgroundColor,
this.borderColor,
this.title,
});
@override
State<KwSuggestionInput<R>> createState() => _KwSuggestionInputState<R>();
}
class _KwSuggestionInputState<R> extends State<KwSuggestionInput<R>> {
R? selectedItem;
var dropdownController = CustomPopupMenuController();
late TextEditingController _textController;
late FocusNode _focusNode;
Timer? _debounce;
UniqueKey key = UniqueKey();
@override
void initState() {
super.initState();
_textController = TextEditingController(text: widget.initialText);
_focusNode = FocusNode();
_textController.addListener(_onTextChanged);
}
@override
void dispose() {
_textController.removeListener(_onTextChanged);
_textController.dispose();
_debounce?.cancel();
dropdownController.dispose();
_focusNode.dispose();
super.dispose();
}
void _onTextChanged() {
if (_debounce?.isActive ?? false) _debounce?.cancel();
_debounce = Timer(widget.debounceDuration, () {
if (selectedItem == null ||
widget.itemToStringBuilder(selectedItem as R) !=
_textController.text) {
widget.onQueryChanged(_textController.text);
}
});
}
@override
void didUpdateWidget(covariant KwSuggestionInput<R> oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.initialText != widget.initialText) {
_textController.text = widget.initialText!;
}
WidgetsBinding.instance.addPostFrameCallback(
(_) {
if (oldWidget.items != widget.items) {
dropdownController.showMenu();
} else {
dropdownController.setState();
}
},
);
}
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
KwPopupMenu(
controller: dropdownController,
horizontalPadding: widget.horizontalPadding,
fit: KwPopupMenuFit.expand,
customButtonBuilder: (context, isOpened) {
return _buildMenuButton(isOpened);
},
menuItems: widget.items
.map((item) => KwPopupMenuItem(
title: widget.itemToStringBuilder(item),
onTap: () {
selectedItem = item;
_textController.text = widget.itemToStringBuilder(item);
dropdownController.hideMenu();
widget.onSelected(item);
}))
.toList()),
],
);
}
Widget _buildMenuButton(bool isOpened) {
return KwTextInput(
title: widget.title,
hintText: widget.hintText,
controller: _textController,
focusNode: _focusNode,
);
}
}

View File

@@ -0,0 +1,289 @@
import 'package:flutter/material.dart';
import 'package:krow/core/presentation/styles/kw_text_styles.dart';
import 'package:krow/core/presentation/styles/theme.dart';
class KwTabBar extends StatefulWidget {
final List<String> tabs;
final void Function(int index) onTap;
final List<int>? flexes;
final bool forceScroll;
const KwTabBar(
{super.key,
required this.tabs,
required this.onTap,
this.flexes,
this.forceScroll = false});
@override
State<KwTabBar> createState() => _KwTabBarState();
}
class _KwTabBarState extends State<KwTabBar>
with SingleTickerProviderStateMixin {
var keyMaps = <int, GlobalKey>{};
var tabPadding = 4.0;
int _selectedIndex = 0;
late AnimationController _controller;
late Animation<double> _animation;
late ScrollController _horScrollController;
@override
void initState() {
super.initState();
_horScrollController = ScrollController();
_controller = AnimationController(
duration: const Duration(milliseconds: 200),
vsync: this,
);
_animation = Tween<double>(begin: 0, end: 0).animate(_controller);
}
@override
dispose() {
_controller.dispose();
_horScrollController.dispose();
_animation.removeListener(() {});
super.dispose();
}
void _setSelectedIndex(int index) {
if (_horScrollController.hasClients) {
_scrollToSelected(index);
}
setState(() {
_selectedIndex = index;
_animation = Tween<double>(
begin: _animation.value,
end: index.toDouble(),
).animate(CurvedAnimation(
parent: _controller,
curve: Curves.easeInOut,
));
_controller.forward(from: 0);
});
widget.onTap(index);
}
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (context, constraints) {
double totalWidth = widget.tabs
.fold(0, (sum, tab) => sum + _calculateTabWidth(tab, 0, context));
totalWidth += (widget.tabs.length) * tabPadding + 26; //who is 26?
bool needScroll = widget.forceScroll ||
widget.flexes == null ||
totalWidth > constraints.maxWidth;
return _buildTabsRow(context, needScroll, constraints.maxWidth);
},
);
}
Widget _buildTabsRow(BuildContext context, bool needScroll, maxWidth) {
return SizedBox(
width: maxWidth,
child: needScroll
? SingleChildScrollView(
physics: const BouncingScrollPhysics(),
controller: _horScrollController,
scrollDirection: Axis.horizontal,
child: Stack(
children: [
_buildAnimatedUnderline(false),
_buildRow(
context,
),
],
),
)
: Stack(
children: [
_buildAnimatedUnderline(true),
_buildRow(context, fixedWidth: true),
],
),
);
}
Widget _buildRow(BuildContext context, {bool fixedWidth = false}) {
return Padding(
padding: const EdgeInsets.only(left: 16, right: 16),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: widget.tabs
.asMap()
.map((index, tab) => MapEntry(
index,
_buildSingleTab(index, tab, context, fixedWidth: fixedWidth),
))
.values
.toList(),
),
);
}
Widget _buildSingleTab(int index, String tab, BuildContext context,
{required bool fixedWidth}) {
double? itemWidth;
var d = (MediaQuery.of(context).size.width -
(tabPadding * widget.tabs.length) -
32);
if (widget.flexes != null) {
itemWidth = d *
(widget.flexes?[index] ?? 1) /
(widget.flexes?.reduce((a, b) => a + b) ?? 1);
} else {
itemWidth = (d / (widget.tabs.length));
}
if (keyMaps[index] == null) {
keyMaps[index] = GlobalKey();
}
return GestureDetector(
key: keyMaps[index],
onTap: () {
_setSelectedIndex(index);
},
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(23),
color: Colors.transparent,
border: Border.all(
color: _selectedIndex != index
? AppColors.grayStroke
: Colors.transparent,
width: 1,
),
),
padding: const EdgeInsets.symmetric(horizontal: 18),
margin: EdgeInsets.only(right: tabPadding),
height: 46,
width: fixedWidth ? itemWidth : null,
alignment: Alignment.center,
child: AnimatedDefaultTextStyle(
duration: const Duration(milliseconds: 200),
style: _selectedIndex == index
? AppTextStyles.bodySmallReg.copyWith(color: AppColors.grayWhite)
: AppTextStyles.bodySmallMed.copyWith(color: AppColors.blackGray),
child: Text(tab),
),
),
);
}
Widget _buildAnimatedUnderline(bool fixedWidth) {
double? tabWidth;
var d = (MediaQuery.of(context).size.width -
(tabPadding * widget.tabs.length) -
32);
if (!fixedWidth) {
tabWidth = null;
} else if (widget.flexes != null) {
tabWidth = d *
(widget.flexes?[_selectedIndex] ?? 1) /
(widget.flexes?.reduce((a, b) => a + b) ?? 1);
} else {
tabWidth = (d / (widget.tabs.length));
}
var content = AnimatedContainer(
duration: const Duration(milliseconds: 200),
width: tabWidth ??
_calculateTabWidth(
widget.tabs[_selectedIndex], _selectedIndex, context),
height: 46,
decoration: BoxDecoration(
color: AppColors.bgColorDark,
borderRadius: BorderRadius.circular(23),
),
);
return fixedWidth && widget.flexes == null
? AnimatedBuilder(
animation: _animation,
builder: (context, child) {
return Padding(
padding: const EdgeInsets.only(left: 16, right: 16),
child: Align(
alignment: Alignment(
(_animation.value * 2 / (widget.tabs.length - 1)) - 1,
1,
),
child: content,
),
);
},
)
: animatedPadding(content);
}
AnimatedPadding animatedPadding(AnimatedContainer content) {
return AnimatedPadding(
curve: Curves.easeIn,
padding: EdgeInsets.only(left: calcTabOffset(_selectedIndex)),
duration: const Duration(milliseconds: 250),
child: content,
);
}
double _calculateTabWidth(String tab, int index, BuildContext context) {
final textPainter = TextPainter(
text: TextSpan(
text: tab,
style: _selectedIndex == index
? AppTextStyles.bodySmallReg
: AppTextStyles.bodySmallMed,
),
maxLines: 1,
textDirection: TextDirection.ltr,
)..layout();
return textPainter.width + 36; // 36?
}
double calcTabOffset(index) {
var scrollOffset =
_horScrollController.hasClients ? _horScrollController.offset : 0.0;
final keyContext = keyMaps[index]?.currentContext;
if (keyContext != null) {
final box = keyContext.findRenderObject() as RenderBox;
return (box.localToGlobal(Offset.zero).dx + scrollOffset)
.clamp(0, double.infinity);
} else {
return 0;
}
}
void _scrollToSelected(int index) {
print(index);
double offset = 0;
double tabWidth = _calculateTabWidth(widget.tabs[index], index, context);
double screenWidth = MediaQuery.of(context).size.width;
offset = calcTabOffset(index);
double maxScrollExtent = _horScrollController.position.maxScrollExtent;
double targetOffset = offset - (screenWidth - tabWidth) / 2;
if (targetOffset < 0) {
targetOffset = 0;
} else if (targetOffset > maxScrollExtent) {
targetOffset = maxScrollExtent;
}
_horScrollController.animateTo(
targetOffset,
duration: const Duration(milliseconds: 200),
curve: Curves.easeInOut,
);
}
}