feat: legacy mobile apps created

This commit is contained in:
Achintha Isuru
2025-12-02 23:51:04 -05:00
parent 850441ca64
commit 8e7753b324
1519 changed files with 0 additions and 16 deletions

View File

@@ -0,0 +1,96 @@
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:graphql_flutter/graphql_flutter.dart';
import 'package:injectable/injectable.dart';
import 'package:krow/core/application/di/injectable.dart';
import 'package:path_provider/path_provider.dart';
class GraphQLConfig {
static HttpLink httpLink = HttpLink(
dotenv.env['BASE_URL']!,
defaultHeaders: {
'Content-Type': 'application/json',
},
);
static AuthLink authLink = AuthLink(
getToken: () async {
User? user = FirebaseAuth.instance.currentUser;
if (user != null) {
return 'Bearer ${await user.getIdToken()}';
}
return null;
},
);
static Link link = authLink.concat(httpLink);
Future<GraphQLClient> configClient() async {
var path = await getApplicationDocumentsDirectory();
Store? store;
if (currentEnv != 'background') {
store = await HiveStore.open(path: '${path.path}/gql_cache');
}
return GraphQLClient(
link: link,
cache: GraphQLCache(
store: store,
),
defaultPolicies: DefaultPolicies(
mutate: Policies(
cacheReread: CacheRereadPolicy.ignoreOptimisitic,
error: ErrorPolicy.all,
fetch: FetchPolicy.networkOnly,
),
query: Policies(
cacheReread: CacheRereadPolicy.ignoreOptimisitic,
error: ErrorPolicy.all,
fetch: FetchPolicy.networkOnly,
),
));
}
}
@Singleton()
class ApiClient {
static final GraphQLConfig _graphQLConfig = GraphQLConfig();
final Future<GraphQLClient> _client = _graphQLConfig.configClient();
Future<QueryResult> query(
{required String schema, Map<String, dynamic>? body}) async {
final QueryOptions options = QueryOptions(
document: gql(schema),
variables: body ?? {},
);
return (await _client).query(options);
}
Stream<QueryResult?> queryWithCache({
required String schema,
Map<String, dynamic>? body,
}) async* {
final WatchQueryOptions options = WatchQueryOptions(
document: gql(schema),
variables: body ?? {},
fetchPolicy: FetchPolicy.cacheAndNetwork);
var result = (await _client).watchQuery(options).fetchResults();
yield result.eagerResult;
yield await result.networkResult;
}
Future<QueryResult> mutate(
{required String schema, Map<String, dynamic>? body}) async {
final MutationOptions options = MutationOptions(
document: gql(schema),
variables: body ?? {},
);
return (await _client).mutate(options);
}
void dropCache() async {
(await _client).cache.store.reset();
}
}

View File

@@ -0,0 +1,54 @@
import 'package:graphql_flutter/graphql_flutter.dart';
interface class DisplayableException {
final String message;
DisplayableException(this.message);
}
class NetworkException implements Exception, DisplayableException {
@override
final String message;
NetworkException([this.message = 'No internet connection']);
@override
String toString() {
return message;
}
}
class GraphQLException implements Exception, DisplayableException {
@override
final String message;
GraphQLException(this.message);
@override
String toString() {
return message;
}
}
class UnknownException implements Exception, DisplayableException {
@override
final String message;
UnknownException([this.message = 'Something went wrong']);
@override
String toString() {
return message;
}
}
Exception parseBackendError(dynamic error) {
if (error is OperationException) {
if (error.graphqlErrors.isNotEmpty) {
return GraphQLException(
error.graphqlErrors.firstOrNull?.message ?? 'GraphQL error occurred');
}
return NetworkException('Network error occurred');
}
return UnknownException();
}

View File

@@ -0,0 +1,90 @@
const String staffFragment = '''
fragment StaffFields on Staff {
id
first_name
last_name
middle_name
email
phone
status
address
avatar
average_rating
}
''';
const String roleKitFragment = '''
fragment RoleKit on RoleKit {
id
name
is_required
photo_required
}
''';
const String skillCategoryFragment = '''
fragment SkillCategory on Skill {
category {
name
slug
}
}
''';
const String skillFragment = '''
$skillCategoryFragment
$roleKitFragment
fragment SkillFragment on Skill {
id
name
slug
price
uniforms {
...RoleKit
}
equipments {
...RoleKit
}
...SkillCategory
}
''';
String getSkillsQuery = '''
$skillFragment
query GetSkills {
skills {
...SkillFragment
}
}
''';
const String getStaffRolesQuery = '''
$skillFragment
query GetStaffRoles {
staff_roles {
id
skill {
...SkillFragment
}
confirmed_uniforms {
id
skill_kit_id
photo
}
confirmed_equipments {
id
skill_kit_id
photo
}
level
experience
status
}
}
''';

View File

@@ -0,0 +1,4 @@
extension BoolExtension on bool {
/// Returns 0 if boolean is true and 1 otherwise.
int toInt() => this ? 0 : 1;
}

View File

@@ -0,0 +1,32 @@
import 'package:intl/intl.dart';
extension DateTimeExtension on DateTime {
String toHourMinuteString() {
return '${hour.toString().padLeft(2, '0')}:'
'${minute.toString().padLeft(2, '0')}';
}
String getDayDateId() => '$year$month$day';
String getWeekdayId() => switch (weekday) {
DateTime.monday => 'monday',
DateTime.tuesday => 'tuesday',
DateTime.wednesday => 'wednesday',
DateTime.thursday => 'thursday',
DateTime.friday => 'friday',
DateTime.saturday => 'saturday',
_ => 'sunday',
};
String toDayMonthYearString() => DateFormat('dd.MM.yyyy').format(this);
DateTime tillDay() {
return DateTime(year, month, day);
}
DateTime toTenthsMinute() {
if (minute % 10 == 0) return this;
return DateTime(year, month, day, hour, minute - minute % 10);
}
}

View File

@@ -0,0 +1,17 @@
extension IntExtensions on int {
String toOrdinal() {
if (this >= 11 && this <= 13) {
return '${this}th';
}
switch (this % 10) {
case 1:
return '${this}st';
case 2:
return '${this}nd';
case 3:
return '${this}rd';
default:
return '${this}th';
}
}
}

View File

@@ -0,0 +1,27 @@
import 'dart:developer';
import 'package:flutter/foundation.dart';
class Logger {
static void error(e, {String? message}) {
log(
e.toString(),
error: message ?? e,
level: 2000,
stackTrace: StackTrace.current,
name: 'ERROR',
);
}
static debug(dynamic msg, {String? message}) {
if (kDebugMode) {
log(
msg.toString(),
name: message ?? msg.runtimeType.toString(),
);
}
}
}
const error = Logger.error;
const debug = Logger.debug;

View File

@@ -0,0 +1,19 @@
import 'package:map_launcher/map_launcher.dart';
class MapUtils {
MapUtils._();
static Future<void> openMapByLatLon(double latitude, double longitude) async {
if (await MapLauncher.isMapAvailable(MapType.google) ?? false) {
await MapLauncher.showDirections(
mapType: MapType.google,
destination: Coords(latitude, longitude),
);
} else {
final availableMaps = await MapLauncher.installedMaps;
await availableMaps.first.showDirections(
destination: Coords(latitude, longitude),
);
}
}
}

View File

@@ -0,0 +1,16 @@
import 'package:krow/core/presentation/widgets/ui_kit/kw_dropdown.dart';
extension StringExtensions on String {
String capitalize() {
if (isEmpty) return this;
return this[0].toUpperCase() + substring(1).toLowerCase();
}
}
extension StringToDropdownItem on String {
KwDropDownItem<String>? toDropDownItem() {
if (isEmpty) return null;
return KwDropDownItem(data: this, title: this);
}
}

View File

@@ -0,0 +1,36 @@
import 'package:flutter/services.dart';
class DateTextFormatter extends TextInputFormatter {
@override
TextEditingValue formatEditUpdate(
TextEditingValue oldValue, TextEditingValue newValue,) {
if (oldValue.text.length >= newValue.text.length) {
return newValue;
}
String filteredText = newValue.text.replaceAll(RegExp(r'[^0-9.]'), '');
var dateText = _addSeparators(filteredText, '.');
return newValue.copyWith(
text: dateText, selection: updateCursorPosition(dateText));
}
String _addSeparators(String value, String separator) {
value = value.replaceAll('.', '');
var newString = '';
for (int i = 0; i < value.length; i++) {
newString += value[i];
if (i == 1) {
newString += separator;
}
if (i == 3) {
newString += separator;
}
}
return newString;
}
TextSelection updateCursorPosition(String text) {
return TextSelection.fromPosition(TextPosition(offset: text.length));
}
}

View File

@@ -0,0 +1,23 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:intl/intl.dart';
class CertificateDateValidator {
CertificateDateValidator._();
static String? validate(String value) {
try {
final date = DateFormat('MM.dd.yyyy').parse(value);
var splited = value.split('.');
if (int.parse(splited.first) > 12 || int.parse(splited[1]) > 31) {
return 'invalid_date_format'.tr();
}
if (date.isBefore(DateTime.now())) {
return 'date_cannot_be_past'.tr();
}
} catch (e) {
return null;
}
return null;
}
}

View File

@@ -0,0 +1,17 @@
import 'package:easy_localization/easy_localization.dart';
abstract class EmailValidator {
static String? validate(String? email, {bool isRequired = true}) {
// Regular expression for validating emails
final RegExp phoneRegExp = RegExp(
r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$',
);
if (isRequired && (email == null || email.isEmpty)) {
return 'email_is_required'.tr();
} else if (email != null && !phoneRegExp.hasMatch(email)) {
return 'invalid_email'.tr();
}
return null;
}
}

View File

@@ -0,0 +1,15 @@
import 'package:easy_localization/easy_localization.dart';
abstract class PhoneValidator {
static String? validate(String? phoneNumber, {bool isRequired = true}) {
// Regular expression for validating phone numbers
final RegExp phoneRegExp = RegExp(r'^\+?[1-9]\d{1,14}$');
if (isRequired && (phoneNumber==null || phoneNumber.isEmpty)) {
return 'phone_number_is_required'.tr();
} else if (phoneNumber !=null && !phoneRegExp.hasMatch(phoneNumber)) {
return 'invalid_phone_number'.tr();
}
return null;
}
}

View File

@@ -0,0 +1,21 @@
import 'package:easy_localization/easy_localization.dart';
class SkillExpValidator {
SkillExpValidator._();
static String? validate(String value) {
if (value.isEmpty) {
return 'experience_is_required'.tr();
}
var parsed = int.tryParse(value);
if (value.isNotEmpty && parsed == null) {
return 'experience_must_be_a_number'.tr();
}
if (value.isNotEmpty && parsed! > 20 || parsed! < 1) {
return 'experience_must_be_between_1_and_20'.tr();
}
return null;
}
}

View File

@@ -0,0 +1,14 @@
abstract class SocialNumberValidator {
static String? validate(String? socialNumber, {bool isRequired = true}) {
// Regular expression for validating US social number
final ssnRegex =
RegExp(r'^(?!000|666|9\d{2})\d{3}-(?!00)\d{2}-(?!0000)\d{4}$');
if (isRequired && (socialNumber == null || socialNumber.isEmpty)) {
return 'Social number is required';
} else if (socialNumber != null && !ssnRegex.hasMatch(socialNumber)) {
return 'Invalid social number';
}
return null;
}
}

View File

@@ -0,0 +1,14 @@
import 'package:get_it/get_it.dart';
import 'package:injectable/injectable.dart';
import 'injectable.config.dart';
final getIt = GetIt.instance;
late final String currentEnv;
@InjectableInit()
void configureDependencies(String env) {
currentEnv = env;
GetIt.instance.init(environment: env);
}

View File

@@ -0,0 +1,70 @@
import 'package:auto_route/auto_route.dart';
import 'package:krow/core/application/routing/routes.gr.dart';
import 'package:krow/core/sevices/auth_state_service/auth_service.dart';
class SplashRedirectGuard extends AutoRouteGuard {
final AuthService authService;
SplashRedirectGuard(this.authService);
@override
Future<void> onNavigation(
NavigationResolver resolver,
StackRouter router,
) async {
final status = await authService.getAuthStatus();
switch (status) {
case AuthStatus.authenticated:
router.replace(const HomeRoute());
break;
case AuthStatus.adminValidation:
router.replace(const WaitingValidationRoute());
break;
case AuthStatus.prepareProfile:
//TODO(Heorhii): Some additional adjustment from backend are required before enabling this screen
router.replace(const CheckListFlowRoute());
break;
case AuthStatus.unauthenticated:
router.replace(const AuthFlowRoute());
break;
case AuthStatus.error:
//todo(Artem):show error screen
router.replace(const AuthFlowRoute());
}
resolver.next(false);
}
}
class SingUpRedirectGuard extends AutoRouteGuard {
final AuthService authService;
SingUpRedirectGuard(this.authService);
@override
Future<void> onNavigation(
NavigationResolver resolver, StackRouter router) async {
final status = await authService.getAuthStatus();
switch (status) {
case AuthStatus.authenticated:
router.replace(const HomeRoute());
break;
case AuthStatus.adminValidation:
router.replace(const WaitingValidationRoute());
break;
case AuthStatus.prepareProfile:
resolver.next(true);
return;
case AuthStatus.unauthenticated:
router.replace(const AuthFlowRoute());
break;
case AuthStatus.error:
//todo(Artem):show error screen
router.replace(const AuthFlowRoute());
}
resolver.next(false);
}
}

View File

@@ -0,0 +1,183 @@
import 'package:auto_route/auto_route.dart';
import 'package:krow/core/application/di/injectable.dart';
import 'package:krow/core/application/routing/guards.dart';
import 'package:krow/core/application/routing/routes.gr.dart';
import 'package:krow/core/sevices/auth_state_service/auth_service.dart';
@AutoRouterConfig()
class AppRouter extends RootStackRouter {
@override
RouteType get defaultRouteType => const RouteType.material();
@override
List<AutoRoute> get routes => [
AdaptiveRoute(
path: '/',
page: SplashRoute.page,
initial: true,
guards: [SplashRedirectGuard(getIt<AuthService>())],
),
authFlow,
signUnFlow,
homeBottomBarFlow,
phoneReLoginFlow,
checkListFlow,
AutoRoute(page: WaitingValidationRoute.page),
];
get homeBottomBarFlow => AdaptiveRoute(
path: '/home',
page: HomeRoute.page,
children: [earningsFlow, shiftsFlow, profileFlow]);
get authFlow => AdaptiveRoute(
path: '/auth',
page: AuthFlowRoute.page,
children: [
AutoRoute(path: 'welcome', page: WelcomeRoute.page, initial: true),
AutoRoute(path: 'phone', page: PhoneVerificationRoute.page),
AutoRoute(path: 'code', page: CodeVerificationRoute.page),
],
);
get phoneReLoginFlow => AdaptiveRoute(
path: '/phoneReLogin',
page: PhoneReLoginFlowRoute.page,
children: [
AutoRoute(page: CodeVerificationRoute.page, initial: true),
],
);
get signUnFlow => AdaptiveRoute(
path: '/signup',
page: SignupFlowRoute.page,
guards: [SingUpRedirectGuard(getIt<AuthService>())],
children: [
AutoRoute(page: PersonalInfoRoute.page, initial: true),
AutoRoute(page: EmailVerificationRoute.page),
AutoRoute(page: EmergencyContactsRoute.page),
AutoRoute(page: MobilityRoute.page),
AutoRoute(page: InclusiveRoute.page),
AutoRoute(page: AddressRoute.page),
AutoRoute(page: WorkingAreaRoute.page),
AutoRoute(page: RoleRoute.page),
],
);
get checkListFlow => AdaptiveRoute(
path: '/check_list',
page: CheckListFlowRoute.page,
children: [
AutoRoute(page: CheckListRoute.page, initial: true),
AutoRoute(page: PersonalInfoRoute.page),
AutoRoute(page: EmailVerificationRoute.page),
AutoRoute(page: EmergencyContactsRoute.page),
AutoRoute(page: AddressRoute.page),
AutoRoute(page: WorkingAreaRoute.page),
AutoRoute(page: RoleRoute.page),
AutoRoute(page: CertificatesRoute.page),
AutoRoute(page: ScheduleRoute.page),
bankAccFlow,
wagesFormFlow,
roleKitFlow
],
);
get profileFlow => AdaptiveRoute(
path: 'profile',
page: ProfileMainFlowRoute.page,
children: [
AutoRoute(path: 'menu', page: ProfileMainRoute.page, initial: true),
roleKitFlow,
AutoRoute(path: 'certificates', page: CertificatesRoute.page),
AutoRoute(path: 'schedule', page: ScheduleRoute.page),
AutoRoute(path: 'working_area', page: WorkingAreaRoute.page),
AutoRoute(path: 'live_photo', page: LivePhotoRoute.page),
profileSettingsFlow,
wagesFormFlow,
bankAccFlow,
AutoRoute(path: 'benefits', page: BenefitsRoute.page),
AutoRoute(path: 'support', page: SupportRoute.page),
AutoRoute(path: 'faq', page: FaqRoute.page),
],
);
get bankAccFlow =>
AutoRoute(path: 'bank', page: BankAccountFlowRoute.page, children: [
AutoRoute(path: 'info', page: BankAccountRoute.page, initial: true),
AutoRoute(path: 'edit', page: BankAccountEditRoute.page),
]);
get wagesFormFlow => AutoRoute(
path: 'wages_form',
page: WagesFormsFlowRoute.page,
children: [
AutoRoute(path: 'list', page: FormsListRoute.page, initial: true),
AutoRoute(path: 'offerLatter', page: OfferLetterFormRoute.page),
AutoRoute(path: 'edd', page: EddFormRoute.page),
AutoRoute(path: 'i-9', page: INineFormRoute.page),
AutoRoute(path: 'w-4', page: WFourFormRoute.page),
AutoRoute(path: 'sign', page: FormSignRoute.page),
AutoRoute(path: 'signedForm', page: SignedFormRoute.page),
AutoRoute(path: 'preview', page: FormPreviewRoute.page),
],
);
get profileSettingsFlow => AutoRoute(
path: 'profile_settings',
page: ProfileSettingsFlowRoute.page,
children: [
AutoRoute(
path: 'menu',
page: ProfileSettingsMenuRoute.page,
initial: true,
),
AutoRoute(path: 'role', page: RoleRoute.page),
AutoRoute(path: 'personal', page: PersonalInfoRoute.page),
AutoRoute(
page: EmailVerificationRoute.page,
path: 'emailVerification',
),
AutoRoute(path: 'address', page: AddressRoute.page),
AutoRoute(path: 'emergency', page: EmergencyContactsRoute.page),
AutoRoute(path: 'mobility', page: MobilityRoute.page),
AutoRoute(path: 'inclusive', page: InclusiveRoute.page),
],
);
get roleKitFlow => AutoRoute(
path: 'role_kit',
page: RoleKitFlowRoute.page,
children: [
AutoRoute(path: 'list', page: RolesKitListRoute.page, initial: true),
AutoRoute(path: 'details', page: RoleKitRoute.page),
],
);
get earningsFlow => AdaptiveRoute(
path: 'earnings',
page: EarningsFlowRoute.page,
children: [
AutoRoute(path: 'list', page: EarningsRoute.page, initial: true),
AutoRoute(path: 'history', page: EarningsHistoryRoute.page),
],
);
get shiftsFlow => AdaptiveRoute(
path: 'shifts',
initial: true,
page: ShiftsFlowRoute.page,
children: [
AdaptiveRoute(
path: 'list',
page: ShiftsListMainRoute.page,
initial: true,
),
AdaptiveRoute(path: 'details', page: ShiftDetailsRoute.page),
AdaptiveRoute(path: 'scanner', page: QrScannerRoute.page)
],
);
@override
List<AutoRouteGuard> get guards => [];
}

View File

@@ -0,0 +1,12 @@
enum PaginationStatus {
initial(true),
loading(false),
idle(true),
empty(false),
error(true),
end(false);
const PaginationStatus(this.allowLoad);
final bool allowLoad;
}

View File

@@ -0,0 +1,12 @@
enum StaffSkillStatus {
verified,
pending,
declined,
deactivated,
}
enum StaffSkillLevel {
beginner,
skilled,
professional,
}

View File

@@ -0,0 +1,6 @@
enum StateStatus {
idle,
loading,
error,
success,
}

View File

@@ -0,0 +1,68 @@
import 'package:json_annotation/json_annotation.dart';
part 'pagination_wrapper.g.dart';
@JsonSerializable()
class PageInfo {
final bool? hasNextPage;
final bool? hasPreviousPage;
final String? startCursor;
final String? endCursor;
PageInfo({
required this.hasNextPage,
this.hasPreviousPage,
this.startCursor,
this.endCursor,
});
factory PageInfo.fromJson(Map<String, dynamic> json) {
return _$PageInfoFromJson(json);
}
Map<String, dynamic> toJson() => _$PageInfoToJson(this);
}
class Edge<T> {
final String? cursor;
final T node;
Edge({
required this.cursor,
required this.node,
});
factory Edge.fromJson(Map<String, dynamic> json) {
return Edge(
cursor: json['cursor'],
node: json['node'],
);
}
}
class PaginationWrapper<T> {
final PageInfo? pageInfo;
final List<Edge<T>> edges;
PaginationWrapper({
required this.pageInfo,
required this.edges,
});
factory PaginationWrapper.fromJson(
Map<String, dynamic> json,
T Function(Map<String, dynamic>) convertor,
) {
return PaginationWrapper(
pageInfo: PageInfo.fromJson(json['pageInfo']),
edges: (json['edges'] as List<dynamic>).map(
(e) {
return Edge(
cursor: e['cursor'],
node: convertor.call(e['node']),
);
},
).toList(),
);
}
}

View File

@@ -0,0 +1,22 @@
import 'package:json_annotation/json_annotation.dart';
part 'role_kit.g.dart';
@JsonSerializable(fieldRename: FieldRename.snake)
class RoleKit {
final String id;
final String? name;
final bool? isRequired;
final bool? photoRequired;
RoleKit({
required this.id,
required this.name,
required this.isRequired,
required this.photoRequired,
});
factory RoleKit.fromJson(Map<String, dynamic> json) => _$RoleKitFromJson(json);
Map<String, dynamic> toJson() => _$RoleKitToJson(this);
}

View File

@@ -0,0 +1,30 @@
import 'package:json_annotation/json_annotation.dart';
import 'package:krow/core/data/models/role_kit.dart';
import 'package:krow/core/data/models/skill_category.dart';
part 'skill.g.dart';
@JsonSerializable(fieldRename: FieldRename.snake)
class Skill {
final String id;
final String name;
final String? slug;
final double ?price;
final List<RoleKit>? uniforms;
final List<RoleKit>? equipments;
final SkillCategory? category;
Skill({
required this.id,
required this.name,
required this.slug,
required this.price,
required this.uniforms,
required this.equipments,
this.category,
});
factory Skill.fromJson(Map<String, dynamic> json) => _$SkillFromJson(json);
Map<String, dynamic> toJson() => _$SkillToJson(this);
}

View File

@@ -0,0 +1,18 @@
import 'package:json_annotation/json_annotation.dart';
part 'skill_category.g.dart';
@JsonSerializable(fieldRename: FieldRename.snake)
class SkillCategory {
final String name;
final String slug;
SkillCategory({
required this.name,
required this.slug,
});
factory SkillCategory.fromJson(Map<String, dynamic> json) => _$SkillCategoryFromJson(json);
Map<String, dynamic> toJson() => _$SkillCategoryToJson(this);
}

View File

@@ -0,0 +1,36 @@
import 'package:json_annotation/json_annotation.dart';
part 'bank_acc.g.dart';
@JsonSerializable(fieldRename: FieldRename.snake)
class BankAcc {
final String? holderName;
final String? bankName;
final String? number;
final String? routingNumber;
final String? country;
final String? state;
final String? city;
final String? street;
final String? building;
final String? zip;
BankAcc({
required this.holderName,
required this.bankName,
required this.number,
required this.routingNumber,
required this.country,
required this.state,
required this.city,
required this.street,
required this.building,
required this.zip,
});
factory BankAcc.fromJson(Map<String, dynamic> json) {
return _$BankAccFromJson(json);
}
Map<String, dynamic> toJson() => _$BankAccToJson(this);
}

View File

@@ -0,0 +1,46 @@
import 'package:json_annotation/json_annotation.dart';
part 'full_address_model.g.dart';
@JsonSerializable(fieldRename: FieldRename.snake)
class FullAddress {
String? streetNumber;
String? zipCode;
double? latitude;
double? longitude;
String? formattedAddress;
String? street;
String? region;
String? city;
String? country;
FullAddress({
this.streetNumber,
this.zipCode,
this.latitude,
this.longitude,
this.formattedAddress,
this.street,
this.region,
this.city,
this.country,
});
factory FullAddress.fromJson(Map<String, dynamic> json) {
return _$FullAddressFromJson(json);
}
Map<String, dynamic> toJson() => _$FullAddressToJson(this);
static FullAddress fromGoogle(Map<String, dynamic> fullAddress) {
return FullAddress(
streetNumber: fullAddress['street_number'],
zipCode: fullAddress['postal_code'],
latitude: fullAddress['lat'],
longitude: fullAddress['lng'],
formattedAddress: fullAddress['formatted_address'],
street: fullAddress['street'],
region: fullAddress['state'],
city: fullAddress['city'],
country: fullAddress['country'],
);
}
}

View File

@@ -0,0 +1,65 @@
import 'package:json_annotation/json_annotation.dart';
import 'package:krow/core/data/models/staff/full_address_model.dart';
import 'package:krow/core/data/models/staff/workin_area.dart';
import 'package:krow/core/data/models/staff/bank_acc.dart';
import 'package:krow/core/data/models/staff_role.dart';
import 'package:krow/features/profile/certificates/data/models/staff_certificate.dart';
part 'staff.g.dart';
enum StaffStatus {
active,
deactivated,
pending,
registered,
declined,
}
@JsonSerializable(fieldRename: FieldRename.snake)
class Staff {
final String id;
final String? firstName;
final String? lastName;
final String? middleName;
final String? email;
final String? phone;
final String? address;
final StaffStatus? status;
final FullAddress? fullAddress;
final String? avatar;
final List<WorkingArea>? workingAreas;
final List<BankAcc>? bankAccount;
final List<StaffRole>? roles;
final List<StaffCertificate>? certificates;
final double? averageRating;
Staff({
required this.id,
required this.firstName,
required this.lastName,
required this.middleName,
required this.email,
required this.phone,
required this.address,
required this.status,
required this.workingAreas,
required this.fullAddress,
required this.bankAccount,
required this.roles,
required this.certificates,
this.averageRating,
this.avatar,
});
factory Staff.fromJson(Map<String, dynamic> json) => _$StaffFromJson(json);
Map<String, dynamic> toJson() => _$StaffToJson(this);
static var staffStatusEnumMap = {
StaffStatus.active: 'active',
StaffStatus.deactivated: 'deactivated',
StaffStatus.pending: 'pending',
StaffStatus.registered: 'registered',
StaffStatus.declined: 'declined',
};
}

View File

@@ -0,0 +1,21 @@
class WorkingArea{
final String id;
final String address;
WorkingArea({required this.id, required this.address});
factory WorkingArea.fromJson(Map<String, dynamic> json) {
return WorkingArea(
id: json['id'],
address: json['address']
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'address': address
};
}
}

View File

@@ -0,0 +1,93 @@
import 'package:json_annotation/json_annotation.dart';
import 'package:krow/core/data/enums/staff_skill_enums.dart';
import 'package:krow/core/data/models/skill.dart';
import 'package:krow/core/data/models/staff_role_kit.dart';
part 'staff_role.g.dart';
@JsonSerializable(fieldRename: FieldRename.snake)
class StaffRole {
final String? id;
final Skill? skill;
final StaffSkillLevel? level;
final int? experience;
final StaffSkillStatus? status;
final String? photo;
final List<StaffRoleKit>? confirmedUniforms;
final List<StaffRoleKit>? confirmedEquipments;
StaffRole({
this.id,
this.skill,
this.level,
this.experience,
this.status,
this.photo,
this.confirmedUniforms,
this.confirmedEquipments,
});
factory StaffRole.empty() {
return StaffRole(
id: '',
);
}
factory StaffRole.fromJson(Map<String, dynamic> json) =>
_$StaffRoleFromJson(json);
Map<String, dynamic> toJson() => _$StaffRoleToJson(this);
StaffRole copyWith({
String? id,
Skill? skill,
StaffSkillLevel? level,
int? experience,
StaffSkillStatus? status,
String? photo,
List<StaffRoleKit>? confirmedUniforms,
List<StaffRoleKit>? confirmedEquipments,
}) {
return StaffRole(
id: id ?? this.id,
skill: skill ?? this.skill,
level: level ?? this.level,
experience: experience ?? this.experience,
status: status ?? this.status,
photo: photo ?? this.photo,
confirmedUniforms: confirmedUniforms ?? this.confirmedUniforms,
confirmedEquipments: confirmedEquipments ?? this.confirmedEquipments,
);
}
@override
String toString() {
return 'StaffRole{id: $id, skill: $skill, level: $level, experience: $experience, status: $status, photo: $photo, confirmedUniforms: $confirmedUniforms, confirmedEquipments: $confirmedEquipments}';
}
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is StaffRole &&
runtimeType == other.runtimeType &&
id == other.id &&
skill == other.skill &&
level == other.level &&
experience == other.experience &&
status == other.status &&
photo == other.photo &&
confirmedUniforms == other.confirmedUniforms &&
confirmedEquipments == other.confirmedEquipments;
@override
int get hashCode =>
id.hashCode ^
skill.hashCode ^
level.hashCode ^
experience.hashCode ^
status.hashCode ^
photo.hashCode ^
confirmedUniforms.hashCode ^
confirmedEquipments.hashCode;
}

View File

@@ -0,0 +1,23 @@
import 'package:json_annotation/json_annotation.dart';
part 'staff_role_kit.g.dart';
@JsonSerializable(fieldRename: FieldRename.snake)
class StaffRoleKit{
String id;
String skillKitId;
String? photo;
StaffRoleKit({
required this.id,
required this.skillKitId,
this.photo,
});
factory StaffRoleKit.fromJson(Map<String, dynamic> json) {
return _$StaffRoleKitFromJson(json);
}
Map<String, dynamic> toJson() => _$StaffRoleKitToJson(this);
}

View File

@@ -0,0 +1,4 @@
abstract class ContactsData {
static const supportEmail = 'orders@legendaryeventstaff.com';
static const supportPhone = '+1 (408) 315-5343';
}

View File

@@ -0,0 +1,4 @@
abstract class EmailValidationConstants {
static const storedEmailKey = 'user_pre_validation_email';
static const oobCodeKey = 'oobCode';
}

View File

@@ -0,0 +1,32 @@
import 'package:flutter/material.dart';
mixin SizeTransitionMixin {
Widget handleSizeTransition(Widget child, Animation<double> animation) {
return SizeTransition(
sizeFactor: Tween<double>(begin: 0, end: 1).animate(
CurvedAnimation(parent: animation, curve: const Interval(0, 0.3)),
),
axisAlignment: 1,
child: DualTransitionBuilder(
animation: animation,
forwardBuilder: (context, animation, child) {
return FadeTransition(
opacity: Tween<double>(begin: 0, end: 1).animate(
CurvedAnimation(parent: animation, curve: const Interval(0.7, 1)),
),
child: child,
);
},
reverseBuilder: (context, animation, child) {
return FadeTransition(
opacity: Tween<double>(begin: 1, end: 0).animate(
CurvedAnimation(parent: animation, curve: const Interval(0, 0.3)),
),
child: child,
);
},
child: child,
),
);
}
}

View File

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

View File

@@ -0,0 +1,134 @@
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,
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 bodySmallRegBlackGrey = TextStyle(
fontFamily: FontFamily.poppins,
fontWeight: FontWeight.w400,
// Regular
fontSize: 12,
letterSpacing: -0.5,
height: 1.3,
color: AppColors.blackGray);
static const TextStyle bodySmallMed = TextStyle(
fontFamily: FontFamily.poppins,
fontWeight: FontWeight.w500,
// Medium
fontSize: 12,
letterSpacing: -0.5,
color: AppColors.blackBlack);
static const TextStyle bodyTinyReg = TextStyle(
fontFamily: FontFamily.poppins,
fontWeight: FontWeight.w400,
// Regular
fontSize: 10,
height: 1.2,
color: AppColors.blackBlack);
static const TextStyle bodyTinyMed = TextStyle(
fontFamily: FontFamily.poppins,
fontWeight: FontWeight.w500,
// Medium
fontSize: 10,
height: 1.2,
color: AppColors.blackBlack);
static const TextStyle badgeRegular = TextStyle(
fontFamily: FontFamily.poppins,
fontWeight: FontWeight.w400, // Regular
fontSize: 12,
color: AppColors.blackBlack);
static const TextStyle captionReg = TextStyle(
fontFamily: FontFamily.poppins,
fontWeight: FontWeight.w400, // Regular
fontSize: 10,
color: AppColors.blackBlack);
static const TextStyle captionBold = TextStyle(
fontFamily: FontFamily.poppins,
fontWeight: FontWeight.w600, // SemiBold
fontSize: 10,
color: AppColors.blackBlack);
}

View File

@@ -0,0 +1,63 @@
import 'package:flutter/material.dart';
abstract class AppColors {
static const Color bgColorLight = Color(0xFFDDE6DF);
static const Color bgColorDark = Color(0xFF172D38);
static const Color blackBlack = Color(0xFF1A1A1A);
static const Color blackGray = Color(0xFF737373);
static const Color blackCaptionText = Color(0xFFA8A8A8);
static const Color blackCaptionGreen = Color(0xFF84A48B);
static const Color blackDarkBgBody = Color(0xFF82939B);
static const Color primaryYellow = Color(0xFFFFF3A8);
static const Color primaryBlue = Color(0xFF002AE8);
static const Color primaryMint = Color(0xFFDFEDE3);
static const Color grayWhite = Color(0xFFFFFFFF);
static const Color grayStroke = Color(0xFFA8BDAD);
static const Color grayDisable = Color(0xFFC2C9C3);
static const Color grayPrimaryFrame = Color(0xFFF9FAF9);
static const Color graySecondaryFrame = Color(0xFFF0F4F1);
static const Color grayTintStroke = Color(0xFFE1E8E2);
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(0xFFE7F8EF);
static const Color tintRed = Color(0xFFFEF2F2);
static const Color tintYellow = Color(0xFFFEF7E0);
static const Color tintBlue = Color(0xFFF0F3FF);
static const Color tintOrange = Color(0xFFFAEBE3);
static const Color tintDarkGreen = Color(0xFFC8EFDB);
static const Color tintDarkBlue = Color(0xFF7A88BE);
static const Color tintGray = Color(0xFFEBEBEB);
static const Color tintDarkRed = Color(0xFFFDB9B9);
static const Color navBarDisabled = Color(0xFF5C7581);
static const Color navBarActive = Color(0xFFFFFFFF);
static const Color darkBgPrimaryFrame = Color(0xFF3D5662);
static const Color darkBgBgElements = Color(0xFF1B3441);
static const Color darkBgActiveButtonState = Color(0xFF25495A);
static const Color darkBgStroke = Color(0xFF4E6E7E);
static const Color darkBgInactive = Color(0xFF7A9AA9);
static const Color darkBgActiveAccentButton = Color(0xFFEEE39F);
static const Color textPrimaryInverted = Color(0xFFAAC0CB);
}
class KWTheme {
static ThemeData get lightTheme {
return ThemeData(
useMaterial3: true,
scaffoldBackgroundColor: AppColors.bgColorLight,
progressIndicatorTheme: const ProgressIndicatorThemeData(
color: AppColors.bgColorDark,
),
fontFamily: 'Poppins',
//unused
appBarTheme: const AppBarTheme(
backgroundColor: AppColors.bgColorLight,
),
colorScheme: ColorScheme.fromSwatch().copyWith(
secondary: AppColors.statusSuccess,
),
);
}
}

View File

@@ -0,0 +1,25 @@
import 'package:flutter/material.dart';
import 'package:krow/core/presentation/gen/assets.gen.dart';
class ContactIconButton extends StatelessWidget {
const ContactIconButton({
super.key,
required this.icon,
required this.onTap,
});
final SvgGenImage icon;
final void Function() onTap;
@override
Widget build(BuildContext context) {
return Material(
type: MaterialType.transparency,
child: InkWell(
borderRadius: BorderRadius.circular(24.0),
onTap: onTap,
child: icon.svg(),
),
);
}
}

View File

@@ -0,0 +1,122 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:krow/core/application/common/date_time_extension.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 KwTimeSlotInput extends StatefulWidget {
const KwTimeSlotInput({
super.key,
required this.label,
required this.onChange,
required this.initialValue,
this.editable = true,
});
static final _timeFormat = DateFormat('h:mma', 'en');
final String label;
final Function(DateTime value) onChange;
final DateTime initialValue;
final bool editable;
@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: () async {
if(!widget.editable) return;
await showModalBottomSheet<void>(
context: context,
isScrollControlled: false,
builder: (context) {
return Container(
alignment: Alignment.topCenter,
height: 216 + 48, //_kPickerHeight + 24top+24bot
child: CupertinoDatePicker(
mode: CupertinoDatePickerMode.time,
initialDateTime: _currentValue.toTenthsMinute(),
minuteInterval: 10,
itemExtent: 36,
onDateTimeChanged: (DateTime value) {
setState(() => _currentValue = value);
},
),
);
},
);
widget.onChange.call(_currentValue);
},
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,
),
if(widget.editable)
Assets.images.icons.caretDown.svg(
height: 16,
width: 16,
colorFilter: const ColorFilter.mode(
AppColors.blackBlack,
BlendMode.srcIn,
),
)
],
),
),
),
),
),
],
);
}
}

View File

@@ -0,0 +1,187 @@
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 = 66,
this.imagePath,
this.imageUrl,
this.imageQuality = 80,
this.showError = false,
});
final double diameter;
final String? imagePath;
final String? imageUrl;
final int imageQuality;
final Function(String) onChange;
final bool showError;
@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);
}
}
@Deprecated('For now, this method is put under triage')
void _showImageSourceDialog() {
showModalBottomSheet(
context: context,
builder: (BuildContext context) {
return SafeArea(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
ListTile(
leading: const Icon(Icons.photo_library),
title: const Text('Choose from Gallery'),
onTap: () {
Navigator.pop(context);
_pickImage(ImageSource.gallery);
},
),
ListTile(
leading: const Icon(Icons.camera_alt),
title: const Text('Take a Photo'),
onTap: () {
Navigator.pop(context);
_pickImage(ImageSource.camera);
},
),
],
),
);
},
);
}
@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 && _imagePath!.isNotEmpty) {
avatar = Image.file(
File(_imagePath!),
fit: BoxFit.cover,
frameBuilder: (_, child, frame, __) {
return frame != null ? child : const KwAnimatedImagePlaceholder();
},
);
} else if (_imageUrl != null && _imageUrl!.isNotEmpty) {
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,
),
);
}
final iconSize = widget.diameter / 4;
return GestureDetector(
onTap: _pickImage,
child: Stack(
clipBehavior: Clip.none,
children: [
ClipOval(
child: AnimatedContainer(
duration: Durations.medium4,
padding: const EdgeInsets.all(2),
decoration: BoxDecoration(
color: widget.showError && !isImageAvailable
? AppColors.statusError
: Colors.transparent,
),
child: ClipOval(
child: SizedBox(
height: widget.diameter,
width: widget.diameter,
child: avatar,
),
),
),
),
PositionedDirectional(
bottom: 1,
end: -5,
child: Container(
height: iconSize + 10,
width: iconSize + 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: iconSize, height: iconSize)
: Assets.images.icons.add.svg(
width: iconSize,
height: iconSize,
fit: BoxFit.scaleDown,
colorFilter: const ColorFilter.mode(
AppColors.grayWhite,
BlendMode.srcIn,
),
),
),
),
],
),
);
}
}

View File

@@ -0,0 +1,32 @@
import 'package:flutter/cupertino.dart';
class RestartWidget extends StatefulWidget {
final Widget child;
const RestartWidget({Key? key, required this.child}) : super(key: key);
static restartApp(BuildContext context) {
context.findAncestorStateOfType<_RestartWidgetState>()?.restartApp();
}
@override
State createState() => _RestartWidgetState();
}
class _RestartWidgetState extends State<RestartWidget> {
Key key = UniqueKey();
void restartApp() {
setState(() {
key = UniqueKey();
});
}
@override
Widget build(BuildContext context) {
return KeyedSubtree(
key: key,
child: widget.child,
);
}
}

View File

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

View File

@@ -0,0 +1,85 @@
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 ShiftPaymentStepWidget extends StatelessWidget {
final int currentIndex;
const ShiftPaymentStepWidget({required this.currentIndex, super.key});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(top: 12, left: 20, right: 20),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
_buildPaymentStep('Payment sent', 0, currentIndex),
_divider(),
_buildPaymentStep('Pending Payment', 1, currentIndex),
_divider(),
_buildPaymentStep('Payment received', 2, currentIndex),
],
),
);
}
Widget _buildPaymentStep(String title, int index, int currentIndex) {
return Container(
width: 60,
margin: const EdgeInsets.symmetric(horizontal: 10),
child: Column(
children: [
Container(
height: 28,
width: 28,
decoration: BoxDecoration(
color: index == currentIndex
? AppColors.blackBlack
: AppColors.grayWhite,
border: Border.all(
color: index == currentIndex
? Colors.transparent
: AppColors.grayTintStroke,
),
shape: BoxShape.circle,
),
child: Center(
child: index < currentIndex
? Assets.images.icons.check.svg()
: Text(
(index + 1).toString(),
style: index == currentIndex
? AppTextStyles.bodyMediumSmb
.copyWith(color: AppColors.grayWhite)
: AppTextStyles.bodyMediumReg
.copyWith(color: AppColors.grayDisable),
),
),
),
SizedBox(
height: 24,
child: Text(
title,
textAlign: TextAlign.center,
style: index <= currentIndex
? AppTextStyles.bodyTinyMed
: AppTextStyles.bodyTinyReg
.copyWith(color: AppColors.grayDisable),
),
),
],
),
);
}
Widget _divider() {
return Expanded(
child: Container(
height: 1,
color: AppColors.grayTintStroke,
),
);
}
}

View File

@@ -0,0 +1,73 @@
import 'package:flutter/material.dart';
import 'package:krow/core/presentation/styles/kw_text_styles.dart';
import 'package:krow/core/presentation/styles/theme.dart';
class ShiftTotalTimeSpendWidget extends StatelessWidget {
final DateTime startTime;
final DateTime endTime;
final int totalBreakTime;
const ShiftTotalTimeSpendWidget({
super.key,
required this.startTime,
required this.endTime,
required this.totalBreakTime,
});
@override
Widget build(BuildContext context) {
final totalTimeFormatted = _formatDuration(endTime.difference(startTime));
final breakTimeFormatted =
_formatDuration(Duration(seconds: totalBreakTime));
return Container(
padding: const EdgeInsets.all(12),
margin: const EdgeInsets.only(top: 24, left: 20, right: 20),
decoration: BoxDecoration(
color: AppColors.graySecondaryFrame,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: AppColors.grayStroke),
),
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Total Hours',
style: AppTextStyles.bodyLargeReg
.copyWith(color: AppColors.blackGray, height: 1),
),
Text(
totalTimeFormatted,
style: AppTextStyles.bodyLargeReg.copyWith(height: 1),
),
],
),
Container(
margin: const EdgeInsets.symmetric(vertical: 12),
width: 160,
height: 1,
color: AppColors.grayTintStroke),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Break Hours',
style: AppTextStyles.bodyLargeReg
.copyWith(color: AppColors.blackGray, height: 1),
),
Text(
breakTimeFormatted,
style: AppTextStyles.bodyLargeReg.copyWith(height: 1),
),
],
)
],
),
);
}
String _formatDuration(Duration duration) {
return duration.toString().split('.').first.padLeft(8, '0');
}
}

View File

@@ -0,0 +1,236 @@
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import '../styles/theme.dart';
class KWSlider extends StatefulWidget {
final double min;
final double max;
final double initialValue;
final ValueChanged<double>? onChanged;
const KWSlider({
super.key,
this.min = 1.0,
this.max = 20.0,
this.initialValue = 3.0,
this.onChanged,
});
@override
State<KWSlider> createState() => _KWSliderState();
}
class _KWSliderState extends State<KWSlider> {
late double _value;
@override
void initState() {
super.initState();
_value = widget.initialValue.clamp(widget.min, widget.max);
}
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (context, constraints) {
final double sliderWidth = constraints.maxWidth;
const double sliderHeight = 12;
const primaryColor = AppColors.bgColorDark;
const secondaryColor = AppColors.grayTintStroke;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
KWSliderTooltip(
text: '${_value.toInt()} y',
leftPosition:
((_value - widget.min) / (widget.max - widget.min)) *
sliderWidth,
sliderWidth: sliderWidth,
),
Container(
padding: const EdgeInsets.symmetric(vertical: 15),
height: 60,
child: Stack(
alignment: Alignment.centerLeft,
children: [
Container(
height: sliderHeight,
width: sliderWidth,
decoration: BoxDecoration(
color: secondaryColor,
borderRadius: BorderRadius.circular(10),
),
),
Container(
height: sliderHeight,
width: ((_value - widget.min) / (widget.max - widget.min)) *
sliderWidth,
decoration: BoxDecoration(
color: primaryColor,
borderRadius: BorderRadius.circular(10),
),
),
Positioned(
left: ((_value - widget.min) / (widget.max - widget.min)) *
sliderWidth -
12,
child: SliderHead(
color: primaryColor,
onPanUpdate: (details) {
setState(() {
final newValue = _value +
(details.delta.dx / sliderWidth) *
(widget.max - widget.min);
_value = newValue.clamp(widget.min, widget.max);
widget.onChanged?.call(_value);
});
},
),
),
],
),
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text('${widget.min.toInt()} year'),
Text('${widget.max.toInt()} years'),
],
),
],
);
},
);
}
}
class KWSliderTooltip extends StatelessWidget {
final String text;
final double leftPosition;
final double sliderWidth;
const KWSliderTooltip({
super.key,
required this.text,
required this.leftPosition,
required this.sliderWidth,
});
@override
Widget build(BuildContext context) {
return Container(
alignment: Alignment.centerLeft,
margin: EdgeInsets.only(
left: (leftPosition - 24).clamp(0, sliderWidth - 48),
),
width: 48,
child: CustomPaint(
painter: TooltipPainter(),
child: Center(
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Text(
text,
textAlign: TextAlign.center,
style: const TextStyle(
color: Colors.white,
fontSize: 12,
fontWeight: FontWeight.bold,
),
),
),
),
),
);
}
}
class TooltipPainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()..color = const Color(0xFF001F2D);
final path = Path();
path.addRRect(
RRect.fromRectAndRadius(
const Rect.fromLTWH(0, 0, 48, 30),
const Radius.circular(15),
),
);
const double triangleWidth = 10;
const double triangleHeight = 6;
const double centerX = 24;
path.moveTo(centerX - triangleWidth / 2, 30);
path.quadraticBezierTo(
centerX,
40 + triangleHeight / 3,
centerX + triangleWidth / 2,
30,
);
path.close();
canvas.drawPath(path, paint);
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
return false;
}
}
class SliderHead extends StatelessWidget {
final double size;
final Color color;
final VoidCallback? onPanStart;
final Function(DragUpdateDetails)? onPanUpdate;
const SliderHead({
super.key,
this.size = 24.0,
required this.color,
this.onPanStart,
this.onPanUpdate,
});
@override
Widget build(BuildContext context) {
return GestureDetector(
behavior: HitTestBehavior.opaque,
dragStartBehavior: DragStartBehavior.down,
onPanUpdate: onPanUpdate,
onPanStart: (_) => onPanStart?.call(),
child: Container(
width: size,
height: size,
decoration: BoxDecoration(
color: color,
border: Border.all(
color: Colors.white,
width: 5,
),
shape: BoxShape.circle,
boxShadow: const [
BoxShadow(
color: Color(0x08000000),
offset: Offset(0, 1),
blurRadius: 2,
spreadRadius: 0,
),
BoxShadow(
color: Color(0x08000000),
offset: Offset(0, 4),
blurRadius: 4,
spreadRadius: 0,
),
],
),
),
);
}
}

View File

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

View File

@@ -0,0 +1,124 @@
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/check_box.dart';
class CheckBoxCard extends StatelessWidget {
final bool isChecked;
final CheckBoxStyle? checkBoxStyle;
final String title;
final String? message;
final String? errorMessage;
final VoidCallback onTap;
final bool trailing;
final EdgeInsets padding;
const CheckBoxCard({
super.key,
required this.isChecked,
required this.title,
required this.onTap,
this.checkBoxStyle,
this.message,
this.errorMessage,
this.trailing = false,
this.padding = EdgeInsets.zero,
});
Color get titleColor {
if (isChecked) {
switch (checkBoxStyle ?? CheckBoxStyle.green) {
case CheckBoxStyle.green:
if (message != null) {
return AppColors.blackBlack;
}
return AppColors.statusSuccess;
case CheckBoxStyle.black:
return AppColors.blackBlack;
case CheckBoxStyle.red:
return AppColors.blackBlack;
}
} else {
switch (checkBoxStyle ?? CheckBoxStyle.green) {
case CheckBoxStyle.green:
return AppColors.blackBlack;
case CheckBoxStyle.black:
return AppColors.blackGray;
case CheckBoxStyle.red:
return AppColors.blackBlack;
}
}
}
Color get messageColor {
if (errorMessage != null) {
return AppColors.statusError;
} else if (isChecked) {
return AppColors.statusSuccess;
} else {
return AppColors.blackGray;
}
}
Color get backgroundMessageColor {
if (message == null &&
errorMessage == null &&
isChecked &&
checkBoxStyle == CheckBoxStyle.green) {
return AppColors.tintGreen;
} else {
return AppColors.grayPrimaryFrame;
}
}
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onTap,
child: Container(
constraints: const BoxConstraints(minHeight: 50),
margin: padding,
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
color: backgroundMessageColor,
),
child: Row(
crossAxisAlignment: message != null || errorMessage != null
? CrossAxisAlignment.start
: CrossAxisAlignment.center,
children: [
KWCheckBox(
value: isChecked, style: checkBoxStyle ?? CheckBoxStyle.green),
const Gap(8),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(title,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: AppTextStyles.bodyMediumMed.copyWith(
color: titleColor,
)),
if (message != null || errorMessage != null) const Gap(2),
if (message != null || errorMessage != null)
Text(
errorMessage ?? message!,
style: AppTextStyles.bodyTinyReg
.copyWith(color: messageColor),
),
]),
),
if (trailing) Padding(
padding: const EdgeInsets.only(left:24.0),
child: Assets.images.arrowRight.svg(),
)
],
),
),
);
}
}

View File

@@ -0,0 +1,447 @@
import 'dart:io';
import 'dart:math' as math;
import 'package:flutter/foundation.dart';
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
State<CustomPopupMenu> 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) {
if (kDebugMode) 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 PopScope(
onPopInvokedWithResult: (willPop, result) {
_hideMenu();
},
child: child,
);
}
}
}
enum _MenuLayoutId {
arrow,
downArrow,
content,
}
enum _MenuPosition {
bottomLeft,
bottomCenter,
bottomRight,
topLeft,
topCenter,
topRight,
}
class _MenuLayoutDelegate extends MultiChildLayoutDelegate {
_MenuLayoutDelegate({
required this.anchorSize,
required this.anchorOffset,
required this.verticalMargin,
this.position,
});
final Size anchorSize;
final Offset anchorOffset;
final double verticalMargin;
final PreferredPosition? position;
@override
void performLayout(Size size) {
Size contentSize = Size.zero;
Size arrowSize = Size.zero;
Offset contentOffset = const Offset(0, 0);
Offset arrowOffset = const Offset(0, 0);
double anchorCenterX = anchorOffset.dx + anchorSize.width / 2;
double anchorTopY = anchorOffset.dy;
double anchorBottomY = anchorTopY + anchorSize.height;
_MenuPosition menuPosition = _MenuPosition.bottomCenter;
if (hasChild(_MenuLayoutId.content)) {
contentSize = layoutChild(
_MenuLayoutId.content,
BoxConstraints.loose(size),
);
}
if (hasChild(_MenuLayoutId.arrow)) {
arrowSize = layoutChild(
_MenuLayoutId.arrow,
BoxConstraints.loose(size),
);
}
if (hasChild(_MenuLayoutId.downArrow)) {
layoutChild(
_MenuLayoutId.downArrow,
BoxConstraints.loose(size),
);
}
bool isTop = false;
if (position == null) {
// auto calculate position
isTop = anchorBottomY > size.height / 2;
} else {
isTop = position == PreferredPosition.top;
}
if (anchorCenterX - contentSize.width / 2 < 0) {
menuPosition = isTop ? _MenuPosition.topLeft : _MenuPosition.bottomLeft;
} else if (anchorCenterX + contentSize.width / 2 > size.width) {
menuPosition = isTop ? _MenuPosition.topRight : _MenuPosition.bottomRight;
} else {
menuPosition =
isTop ? _MenuPosition.topCenter : _MenuPosition.bottomCenter;
}
switch (menuPosition) {
case _MenuPosition.bottomCenter:
arrowOffset = Offset(
anchorCenterX - arrowSize.width / 2,
anchorBottomY + verticalMargin,
);
contentOffset = Offset(
anchorCenterX - contentSize.width / 2,
anchorBottomY + verticalMargin + arrowSize.height,
);
break;
case _MenuPosition.bottomLeft:
arrowOffset = Offset(anchorCenterX - arrowSize.width / 2,
anchorBottomY + verticalMargin);
contentOffset = Offset(
0,
anchorBottomY + verticalMargin + arrowSize.height,
);
break;
case _MenuPosition.bottomRight:
arrowOffset = Offset(anchorCenterX - arrowSize.width / 2,
anchorBottomY + verticalMargin);
contentOffset = Offset(
size.width - contentSize.width,
anchorBottomY + verticalMargin + arrowSize.height,
);
break;
case _MenuPosition.topCenter:
arrowOffset = Offset(
anchorCenterX - arrowSize.width / 2,
anchorTopY - verticalMargin - arrowSize.height,
);
contentOffset = Offset(
anchorCenterX - contentSize.width / 2,
anchorTopY - verticalMargin - arrowSize.height - contentSize.height,
);
break;
case _MenuPosition.topLeft:
arrowOffset = Offset(
anchorCenterX - arrowSize.width / 2,
anchorTopY - verticalMargin - arrowSize.height,
);
contentOffset = Offset(
0,
anchorTopY - verticalMargin - arrowSize.height - contentSize.height,
);
break;
case _MenuPosition.topRight:
arrowOffset = Offset(
anchorCenterX - arrowSize.width / 2,
anchorTopY - verticalMargin - arrowSize.height,
);
contentOffset = Offset(
size.width - contentSize.width,
anchorTopY - verticalMargin - arrowSize.height - contentSize.height,
);
break;
}
if (hasChild(_MenuLayoutId.content)) {
positionChild(_MenuLayoutId.content, contentOffset);
}
_menuRect = Rect.fromLTWH(
contentOffset.dx,
contentOffset.dy,
contentSize.width,
contentSize.height,
);
bool isBottom = false;
if (_MenuPosition.values.indexOf(menuPosition) < 3) {
// bottom
isBottom = true;
}
if (hasChild(_MenuLayoutId.arrow)) {
positionChild(
_MenuLayoutId.arrow,
isBottom
? Offset(arrowOffset.dx, arrowOffset.dy + 0.1)
: const Offset(-100, 0),
);
}
if (hasChild(_MenuLayoutId.downArrow)) {
positionChild(
_MenuLayoutId.downArrow,
!isBottom
? Offset(arrowOffset.dx, arrowOffset.dy - 0.1)
: const Offset(-100, 0),
);
}
}
@override
bool shouldRelayout(MultiChildLayoutDelegate oldDelegate) => false;
}
class _ArrowClipper extends CustomClipper<Path> {
@override
Path getClip(Size size) {
Path path = Path();
path.moveTo(0, size.height);
path.lineTo(size.width / 2, size.height / 2);
path.lineTo(size.width, size.height);
return path;
}
@override
bool shouldReclip(CustomClipper<Path> oldClipper) {
return true;
}
}

View File

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

View File

@@ -0,0 +1,197 @@
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/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,
required 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,
required String message,
String? primaryButtonLabel,
String? secondaryButtonLabel,
bool barrierDismissible = true,
void Function(BuildContext dialogContext)? onPrimaryButtonPressed,
void Function(BuildContext dialogContext)? onSecondaryButtonPressed,
Widget? child,
}) async {
return showDialog<R>(
context: context,
barrierDismissible: barrierDismissible,
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: BoxDecoration(
color: AppColors.grayWhite,
borderRadius: BorderRadius.circular(24),
),
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,
),
const Gap(8),
Text(
widget.message,
style: AppTextStyles.bodyMediumReg
.copyWith(color: AppColors.blackGray),
textAlign: TextAlign.center,
),
if (widget.child != null) ...[
const Gap(8),
widget.child!,
],
const Gap(24),
..._buttonGroup(),
],
),
),
),
),
),
);
}
List<Widget> _buttonGroup() {
return [
if (widget.primaryButtonLabel != null)
KwButton.primary(
label: widget.primaryButtonLabel ?? '',
onPressed: () {
if (widget.onPrimaryButtonPressed != null) {
widget.onPrimaryButtonPressed?.call(context);
} else {
Navigator.of(context).pop();
}
},
),
if (widget.primaryButtonLabel != null &&
widget.secondaryButtonLabel != null)
const Gap(8),
if (widget.secondaryButtonLabel != null)
KwButton.outlinedPrimary(
label: widget.secondaryButtonLabel ?? '',
onPressed: () {
if (widget.onSecondaryButtonPressed != null) {
widget.onSecondaryButtonPressed?.call(context);
} else {
Navigator.of(context).pop();
}
},
),
];
}
}

View File

@@ -0,0 +1,100 @@
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 [];//todo remove after implementation
return [
if (showNotification)
Container(
margin: const EdgeInsets.only(right: 16),
height: 48,
width: 48,
color: Colors.transparent,
child: Center(
child: Assets.images.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: Container(
color: Colors.transparent,
height: 40,
width: 40,
child: Center(
child: Assets.images.appBar.appbarLeading.svg(
colorFilter: ColorFilter.mode(
iconColorStyle == AppBarIconColorStyle.normal
? AppColors.blackBlack
: Colors.white,
BlendMode.srcIn,
),
),
),
),
),
)
: const SizedBox.shrink();
});
}
static TextStyle _titleTextStyle(contentColor) {
return AppTextStyles.headingH2.copyWith(
color: contentColor ?? AppColors.blackBlack,
);
}
}

View File

@@ -0,0 +1,299 @@
import 'dart:async';
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? textColors;
final bool isOutlined;
final VoidCallback onPressed;
final KwButtonFit? fit;
final double height;
const KwButton._({
required this.onPressed,
required this.color,
required this.isOutlined,
required this.pressedColor,
required this.disabledColor,
this.disabled = false,
this.label,
this.leftIcon,
this.rightIcon,
this.textColors,
this.height = 52,
this.fit = KwButtonFit.expanded,
}) : 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,
}) : color = AppColors.bgColorDark,
pressedColor = AppColors.darkBgActiveButtonState,
disabledColor = AppColors.grayDisable,
textColors = Colors.white,
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,
}) : color = Colors.white,
pressedColor = AppColors.graySecondaryFrame,
disabledColor = Colors.white,
textColors = disabled ? AppColors.grayDisable : AppColors.blackBlack,
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,
}) : color = AppColors.primaryYellow,
pressedColor = AppColors.darkBgActiveAccentButton,
disabledColor = AppColors.navBarDisabled,
textColors = disabled ? AppColors.darkBgInactive : AppColors.blackBlack,
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,
}) : color = AppColors.bgColorDark,
pressedColor = AppColors.darkBgActiveButtonState,
disabledColor = AppColors.grayDisable,
isOutlined = true,
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,
}) : color = AppColors.primaryYellow,
pressedColor = AppColors.darkBgActiveAccentButton,
disabledColor = AppColors.navBarDisabled,
isOutlined = true,
textColors = null;
KwButton copyWith({
String? label,
SvgGenImage? icon,
bool? disabled,
Color? color,
Color? pressedColor,
Color? disabledColor,
Color? textColors,
bool? isOutlined,
VoidCallback? onPressed,
KwButtonFit? fit,
double? height,
}) {
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,
isOutlined: isOutlined ?? this.isOutlined,
onPressed: onPressed ?? this.onPressed,
fit: fit ?? this.fit,
height: height ?? this.height,
);
}
@override
State<KwButton> createState() => _KwButtonState();
}
class _KwButtonState extends State<KwButton> {
bool pressed = false;
Timer? _tapCancelTimer;
@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 ? Colors.transparent : widget.pressedColor,
splashColor:
widget.isOutlined ? 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: 16,
width: 16,
colorFilter: 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: 16,
width: 16,
colorFilter: 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) {
_tapCancelTimer?.cancel();
_tapCancelTimer = Timer(
const Duration(milliseconds: 50),
_onTapCancel,
);
widget.onPressed();
}
Border? _getBorder() {
return widget.isOutlined
? Border.all(
color: widget.disabled
? widget.disabledColor
: pressed
? widget.pressedColor
: widget.color,
width: 1,
)
: null;
}
Color _getColor() {
return widget.isOutlined
? Colors.transparent
: widget.disabled
? widget.disabledColor
: widget.color;
}
Color _getTextColor() {
return widget.textColors ??
(pressed
? widget.pressedColor
: widget.disabled
? widget.disabledColor
: widget.color);
}
@override
void dispose() {
_tapCancelTimer?.cancel();
super.dispose();
}
}

View File

@@ -0,0 +1,212 @@
import 'package:calendar_date_picker2/calendar_date_picker2.dart';
import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:krow/core/application/common/date_time_extension.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 KwCalendarWidget extends StatefulWidget {
const KwCalendarWidget({
super.key,
required this.initialDate,
required this.onDateSelected,
this.firstDate,
this.lastDate,
this.currentDate,
});
final DateTime? initialDate;
final DateTime? firstDate;
final DateTime? lastDate;
final DateTime? currentDate;
final void Function(DateTime value) onDateSelected;
@override
State<KwCalendarWidget> createState() => _KwCalendarWidgetState();
}
class _KwCalendarWidgetState extends State<KwCalendarWidget> {
final monthStr = [
'Jan',
'Feb',
'Mar',
'Apr',
'May',
'Jun',
'Jul',
'Aug',
'Sep',
'Oct',
'Nov',
'Dec'
];
final selectedTextStyle = AppTextStyles.bodyMediumSmb.copyWith(
color: AppColors.grayWhite,
);
final dayTextStyle = AppTextStyles.bodyMediumReg.copyWith(
color: AppColors.bgColorDark,
);
@override
Widget build(BuildContext context) {
return CalendarDatePicker2(
value: widget.initialDate == null ? [] : [widget.initialDate],
config: CalendarDatePicker2Config(
firstDate: widget.firstDate,
lastDate: widget.lastDate,
currentDate: widget.currentDate,
hideMonthPickerDividers: false,
modePickersGap: 0,
controlsHeight: 80,
calendarType: CalendarDatePicker2Type.single,
selectedRangeHighlightColor: AppColors.tintGray,
selectedDayTextStyle: selectedTextStyle,
dayTextStyle: dayTextStyle,
selectedMonthTextStyle: selectedTextStyle,
selectedYearTextStyle: selectedTextStyle,
selectedDayHighlightColor: AppColors.bgColorDark,
centerAlignModePicker: true,
monthTextStyle: dayTextStyle,
weekdayLabelBuilder: _dayWeekBuilder,
dayBuilder: _dayBuilder,
monthBuilder: _monthBuilder,
yearBuilder: _yearBuilder,
controlsTextStyle: AppTextStyles.headingH3,
nextMonthIcon: Assets.images.icons.caretRight.svg(width: 24),
lastMonthIcon: Assets.images.icons.caretLeft.svg(width: 24),
customModePickerIcon: Padding(
padding: const EdgeInsets.only(left: 4),
child: Assets.images.icons.caretDown.svg(),
),
// modePickerBuilder: _controlBuilder,
),
onValueChanged: (dates) {
widget.onDateSelected.call(dates.first);
},
);
}
Widget? _monthBuilder({
required int month,
TextStyle? textStyle,
BoxDecoration? decoration,
bool? isSelected,
bool? isDisabled,
bool? isCurrentMonth,
}) {
return Center(
child: Container(
margin: const EdgeInsets.only(top: 16),
height: 52,
decoration: BoxDecoration(
color: isSelected == true ? AppColors.bgColorDark : null,
borderRadius: BorderRadius.circular(23),
border: Border.all(
color: AppColors.grayStroke,
width: isSelected == true ? 0 : 1,
),
),
child: Center(
child: Text(
monthStr[month - 1],
style: isSelected == true
? AppTextStyles.bodyMediumMed
.copyWith(color: AppColors.grayWhite)
: AppTextStyles.bodyMediumReg
.copyWith(color: AppColors.blackGray),
textAlign: TextAlign.center,
),
),
),
);
}
Widget? _yearBuilder({
required int year,
TextStyle? textStyle,
BoxDecoration? decoration,
bool? isSelected,
bool? isDisabled,
bool? isCurrentYear,
}) {
return Container(
margin: const EdgeInsets.only(top: 12),
height: 52,
decoration: BoxDecoration(
color: isSelected == true ? AppColors.bgColorDark : null,
borderRadius: BorderRadius.circular(23),
border: Border.all(
color: AppColors.grayStroke,
width: isSelected == true ? 0 : 1,
),
),
child: Center(
child: Text(
year.toString(),
style: isSelected == true
? AppTextStyles.bodyMediumMed.copyWith(color: AppColors.grayWhite)
: AppTextStyles.bodyMediumReg
.copyWith(color: AppColors.blackGray),
textAlign: TextAlign.center,
),
),
);
}
Widget? _dayBuilder({
required DateTime date,
TextStyle? textStyle,
BoxDecoration? decoration,
bool? isSelected,
bool? isDisabled,
bool? isToday,
}) {
bool past = _isPast(date);
var dayDecoration = BoxDecoration(
color: isSelected == true ? AppColors.bgColorDark : null,
borderRadius: BorderRadius.circular(20),
);
var dayTextStyle = AppTextStyles.bodyMediumReg.copyWith(
color: past ? AppColors.blackCaptionText : AppColors.bgColorDark,
);
return Center(
child: Container(
margin: const EdgeInsets.only(left: 2, right: 2),
alignment: Alignment.center,
decoration: dayDecoration,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
date.day.toString(),
style: isSelected == true ? selectedTextStyle : dayTextStyle,
textAlign: TextAlign.center,
),
const Gap(2),
],
),
),
);
}
bool _isPast(DateTime date) {
return date.isBefore(DateTime.now().tillDay());
}
Widget? _dayWeekBuilder({
required int weekday,
bool? isScrollViewTopHeader,
}) {
return Text(
['S', 'M', 'T', 'W', 'T', 'F', 'S'][weekday],
style: AppTextStyles.bodyMediumSmb.copyWith(
color: AppColors.bgColorDark,
),
textAlign: TextAlign.center,
);
}
}

View File

@@ -0,0 +1,114 @@
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';
import 'package:krow/core/presentation/widgets/ui_kit/kw_button.dart';
import 'package:krow/core/presentation/widgets/ui_kit/kw_calendar_widget.dart';
class KwDatePickerPopup extends StatefulWidget {
const KwDatePickerPopup({
super.key,
this.initialDate,
this.firstDate,
this.lastDate,
this.currentDate,
});
final DateTime? initialDate;
final DateTime? firstDate;
final DateTime? lastDate;
final DateTime? currentDate;
@override
State<KwDatePickerPopup> createState() => _KwDatePickerPopupState();
static Future<DateTime?> show({
required BuildContext context,
DateTime? initDate,
DateTime? firstDate,
DateTime? lastDate,
DateTime? currentDate,
}) async {
return showDialog<DateTime>(
context: context,
builder: (context) => KwDatePickerPopup(
initialDate: initDate,
firstDate: firstDate,
lastDate: lastDate,
currentDate: currentDate,
),
);
}
}
class _KwDatePickerPopupState extends State<KwDatePickerPopup> {
DateTime? selectedDate;
@override
void initState() {
selectedDate = widget.initialDate;
super.initState();
}
@override
Widget build(BuildContext context) {
return Material(
type: MaterialType.transparency,
child: Center(
child: Container(
padding: const EdgeInsets.all(24),
margin: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: AppColors.bgColorLight,
borderRadius: BorderRadius.circular(24),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
const Row(
children: [
Text(
'Select Date from Calendar',
style: AppTextStyles.headingH3,
),
],
),
const Gap(24),
Container(
decoration: BoxDecoration(
color: AppColors.grayPrimaryFrame,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: AppColors.grayTintStroke),
),
child: KwCalendarWidget(
onDateSelected: (date) {
setState(() {
selectedDate = date;
});
},
initialDate: selectedDate ?? widget.initialDate,
),
),
const Gap(24),
KwButton.primary(
disabled: selectedDate == null,
label: 'Pick Date',
onPressed: () {
Navigator.of(context).pop(selectedDate);
},
),
const Gap(8),
KwButton.outlinedPrimary(
label: 'Cancel',
onPressed: () {
Navigator.of(context).pop(widget.initialDate);
},
),
],
),
),
),
);
}
}

View File

@@ -0,0 +1,220 @@
import 'package:flutter/material.dart';
import 'package:gap/gap.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/core/presentation/widgets/ui_kit/kw_date_picker_popup.dart';
class KwDateSelector extends StatefulWidget {
const KwDateSelector({
required this.onSelect,
super.key,
this.title,
this.hintText,
this.helperText,
this.minHeight = standardHeight,
this.suffixIcon,
this.showError = false,
this.enabled = true,
this.readOnly = false,
this.focusNode,
this.textStyle,
this.radius = 12,
this.borderColor,
this.dateFormat,
this.initialValue,
this.firstDate,
this.lastDate,
this.initialDate,
});
static const standardHeight = 48.0;
final void Function(DateTime) onSelect;
final String? title;
final String? hintText;
final String? helperText;
final bool enabled;
final bool readOnly;
final bool showError;
final FocusNode? focusNode;
final double minHeight;
final TextStyle? textStyle;
final Widget? suffixIcon;
final double radius;
final Color? borderColor;
final DateFormat? dateFormat;
final DateTime? initialValue;
final DateTime? firstDate;
final DateTime? lastDate;
final DateTime? initialDate;
@override
State<KwDateSelector> createState() => _KwDateSelectorState();
}
class _KwDateSelectorState extends State<KwDateSelector> {
final _controller = TextEditingController();
late FocusNode _focusNode;
late DateFormat _dateFormat;
@override
initState() {
_focusNode = widget.focusNode ?? FocusNode();
_dateFormat = widget.dateFormat ?? DateFormat('M/d/y');
super.initState();
_focusNode.addListener(() => setState(() {}));
if (widget.initialValue != null) {
_controller.text = _dateFormat.format(widget.initialValue!);
}
}
Color _helperTextColor() {
if (widget.showError) return AppColors.statusError;
if (!widget.enabled) return AppColors.grayDisable;
return AppColors.bgColorDark;
}
Color _borderColor() {
if (!widget.enabled) return AppColors.grayDisable;
if (widget.showError) return AppColors.statusError;
if (_focusNode.hasFocus) return AppColors.bgColorDark;
return widget.borderColor ?? AppColors.grayStroke;
}
Future<void> _handleOnTap() async {
final today = DateTime.now();
final date = await KwDatePickerPopup.show(
context: context,
firstDate:
widget.firstDate ?? today.subtract(const Duration(days: 3650)),
lastDate: widget.lastDate ?? today.add(const Duration(days: 10)),
initDate: widget.initialDate,
currentDate: today,
);
if (date == null) return;
_controller.text = _dateFormat.format(date);
widget.onSelect(date);
}
@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),
Stack(
children: [
Container(
alignment: Alignment.topCenter,
constraints: BoxConstraints(
minHeight: widget.minHeight,
),
decoration: BoxDecoration(
color: Colors.transparent,
borderRadius: BorderRadius.all(
Radius.circular(
widget.minHeight > KwDateSelector.standardHeight
? widget.radius
: widget.minHeight / 2,
),
),
border: Border.all(
color: _borderColor(),
),
),
child: Row(
children: [
Expanded(
child: TextFormField(
controller: _controller,
focusNode: _focusNode,
enabled: widget.enabled && !widget.readOnly,
onTap: _handleOnTap,
onTapOutside: (_) => _focusNode.unfocus(),
style: widget.textStyle ??
AppTextStyles.bodyMediumReg.copyWith(
color: !widget.enabled
? AppColors.grayDisable
: null,
),
cursorColor: Colors.transparent,
decoration: InputDecoration(
fillColor: Colors.transparent,
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 8,
),
hintText: widget.hintText,
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.minHeight > KwDateSelector.standardHeight)
Positioned(
bottom: 12,
right: 12,
child: Assets.images.icons.textFieldNotches.svg(
height: 12,
width: 12,
),
),
],
),
if (widget.helperText != null &&
(widget.helperText?.isNotEmpty ?? false)) ...[
const SizedBox(height: 4),
Row(
children: [
const Gap(16),
Text(
widget.helperText!,
style: AppTextStyles.bodyTinyReg.copyWith(
height: 1,
color: _helperTextColor(),
),
),
],
)
]
],
),
);
}
}

View File

@@ -0,0 +1,159 @@
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/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;
final bool showError;
final String? helperText;
const KwDropdown({
super.key,
required this.hintText,
this.horizontalPadding = 0,
required this.items,
required this.onSelected,
this.backgroundColor,
this.borderColor = AppColors.grayTintStroke,
this.title,
this.selectedItem,
this.showError = false,
this.helperText,
});
@override
State<KwDropdown<R>> createState() => _KwDropdownState<R>();
}
class _KwDropdownState<R> extends State<KwDropdown<R>> {
KwDropDownItem<R>? _selectedItem;
@override
void initState() {
_selectedItem = widget.selectedItem;
super.initState();
}
Color _helperTextColor() {
if (widget.showError) return AppColors.statusError;
return AppColors.bgColorDark;
}
@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,
),
),
),
KwPopupMenu(
horizontalPadding: widget.horizontalPadding,
fit: KwPopupMenuFit.expand,
customButtonBuilder: _buildMenuButton,
menuItems: [
for (final item in widget.items)
KwPopupMenuItem(
title: item.title,
icon: item.icon,
onTap: () {
setState(() => _selectedItem = item);
widget.onSelected(item.data);
},
),
],
),
if (widget.helperText?.isNotEmpty == true) ...[
const SizedBox(height: 4),
Row(
children: [
const Gap(16),
Text(
widget.helperText!,
style: AppTextStyles.bodyTinyReg.copyWith(
height: 1,
color: _helperTextColor(),
),
),
],
),
],
],
);
}
Color _borderColor(bool isOpened) {
if (isOpened) return AppColors.bgColorDark;
if (widget.showError) return AppColors.statusError;
return widget.borderColor;
}
Widget _buildMenuButton(BuildContext context, 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: _borderColor(isOpened),
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.darkBgInactive,
BlendMode.srcIn,
),
),
)
],
),
);
}
}
class KwDropDownItem<R> {
final String title;
final R data;
final Widget? icon;
const KwDropDownItem({required this.data, required this.title, this.icon});
}

View File

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

View File

@@ -0,0 +1,251 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:krow/core/presentation/gen/assets.gen.dart';
import 'package:krow/core/presentation/mixins/size_transition_mixin.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 TextCapitalization textCapitalization;
final String? initialValue;
const KwTextInput({
super.key,
this.initialValue,
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.textCapitalization = TextCapitalization.none,
});
static const standardHeight = 48.0;
@override
State<KwTextInput> createState() => _KwTextInputState();
}
class _KwTextInputState extends State<KwTextInput> with SizeTransitionMixin {
late FocusNode _focusNode;
@override
initState() {
_focusNode = widget.focusNode ?? FocusNode();
_focusNode.addListener(() {
setState(() {});
});
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: [
AnimatedContainer(
duration: Durations.medium4,
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,
textCapitalization: widget.textCapitalization,
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,
),
),
],
),
),
AnimatedSwitcher(
duration: Durations.medium4,
transitionBuilder: handleSizeTransition,
child: widget.helperText?.isNotEmpty ?? false
? Padding(
padding: const EdgeInsetsDirectional.fromSTEB(16, 4, 4, 0),
child: Text(
widget.helperText!,
style: AppTextStyles.bodyTinyReg.copyWith(
height: 1,
color: _helperTextColor(),
),
),
)
: const SizedBox(),
),
],
),
);
}
}

View File

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

View File

@@ -0,0 +1,119 @@
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,
});
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;
@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,
0.0,
),
duration: Durations.short4,
child: Container(
height: height,
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: Center(
child: AnimatedDefaultTextStyle(
duration: Durations.short4,
style: index == selectedIndex
? AppTextStyles.bodyMediumMed
.copyWith(color: AppColors.grayWhite)
: AppTextStyles.bodyMediumReg
.copyWith(color: AppColors.blackGray),
child: Text(items[index]),
),
),
),
),
],
),
],
),
);
},
)
],
);
}
}

View File

@@ -0,0 +1,178 @@
import 'package:easy_localization/easy_localization.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 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;
final String? initialValue;
const KwPhoneInput({
super.key,
required this.title,
this.initialValue,
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: [
_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_u_number'.tr(),
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',
),
),
/// dont show arrow
// const Gap(6),
// const Icon(
// Icons.keyboard_arrow_down_rounded,
// color: AppColors.blackGray,
// opticalSize: 16,
// ),
const Gap(12),
SizedBox(
height: 48,
child: VerticalDivider(
width: 1,
color: _borderColor(),
),
),
],
),
);
}
_buildError() {
return AnimatedSize(
duration: const Duration(milliseconds: 200),
alignment: Alignment.bottomCenter,
child: Container(
height: widget.error == null ? 0 : 24,
clipBehavior: Clip.none,
padding: const EdgeInsets.only(left: 16),
alignment: Alignment.bottomLeft,
child: Text(
widget.error ?? '',
style: AppTextStyles.bodyTinyMed.copyWith(
color: AppColors.statusError,
),
),
),
);
}
}

View File

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

View File

@@ -0,0 +1,129 @@
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);
WidgetsBinding.instance.addPostFrameCallback(
(_) {
if (oldWidget.items != widget.items) {
dropdownController.showMenu();
} else {
dropdownController.setState();
}
},
);
}
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
KwPopupMenu(
controller: dropdownController,
horizontalPadding: widget.horizontalPadding,
fit: KwPopupMenuFit.expand,
customButtonBuilder: (context, isOpened) {
return _buildMenuButton(isOpened);
},
menuItems: widget.items
.map((item) => KwPopupMenuItem(
title: widget.itemToStringBuilder(item),
onTap: () {
selectedItem = item;
_textController.text = widget.itemToStringBuilder(item);
dropdownController.hideMenu();
widget.onSelected(item);
}))
.toList()),
],
);
}
Widget _buildMenuButton(bool isOpened) {
return KwTextInput(
title: widget.title,
hintText: widget.hintText,
controller: _textController,
focusNode: _focusNode,
);
}
}

View File

@@ -0,0 +1,288 @@
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) {
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,
);
}
}

View File

@@ -0,0 +1,218 @@
import 'dart:io';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:easy_localization/easy_localization.dart';
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/dialogs/image_preview_dialog.dart';
import 'package:krow/core/presentation/widgets/ui_kit/kw_button.dart';
import 'package:krow/core/presentation/widgets/ui_kit/kw_popup_menu.dart';
class UploadImageCard extends StatelessWidget {
final String title;
final String? message;
final VoidCallback onTap;
final EdgeInsets padding;
final Color? statusColor;
final String? imageUrl;
final bool inUploading;
final bool hasError;
final VoidCallback? onSelectImage;
final VoidCallback? onDeleteTap;
final Widget child;
final String? localImagePath;
const UploadImageCard({
super.key,
required this.title,
required this.onTap,
this.onSelectImage,
this.onDeleteTap,
this.message,
this.padding = EdgeInsets.zero,
this.statusColor,
this.imageUrl,
this.inUploading = false,
this.hasError = false,
this.child = const SizedBox.shrink(),
this.localImagePath,
});
@override
Widget build(BuildContext context) {
return Container(
constraints: const BoxConstraints(minHeight: 48),
margin: padding,
padding: const EdgeInsets.all(4),
decoration: KwBoxDecorations.primaryLight8,
child: Column(
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
_buildImageSection(),
const Gap(8),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: AppTextStyles.bodyMediumMed.copyWith(
color: AppColors.blackBlack,
),
),
if (message != null) const Gap(4),
if (message != null)
Text(
message!,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: AppTextStyles.bodyTinyReg.copyWith(
color: statusColor ?? AppColors.blackGray,
),
),
],
),
),
const Gap(16),
_actionSection(context),
],
),
child,
],
),
);
}
Widget _buildImageSection() {
if (inUploading) {
return _buildImageLoadingSpinner();
}
if (imageUrl != null || localImagePath != null) {
return Container(
width: 48,
height: 48,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(6),
border: Border.all(
color: statusColor ?? AppColors.tintDarkGreen,
width: 1,
),
image: DecorationImage(
image: localImagePath != null
? FileImage(File(localImagePath!))
: CachedNetworkImageProvider(imageUrl ?? ''),
fit: BoxFit.cover,
),
),
);
}
return (hasError
? Assets.images.icons.imageErrorPlaceholder
: Assets.images.icons.imagePlaceholder)
.svg();
}
Container _buildImageLoadingSpinner() {
return Container(
width: 48,
height: 48,
decoration: KwBoxDecorations.primaryLight8,
child: const Center(
child: SizedBox(
height: 24,
width: 24,
child: CircularProgressIndicator(
color: AppColors.statusSuccess,
),
),
),
);
}
Widget _actionSection(context) {
if (imageUrl != null || localImagePath != null) {
return _buildImageMenu(context);
}
return _buildButton(
'upload'.tr(),
onSelectImage,
Assets.images.userProfile.menu.gallery,
);
}
Container _buildButton(
String? title,
VoidCallback? onTap,
SvgGenImage icon, {
KwButtonFit? fit,
}) {
return Container(
margin: const EdgeInsets.only(right: 4),
alignment: Alignment.center,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16),
border: Border.all(color: AppColors.grayTintStroke, width: 1),
),
child: KwButton.secondary(
label: title,
fit: fit,
onPressed: () {
onTap?.call();
},
height: 30,
leftIcon: icon,
),
);
}
Widget _buildImageMenu(context) {
return Row(
children: [
_buildButton(
'preview'.tr(),
() {
ImagePreviewDialog.show(
context,
title,
localImagePath ?? imageUrl ?? '',
);
},
Assets.images.icons.eye,
),
KwPopupMenu(
horizontalMargin: 24,
menuItems: [
KwPopupMenuItem(
title: 're-upload'.tr(),
icon: Assets.images.icons.downloadCloud.svg(),
onTap: () {
onSelectImage?.call();
},
),
KwPopupMenuItem(
title: 'delete'.tr(),
textStyle: AppTextStyles.bodyMediumReg.copyWith(
color: AppColors.statusError,
),
icon: Assets.images.icons.trash.svg(),
onTap: () {
onDeleteTap?.call();
},
),
],
),
],
);
}
}

View File

@@ -0,0 +1,126 @@
import 'dart:io';
import 'package:firebase_remote_config/firebase_remote_config.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:injectable/injectable.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:url_launcher/url_launcher_string.dart';
@singleton
class AppUpdateService {
Future<void> checkForUpdate(BuildContext context) async {
final remoteConfig = FirebaseRemoteConfig.instance;
await remoteConfig.setConfigSettings(RemoteConfigSettings(
fetchTimeout: const Duration(seconds: 10),
minimumFetchInterval: const Duration(seconds: 1),
));
await remoteConfig.fetchAndActivate();
final minBuildNumber = remoteConfig.getInt(Platform.isIOS?'min_build_number_staff_ios':'min_build_number_staff_android');
final canSkip = remoteConfig.getBool('can_skip_staff');
final canIgnore = remoteConfig.getBool('can_ignore_staff');
final message = remoteConfig.getString('message_staff');
final packageInfo = await PackageInfo.fromPlatform();
final currentBuildNumber = int.parse(packageInfo.buildNumber);
final prefs = await SharedPreferences.getInstance();
final skippedVersion = prefs.getInt('skipped_version') ?? 0;
if (minBuildNumber > currentBuildNumber &&
minBuildNumber > skippedVersion) {
await _showUpdateDialog(context, message, canSkip, canIgnore, minBuildNumber);
}
}
_showUpdateDialog(
BuildContext context, String message, bool canSkip, bool canIgnore, int minBuildNumber) {
return showDialog(
context: context,
barrierDismissible: canIgnore,
builder: (BuildContext context) {
if (Theme.of(context).platform == TargetPlatform.iOS) {
return WillPopScope(
onWillPop: () async => canIgnore,
child: CupertinoAlertDialog(
title: const Text('Update Available'),
content: Text(
message),
actions: <Widget>[
if (canSkip)
CupertinoDialogAction(
child: const Text('Skip this version'),
onPressed: () async {
final prefs = await SharedPreferences.getInstance();
await prefs.setInt('skipped_version', minBuildNumber);
Navigator.of(context).pop();
},
),
CupertinoDialogAction(
onPressed:
canIgnore ? () => Navigator.of(context).pop() : null,
child: const Text('Maybe later'),
),
CupertinoDialogAction(
child: const Text('Update'),
onPressed: () async {
var url = dotenv.env['IOS_STORE_URL'] ??
'';
if (await canLaunchUrlString(url)) {
await launchUrlString(url);
} else {
throw 'Could not launch $url';
}
},
),
],
),
);
} else {
return WillPopScope(
onWillPop: () async => canIgnore,
child: AlertDialog(
title: const Text('Update Available'),
content: Text(
message),
actions: <Widget>[
if (canSkip)
TextButton(
child: const Text('Skip this version'),
onPressed: () async {
final prefs = await SharedPreferences.getInstance();
await prefs.setInt('skipped_version', minBuildNumber);
Navigator.of(context).pop();
},
),
TextButton(
onPressed:
canIgnore ? () => Navigator.of(context).pop() : null,
child: const Text('Maybe later'),
),
TextButton(
child: const Text('Update'),
onPressed: () async {
var url = dotenv.env['ANDROID_STORE_URL'] ??
'';
if (await canLaunchUrlString(url)) {
await launchUrlString(url);
} else {
throw 'Could not launch $url';
}
},
),
],
),
);
}
},
);
}
}

View File

@@ -0,0 +1,120 @@
import 'dart:developer';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:injectable/injectable.dart';
import 'package:krow/core/application/clients/api/api_client.dart';
import 'package:krow/core/application/di/injectable.dart';
import 'package:krow/core/data/models/staff/staff.dart';
import 'package:krow/core/data/static/email_validation_constants.dart';
import 'package:krow/core/sevices/auth_state_service/auth_service_data_provider.dart';
import 'package:shared_preferences/shared_preferences.dart';
enum AuthStatus {
authenticated,
adminValidation,
prepareProfile,
unauthenticated,
error
}
@injectable
class AuthService {
final AuthServiceDataProvider _dataProvider;
AuthService(this._dataProvider);
Future<AuthStatus> getAuthStatus() async {
User? user;
try {
user = FirebaseAuth.instance.currentUser;
if (user == null) {
return AuthStatus.unauthenticated;
}
user.getIdToken();
} catch (e) {
return AuthStatus.unauthenticated;
}
final staffStatus = await getCachedStaffStatus();
if (staffStatus == StaffStatus.deactivated) {
return AuthStatus.unauthenticated;
} else if (staffStatus == StaffStatus.pending) {
return AuthStatus.adminValidation;
} else if (staffStatus == StaffStatus.registered ||
staffStatus == StaffStatus.declined) {
return AuthStatus.prepareProfile;
} else {
return AuthStatus.authenticated;
}
}
Future<void> signInWithEmailLink({required Uri link}) async {
final auth = FirebaseAuth.instance;
//TODO: Investigate iOS issue that blocks correct usage of continue URL for seamless email verification
//
// if (!auth.isSignInWithEmailLink(link.toString())) {
// throw Exception('Invalid auth link provided.');
// }
//
// final sharedPrefs = await SharedPreferences.getInstance();
//
// final userEmail = sharedPrefs.getString(
// EmailValidationConstants.storedEmailKey,
// );
//
// if (userEmail == null) {
// throw Exception(
// 'Failed to sign in user with Email Link, '
// 'because the is no email stored',
// );
// }
//
// await auth.currentUser?.linkWithCredential(
// EmailAuthProvider.credentialWithLink(
// email: userEmail,
// emailLink: link.toString(),
// ),
// );
final oobCode = link.queryParameters[EmailValidationConstants.oobCodeKey];
if (oobCode == null) return;
log('Incoming auth link: $link');
return auth.applyActionCode(oobCode);
}
Future<StaffStatus?> getCachedStaffStatus() async {
final SharedPreferences prefs = await SharedPreferences.getInstance();
var uid = FirebaseAuth.instance.currentUser?.uid;
try {
var staffStatus = await _dataProvider.getStaffStatus();
prefs.setInt('staff_status_$uid', staffStatus.index);
return staffStatus;
} catch (e) {
log('Error in AuthService, on getCachedStaffStatus()', error: e);
}
if (uid != null) {
var staffStatusIndex = prefs.getInt('staff_status_$uid');
if (staffStatusIndex != null) {
return StaffStatus.values[staffStatusIndex];
}
}
return null;
}
void logout() {
FirebaseAuth.instance.signOut();
getIt<ApiClient>().dropCache();
}
Future<void> deleteAccount() async {
await FirebaseAuth.instance.currentUser!.delete();
getIt<ApiClient>().dropCache();
}
}

View File

@@ -0,0 +1,24 @@
import 'package:graphql_flutter/graphql_flutter.dart';
import 'package:injectable/injectable.dart';
import 'package:krow/core/application/clients/api/api_client.dart';
import 'package:krow/core/data/models/staff/staff.dart';
import 'package:krow/core/sevices/auth_state_service/gql.dart';
@injectable
class AuthServiceDataProvider {
final ApiClient _client;
AuthServiceDataProvider({required ApiClient client}) : _client = client;
Future<StaffStatus> getStaffStatus() async {
final QueryResult result = await _client.query(schema: getStaffStatusQuery);
if (result.hasException) {
throw Exception(result.exception.toString());
}
final Map<String, dynamic> data = result.data!['me'];
return Staff.fromJson(data).status!;
}
}

View File

@@ -0,0 +1,9 @@
const String getStaffStatusQuery = '''
query GetMe {
me {
id
status
}
}
''';

View File

@@ -0,0 +1,102 @@
import 'dart:async';
import 'dart:io';
import 'dart:ui';
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/material.dart';
import 'package:flutter_background_service/flutter_background_service.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:injectable/injectable.dart';
import 'package:krow/core/application/di/injectable.dart';
import 'package:krow/core/sevices/background_service/task_registry.dart';
import 'package:krow/features/shifts/domain/services/clockout_checker_bg_task.dart';
import 'package:workmanager/workmanager.dart';
@Singleton()
class BackgroundService {
Future<void> initializeService() async {
// FirebaseMessaging.onBackgroundMessage(firebaseMessagingBackgroundHandler);
//
// if (Platform.isIOS) {
// final service = FlutterBackgroundService();
// await service.configure(
// iosConfiguration: IosConfiguration(
// autoStart: true,
// onForeground: (_) {},
// onBackground: iosDispatcher,
// ),
// androidConfiguration: AndroidConfiguration(
// autoStart: false,
// onStart: (_) {},
// isForegroundMode: false,
// autoStartOnBoot: true,
// ),
// );
// service.startService();
// } else if (Platform.isAndroid) {
// Workmanager().initialize(
// androidDispatcher, // The top level function, aka callbackDispatcher
// isInDebugMode: false,
// // If enabled it will post a notification whenever the task is running. Handy for debugging tasks
// );
// if (Platform.isAndroid) {
// Workmanager().registerPeriodicTask(
// 'androidDispatcher',
// 'androidDispatcher',
// frequency: const Duration(minutes: 15),
// );
// }
// }
}
}
@pragma('vm:entry-point')
Future firebaseMessagingBackgroundHandler(RemoteMessage message) async {
// if (message.data['type'] == 'location_check') {
// await _configDependencies();
// await iosDispatcher(null);
// }
}
@pragma('vm:entry-point')
Future<bool> iosDispatcher(ServiceInstance? service) async {
// await _configDependencies();
// TaskRegistry.registerTask(ContinuousClockoutCheckerTask());
// for (var task in TaskRegistry.getRegisteredTasks()) {
// await task.oneTime(service);
// }
return true;
}
@pragma('vm:entry-point')
void androidDispatcher() async {
// Workmanager().executeTask((task, inputData) async {
// print('BG Task: executeTask');
//
// try {
// await _configDependencies();
// TaskRegistry.registerTask(ContinuousClockoutCheckerTask());
// for (var task in TaskRegistry.getRegisteredTasks()) {
// await task.oneTime(null);
// }
// }catch (e) {
// return Future.error(e);
// }
// return Future.value(true);
// });
}
Future<void> _configDependencies() async {
WidgetsFlutterBinding.ensureInitialized();
DartPluginRegistrant.ensureInitialized();
// await initHiveForFlutter(); - do no init hive in background!
//todo: add env variable
await dotenv.load(fileName: '.env');
await Firebase.initializeApp();
try {
configureDependencies('background');
} catch (e) {
print('Error configuring dependencies: $e');
}
}

View File

@@ -0,0 +1,6 @@
import 'package:flutter_background_service/flutter_background_service.dart';
abstract class BackgroundTask {
Future<void> oneTime(ServiceInstance? service);
Future<void> stop();
}

View File

@@ -0,0 +1,11 @@
import 'package:krow/core/sevices/background_service/background_task.dart';
class TaskRegistry {
static final List<BackgroundTask> _tasks = [];
static void registerTask(BackgroundTask task) {
_tasks.add(task);
}
static List<BackgroundTask> getRegisteredTasks() => _tasks;
}

View File

@@ -0,0 +1,107 @@
import 'dart:developer';
import 'package:geolocator/geolocator.dart';
import 'package:injectable/injectable.dart';
@injectable
class GeofencingService {
Future<GeolocationStatus> requestGeolocationPermission() async {
LocationPermission permission;
permission = await Geolocator.checkPermission();
if (permission == LocationPermission.denied) {
permission = await Geolocator.requestPermission();
if (permission == LocationPermission.denied) {
return GeolocationStatus.denied;
}
}
if (permission == LocationPermission.deniedForever) {
return GeolocationStatus.prohibited;
}
if (!(await Geolocator.isLocationServiceEnabled())) {
// Location services are not enabled
return GeolocationStatus.disabled;
}
// if (permission == LocationPermission.whileInUse) {
// return GeolocationStatus.onlyInUse;
// }
return GeolocationStatus.enabled;
}
bool _isInRange(
Position currentPosition,
double pointLat,
double pointLon,
int range,
) {
double distance = Geolocator.distanceBetween(
currentPosition.latitude,
currentPosition.longitude,
pointLat,
pointLon,
);
return distance <= range;
}
/// Checks if the user's current location is within [range] meters
/// of the given [pointLatitude] and [pointLongitude].
Future<bool> isInRangeCheck({
required double pointLatitude,
required double pointLongitude,
int range = 500,
}) async {
try {
Position position = await Geolocator.getCurrentPosition(
locationSettings: const LocationSettings(
accuracy: LocationAccuracy.best,
timeLimit: Duration(seconds: 7)));
return _isInRange(position, pointLatitude, pointLongitude, range);
} catch (except) {
log('Error getting location: $except');
return false;
}
}
/// Constantly checks if the user's current location is within [range] meters
/// of the given [pointLatitude] and [pointLongitude].
Stream<bool> isInRangeStream({
required double pointLatitude,
required double pointLongitude,
int range = 500,
}) async* {
yield _isInRange(
await Geolocator.getCurrentPosition(),
pointLatitude,
pointLongitude,
range,
);
await for (final position in Geolocator.getPositionStream(
locationSettings: const LocationSettings())) {
try {
yield _isInRange(position, pointLatitude, pointLongitude, range);
} catch (except) {
log('Error getting location: $except');
yield false;
}
}
}
}
/// [disabled] indicates that the user should enable geolocation on the device.
/// [denied] indicates that permission should be requested or re-requested.
/// [prohibited] indicates that permission is denied and can only be changed
/// via the Settings.
/// [enabled] - geolocation service is allowed and available for usage.
enum GeolocationStatus {
disabled,
denied,
prohibited,
onlyInUse,
enabled,
}