feat: legacy mobile apps created
This commit is contained in:
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
''';
|
||||
@@ -0,0 +1,4 @@
|
||||
extension BoolExtension on bool {
|
||||
/// Returns 0 if boolean is true and 1 otherwise.
|
||||
int toInt() => this ? 0 : 1;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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 => [];
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
enum StaffSkillStatus {
|
||||
verified,
|
||||
pending,
|
||||
declined,
|
||||
deactivated,
|
||||
}
|
||||
|
||||
enum StaffSkillLevel {
|
||||
beginner,
|
||||
skilled,
|
||||
professional,
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
enum StateStatus {
|
||||
idle,
|
||||
loading,
|
||||
error,
|
||||
success,
|
||||
}
|
||||
@@ -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(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
30
mobile-apps/legacy/staff-app/lib/core/data/models/skill.dart
Normal file
30
mobile-apps/legacy/staff-app/lib/core/data/models/skill.dart
Normal 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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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'],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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',
|
||||
};
|
||||
}
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
abstract class ContactsData {
|
||||
static const supportEmail = 'orders@legendaryeventstaff.com';
|
||||
static const supportPhone = '+1 (408) 315-5343';
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
abstract class EmailValidationConstants {
|
||||
static const storedEmailKey = 'user_pre_validation_email';
|
||||
static const oobCodeKey = 'oobCode';
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Helps to create a scrollable layout with two widgets in a scrollable column
|
||||
/// and space between them
|
||||
class ScrollLayoutHelper extends StatelessWidget {
|
||||
final Widget upperWidget;
|
||||
final Widget lowerWidget;
|
||||
final EdgeInsets padding;
|
||||
|
||||
final ScrollController? controller;
|
||||
|
||||
final Future Function()? onRefresh;
|
||||
|
||||
const ScrollLayoutHelper(
|
||||
{super.key,
|
||||
required this.upperWidget,
|
||||
required this.lowerWidget,
|
||||
this.padding = const EdgeInsets.symmetric(horizontal: 16),
|
||||
this.controller,
|
||||
this.onRefresh});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return LayoutBuilder(
|
||||
builder: (BuildContext context, BoxConstraints constraints) {
|
||||
Widget content = SingleChildScrollView(
|
||||
physics:
|
||||
onRefresh != null ? const AlwaysScrollableScrollPhysics() : null,
|
||||
controller: controller,
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
minHeight: constraints.maxHeight,
|
||||
),
|
||||
child: SafeArea(
|
||||
child: Padding(
|
||||
padding: padding,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
upperWidget,
|
||||
lowerWidget,
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
if (onRefresh != null) {
|
||||
content = RefreshIndicator(
|
||||
onRefresh: () => onRefresh!.call(),
|
||||
child: content,
|
||||
);
|
||||
}
|
||||
|
||||
return content;
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:krow/core/presentation/gen/assets.gen.dart';
|
||||
import 'package:krow/core/presentation/styles/theme.dart';
|
||||
|
||||
enum CheckBoxStyle {
|
||||
green,
|
||||
black,
|
||||
red,
|
||||
}
|
||||
|
||||
class KWCheckBox extends StatelessWidget {
|
||||
final bool value;
|
||||
final CheckBoxStyle? style;
|
||||
|
||||
const KWCheckBox({
|
||||
super.key,
|
||||
required this.value,
|
||||
this.style = CheckBoxStyle.green,
|
||||
});
|
||||
|
||||
Color get _color {
|
||||
switch (style!) {
|
||||
case CheckBoxStyle.green:
|
||||
return value ? AppColors.statusSuccess : Colors.white;
|
||||
case CheckBoxStyle.black:
|
||||
return value ? AppColors.bgColorDark : Colors.white;
|
||||
case CheckBoxStyle.red:
|
||||
return value ? AppColors.statusError : Colors.white;
|
||||
}
|
||||
}
|
||||
|
||||
Widget get _icon {
|
||||
switch (style!) {
|
||||
case CheckBoxStyle.green:
|
||||
case CheckBoxStyle.black:
|
||||
return Assets.images.icons.checkBox.check.svg();
|
||||
case CheckBoxStyle.red:
|
||||
return Assets.images.icons.checkBox.x.svg();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
width: 16,
|
||||
height: 16,
|
||||
decoration: BoxDecoration(
|
||||
color: _color,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
border: value
|
||||
? null
|
||||
: Border.all(color: AppColors.grayTintStroke, width: 1),
|
||||
),
|
||||
child: Center(
|
||||
child: value ? _icon : null,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,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(),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:krow/core/presentation/gen/assets.gen.dart';
|
||||
import 'package:krow/core/presentation/styles/kw_text_styles.dart';
|
||||
import 'package:krow/core/presentation/styles/theme.dart';
|
||||
|
||||
class ImagePreviewDialog extends StatelessWidget {
|
||||
final String title;
|
||||
final String imageUrl;
|
||||
|
||||
const ImagePreviewDialog({
|
||||
super.key,
|
||||
required this.title,
|
||||
required this.imageUrl,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Center(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.grayWhite,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24.0),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: AppTextStyles.bodyLargeMed,
|
||||
),
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: Assets.images.icons.x.svg(),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
//rounded corner image
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
child: _buildImage(context),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildImage(context) {
|
||||
if (Uri.parse(imageUrl).isAbsolute) {
|
||||
return Image.network(
|
||||
imageUrl,
|
||||
width: MediaQuery.of(context).size.width - 80,
|
||||
fit: BoxFit.cover,
|
||||
);
|
||||
} else {
|
||||
return Image.file(
|
||||
File(imageUrl),
|
||||
width: MediaQuery.of(context).size.width - 80,
|
||||
fit: BoxFit.cover,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
static void show(BuildContext context, String title, String imageUrl) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return ImagePreviewDialog(
|
||||
title: title,
|
||||
imageUrl: imageUrl,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,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();
|
||||
}
|
||||
},
|
||||
),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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(),
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
]
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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});
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class KwAnimatedImagePlaceholder extends StatefulWidget {
|
||||
const KwAnimatedImagePlaceholder({
|
||||
super.key,
|
||||
this.height = double.maxFinite,
|
||||
this.width = double.maxFinite,
|
||||
});
|
||||
|
||||
final double height;
|
||||
final double width;
|
||||
|
||||
@override
|
||||
State<KwAnimatedImagePlaceholder> createState() =>
|
||||
_KwAnimatedImagePlaceholderState();
|
||||
}
|
||||
|
||||
class _KwAnimatedImagePlaceholderState extends State<KwAnimatedImagePlaceholder>
|
||||
with TickerProviderStateMixin {
|
||||
late final AnimationController _controller = AnimationController(
|
||||
vsync: this,
|
||||
duration: Durations.long4,
|
||||
animationBehavior: AnimationBehavior.preserve,
|
||||
)
|
||||
..forward()
|
||||
..addListener(
|
||||
() {
|
||||
if (!_controller.isCompleted) return;
|
||||
|
||||
if (_controller.value == 0) {
|
||||
_controller.forward();
|
||||
} else {
|
||||
_controller.reverse();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
late final Animation<double> _opacity = Tween(
|
||||
begin: 0.6,
|
||||
end: 0.2,
|
||||
).animate(_controller);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AnimatedBuilder(
|
||||
animation: _controller,
|
||||
child: DecoratedBox(
|
||||
decoration: const BoxDecoration(
|
||||
color: Colors.grey,
|
||||
),
|
||||
child: SizedBox(
|
||||
height: widget.height,
|
||||
width: widget.width,
|
||||
),
|
||||
),
|
||||
builder: (context, child) {
|
||||
return Opacity(
|
||||
opacity: _opacity.value,
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,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(),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class KwLoadingOverlay extends StatefulWidget {
|
||||
const KwLoadingOverlay({
|
||||
super.key,
|
||||
required this.child,
|
||||
this.controller,
|
||||
this.shouldShowLoading,
|
||||
});
|
||||
|
||||
final Widget child;
|
||||
final OverlayPortalController? controller;
|
||||
final bool? shouldShowLoading;
|
||||
|
||||
@override
|
||||
State<KwLoadingOverlay> createState() => _KwLoadingOverlayState();
|
||||
}
|
||||
|
||||
class _KwLoadingOverlayState extends State<KwLoadingOverlay> {
|
||||
late final OverlayPortalController _controller;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_controller = widget.controller ?? OverlayPortalController();
|
||||
super.initState();
|
||||
|
||||
if (widget.shouldShowLoading ?? false) {
|
||||
WidgetsBinding.instance.addPostFrameCallback(
|
||||
(_) => _controller.show(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant KwLoadingOverlay oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback(
|
||||
(_) {
|
||||
if (widget.shouldShowLoading == null) return;
|
||||
|
||||
if (widget.shouldShowLoading!) {
|
||||
_controller.show();
|
||||
} else {
|
||||
_controller.hide();
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return OverlayPortal(
|
||||
controller: _controller,
|
||||
overlayChildBuilder: (context) {
|
||||
return const SizedBox(
|
||||
height: double.maxFinite,
|
||||
width: double.maxFinite,
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black38,
|
||||
),
|
||||
child: Center(
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
child: widget.child,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
if (context.mounted) _controller.hide();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,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]),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
}
|
||||
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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!;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
|
||||
const String getStaffStatusQuery = '''
|
||||
query GetMe {
|
||||
me {
|
||||
id
|
||||
status
|
||||
}
|
||||
}
|
||||
''';
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import 'package:flutter_background_service/flutter_background_service.dart';
|
||||
|
||||
abstract class BackgroundTask {
|
||||
Future<void> oneTime(ServiceInstance? service);
|
||||
Future<void> stop();
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
Reference in New Issue
Block a user