feat: Refactor code structure and optimize performance across multiple modules
This commit is contained in:
@@ -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),
|
||||
);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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()),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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 Didn’t 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',
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
]
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
},
|
||||
),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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});
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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(),
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
]
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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]),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user