first commit
This commit is contained in:
118
lib/app/core/widgets/app_accordion.dart
Normal file
118
lib/app/core/widgets/app_accordion.dart
Normal file
@@ -0,0 +1,118 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:frontend_eccp_mobile/app/core/constants/constants.dart';
|
||||
|
||||
class AppAccordion extends StatefulWidget {
|
||||
const AppAccordion({
|
||||
required this.title,
|
||||
required this.child,
|
||||
this.description,
|
||||
this.initialExpanded = false,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final String title;
|
||||
final String? description;
|
||||
final Widget child;
|
||||
final bool initialExpanded;
|
||||
|
||||
@override
|
||||
State<AppAccordion> createState() => _AppAccordionState();
|
||||
}
|
||||
|
||||
class _AppAccordionState extends State<AppAccordion> {
|
||||
late bool expanded;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
expanded = widget.initialExpanded;
|
||||
}
|
||||
|
||||
void toggle() {
|
||||
setState(() {
|
||||
expanded = !expanded;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
margin: AppPadding.b12,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: Border.all(color: AppColors.neutral200),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.06),
|
||||
blurRadius: 3,
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
InkWell(
|
||||
onTap: toggle,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 14,
|
||||
vertical: 12,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
widget.title,
|
||||
style: AppTextStyles.label.copyWith(
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
),
|
||||
AnimatedRotation(
|
||||
turns: expanded ? 0.5 : 0,
|
||||
duration: AppDurations.fast,
|
||||
child: Icon(
|
||||
Icons.keyboard_arrow_down,
|
||||
color: AppColors.neutral400,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
ClipRect(
|
||||
child: AnimatedAlign(
|
||||
alignment: Alignment.topCenter,
|
||||
heightFactor: expanded ? 1 : 0,
|
||||
duration: AppDurations.fast,
|
||||
curve: Curves.easeInOut,
|
||||
child: AnimatedOpacity(
|
||||
opacity: expanded ? 1 : 0,
|
||||
duration: AppDurations.fast,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(14, 0, 14, 12),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (widget.description != null) ...[
|
||||
Text(
|
||||
widget.description!,
|
||||
style: AppTextStyles.small.copyWith(
|
||||
fontStyle: FontStyle.italic,
|
||||
color: AppColors.neutral700,
|
||||
),
|
||||
),
|
||||
AppSpacing.h8,
|
||||
],
|
||||
widget.child,
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
108
lib/app/core/widgets/app_bottom_nav.dart
Normal file
108
lib/app/core/widgets/app_bottom_nav.dart
Normal file
@@ -0,0 +1,108 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:frontend_eccp_mobile/app/core/constants/constants.dart';
|
||||
import 'package:iconsax_plus/iconsax_plus.dart';
|
||||
|
||||
class AppBottomNav extends StatelessWidget {
|
||||
const AppBottomNav({
|
||||
required this.currentIndex,
|
||||
required this.onTap,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final int currentIndex;
|
||||
final ValueChanged<int> onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: EdgeInsets.only(
|
||||
top: AppSizes.s12,
|
||||
bottom: MediaQuery.of(context).viewPadding.bottom,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
border: Border(
|
||||
top: BorderSide(color: AppColors.neutral200),
|
||||
),
|
||||
color: Colors.white,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
_NavItem(
|
||||
0,
|
||||
currentIndex,
|
||||
IconsaxPlusLinear.home_2,
|
||||
IconsaxPlusBold.home_2,
|
||||
'Beranda',
|
||||
onTap,
|
||||
),
|
||||
_NavItem(
|
||||
1,
|
||||
currentIndex,
|
||||
IconsaxPlusLinear.document,
|
||||
IconsaxPlusBold.document,
|
||||
'Berkas',
|
||||
onTap,
|
||||
),
|
||||
_NavItem(
|
||||
2,
|
||||
currentIndex,
|
||||
IconsaxPlusLinear.setting,
|
||||
IconsaxPlusBold.setting,
|
||||
'Pengaturan',
|
||||
onTap,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _NavItem extends StatelessWidget {
|
||||
const _NavItem(
|
||||
this.index,
|
||||
this.currentIndex,
|
||||
this.icon,
|
||||
this.activeIcon,
|
||||
this.label,
|
||||
this.onTap,
|
||||
);
|
||||
|
||||
final int index;
|
||||
final int currentIndex;
|
||||
final IconData icon;
|
||||
final IconData activeIcon;
|
||||
final String label;
|
||||
final ValueChanged<int> onTap;
|
||||
|
||||
bool get isActive => index == currentIndex;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final activeColor = AppColors.primary900;
|
||||
final inactiveColor = AppColors.neutral500;
|
||||
|
||||
return GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: () => onTap(index),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
isActive ? activeIcon : icon,
|
||||
size: AppIconSizes.lg,
|
||||
color: AppColors.primary900,
|
||||
),
|
||||
AppSpacing.h4,
|
||||
Text(
|
||||
label,
|
||||
style: AppTextStyles.badge.copyWith(
|
||||
fontSize: 11,
|
||||
color: isActive ? activeColor : inactiveColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
128
lib/app/core/widgets/app_button.dart
Normal file
128
lib/app/core/widgets/app_button.dart
Normal file
@@ -0,0 +1,128 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:frontend_eccp_mobile/app/core/constants/constants.dart';
|
||||
import 'package:frontend_eccp_mobile/app/core/constants/enum/appbar_type_enum.dart';
|
||||
|
||||
class AppButton extends StatelessWidget {
|
||||
const AppButton({
|
||||
required this.label,
|
||||
required this.onPressed,
|
||||
super.key,
|
||||
this.type = AppButtonType.primary,
|
||||
this.width,
|
||||
this.height,
|
||||
this.isExpanded = false,
|
||||
this.radius = AppSizes.s8,
|
||||
this.borderWidth = 1.5,
|
||||
this.paddingVertical = 14,
|
||||
this.paddingHorizontal = 24,
|
||||
this.backgroundColor,
|
||||
this.borderColor,
|
||||
this.textColor,
|
||||
this.fontSize = 16,
|
||||
this.fontWeight = FontWeight.w600,
|
||||
this.isLoading = false,
|
||||
this.customContent,
|
||||
});
|
||||
|
||||
final String label;
|
||||
final VoidCallback? onPressed;
|
||||
final AppButtonType type;
|
||||
|
||||
final double? width;
|
||||
final double? height;
|
||||
final bool isExpanded;
|
||||
|
||||
final double radius;
|
||||
final double borderWidth;
|
||||
final double paddingVertical;
|
||||
final double paddingHorizontal;
|
||||
|
||||
final Color? backgroundColor;
|
||||
final Color? borderColor;
|
||||
final Color? textColor;
|
||||
|
||||
final double fontSize;
|
||||
final FontWeight fontWeight;
|
||||
final bool isLoading;
|
||||
|
||||
final Widget? customContent;
|
||||
|
||||
bool get _isPrimary => type == AppButtonType.primary;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final bgColor = _isPrimary
|
||||
? (backgroundColor ?? AppColors.primary900)
|
||||
: (backgroundColor ?? AppColors.neutralWhite);
|
||||
|
||||
final brColor = _isPrimary
|
||||
? Colors.transparent
|
||||
: (borderColor ?? AppColors.primary900);
|
||||
|
||||
final txtColor = _isPrimary
|
||||
? (textColor ?? Colors.white)
|
||||
: (textColor ?? AppColors.primary900);
|
||||
|
||||
final defaultChild = Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
width: isLoading ? AppIconSizes.sm : 0,
|
||||
margin: EdgeInsets.only(
|
||||
right: isLoading ? AppSizes.s12 : 0,
|
||||
),
|
||||
child: AnimatedOpacity(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
opacity: isLoading ? 1 : 0,
|
||||
child: SizedBox(
|
||||
width: AppIconSizes.sm,
|
||||
height: AppIconSizes.sm,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
valueColor: AlwaysStoppedAnimation(txtColor),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
label,
|
||||
style: AppTextStyles.button.copyWith(
|
||||
fontSize: fontSize,
|
||||
fontWeight: fontWeight,
|
||||
color: txtColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
final button = ElevatedButton(
|
||||
onPressed: isLoading ? null : onPressed,
|
||||
style: ElevatedButton.styleFrom(
|
||||
elevation: 0,
|
||||
backgroundColor: bgColor,
|
||||
padding: EdgeInsets.symmetric(
|
||||
vertical: paddingVertical,
|
||||
horizontal: paddingHorizontal,
|
||||
),
|
||||
shape: RoundedRectangleBorder(
|
||||
side: BorderSide(
|
||||
color: brColor,
|
||||
width: borderWidth,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(radius),
|
||||
),
|
||||
),
|
||||
child: AnimatedSize(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
child: customContent ?? defaultChild,
|
||||
),
|
||||
);
|
||||
|
||||
return SizedBox(
|
||||
width: isExpanded ? double.infinity : width,
|
||||
height: height,
|
||||
child: button,
|
||||
);
|
||||
}
|
||||
}
|
||||
65
lib/app/core/widgets/app_dropdown.dart
Normal file
65
lib/app/core/widgets/app_dropdown.dart
Normal file
@@ -0,0 +1,65 @@
|
||||
import 'package:animated_custom_dropdown/custom_dropdown.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:frontend_eccp_mobile/app/core/constants/constants.dart';
|
||||
|
||||
class AppDropdown<T> extends StatelessWidget {
|
||||
const AppDropdown({
|
||||
required this.items,
|
||||
required this.onChanged,
|
||||
super.key,
|
||||
this.value,
|
||||
this.title,
|
||||
this.hint = 'Select',
|
||||
this.showTitle = true,
|
||||
this.isRequired = false,
|
||||
this.isValid = true,
|
||||
this.informationValid = '',
|
||||
this.bgColor = const Color(0xFFF9FAFB),
|
||||
this.borderColor = const Color(0xFFE5E7EB),
|
||||
this.radius = AppSizes.s16,
|
||||
});
|
||||
|
||||
final List<T> items;
|
||||
final T? value;
|
||||
final ValueChanged<T?> onChanged;
|
||||
|
||||
final String? title;
|
||||
final String hint;
|
||||
final bool showTitle;
|
||||
final bool isRequired;
|
||||
final bool isValid;
|
||||
final String informationValid;
|
||||
|
||||
final Color bgColor;
|
||||
final Color borderColor;
|
||||
final double radius;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (showTitle && title != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: AppSizes.s12),
|
||||
child: Text(
|
||||
title!,
|
||||
style: AppTextStyles.label,
|
||||
),
|
||||
),
|
||||
CustomDropdown<T>(
|
||||
items: items,
|
||||
initialItem: value,
|
||||
onChanged: onChanged,
|
||||
hintText: hint,
|
||||
overlayHeight: 240,
|
||||
decoration: CustomDropdownDecoration(
|
||||
closedBorderRadius: BorderRadius.circular(radius),
|
||||
closedFillColor: bgColor,
|
||||
hintStyle: AppTextStyles.body,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
32
lib/app/core/widgets/app_empty.dart
Normal file
32
lib/app/core/widgets/app_empty.dart
Normal file
@@ -0,0 +1,32 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:frontend_eccp_mobile/app/core/constants/app_colors.dart';
|
||||
import 'package:frontend_eccp_mobile/app/core/constants/app_spacing.dart';
|
||||
import 'package:frontend_eccp_mobile/app/core/constants/app_text_styles.dart';
|
||||
import 'package:iconsax_plus/iconsax_plus.dart';
|
||||
|
||||
Widget empty() {
|
||||
return Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
IconsaxPlusLinear.task_square,
|
||||
size: 42,
|
||||
color: AppColors.neutral400,
|
||||
),
|
||||
|
||||
AppSpacing.h12,
|
||||
|
||||
Text(
|
||||
'Oops,',
|
||||
style: AppTextStyles.label,
|
||||
),
|
||||
|
||||
Text(
|
||||
'No Data Found',
|
||||
style: AppTextStyles.small.copyWith(
|
||||
color: AppColors.neutral600,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
212
lib/app/core/widgets/app_shad_textfield.dart
Normal file
212
lib/app/core/widgets/app_shad_textfield.dart
Normal file
@@ -0,0 +1,212 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:frontend_eccp_mobile/app/core/constants/constants.dart';
|
||||
import 'package:shadcn_ui/shadcn_ui.dart';
|
||||
|
||||
class AppShadInput extends StatefulWidget {
|
||||
const AppShadInput({
|
||||
required this.title,
|
||||
required this.hint,
|
||||
super.key,
|
||||
this.showTitle = true,
|
||||
this.isRequired = false,
|
||||
this.isEnabled = true,
|
||||
this.width,
|
||||
this.isPassword = false,
|
||||
this.maxLines = 1,
|
||||
this.maxLength,
|
||||
this.keyboardType,
|
||||
this.inputFormatters,
|
||||
this.onChanged,
|
||||
this.leading,
|
||||
this.trailing,
|
||||
this.focusNode,
|
||||
this.textInputAction,
|
||||
this.autofocus = false,
|
||||
this.readOnly = false,
|
||||
this.padding,
|
||||
this.inputPadding = const EdgeInsets.symmetric(vertical: 7, horizontal: 5),
|
||||
this.bgColor = Colors.white,
|
||||
this.decoration,
|
||||
this.controller,
|
||||
this.isForm = false,
|
||||
this.id,
|
||||
this.validator,
|
||||
this.description,
|
||||
this.radius,
|
||||
});
|
||||
|
||||
final String title;
|
||||
final String hint;
|
||||
|
||||
final bool showTitle;
|
||||
final bool isRequired;
|
||||
final bool isEnabled;
|
||||
final double? width;
|
||||
final double? radius;
|
||||
|
||||
final bool isPassword;
|
||||
final int maxLines;
|
||||
final int? maxLength;
|
||||
|
||||
final TextInputType? keyboardType;
|
||||
final List<TextInputFormatter>? inputFormatters;
|
||||
final void Function(String)? onChanged;
|
||||
|
||||
final Widget? leading;
|
||||
final Widget? trailing;
|
||||
|
||||
final FocusNode? focusNode;
|
||||
final TextInputAction? textInputAction;
|
||||
final bool autofocus;
|
||||
final bool readOnly;
|
||||
|
||||
final EdgeInsetsGeometry? padding;
|
||||
final EdgeInsetsGeometry inputPadding;
|
||||
final Color bgColor;
|
||||
final ShadDecoration? decoration;
|
||||
|
||||
final TextEditingController? controller;
|
||||
|
||||
final bool isForm;
|
||||
final String? id;
|
||||
final String? Function(String value)? validator;
|
||||
final Widget? description;
|
||||
|
||||
@override
|
||||
State<AppShadInput> createState() => _AppShadInputState();
|
||||
}
|
||||
|
||||
class _AppShadInputState extends State<AppShadInput> {
|
||||
bool _obscure = true;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
assert(
|
||||
widget.isForm ? widget.id != null : widget.controller != null,
|
||||
'Jika isForm=true maka id wajib diisi, jika isForm=false maka controller wajib diisi',
|
||||
);
|
||||
}
|
||||
|
||||
ShadDecoration get _decoration =>
|
||||
widget.decoration ??
|
||||
ShadDecoration(
|
||||
color: widget.bgColor,
|
||||
border: ShadBorder.all(
|
||||
color: AppColors.neutral200,
|
||||
radius: BorderRadius.circular(widget.radius ?? AppSizes.s12),
|
||||
),
|
||||
secondaryFocusedBorder: ShadBorder.all(
|
||||
color: AppColors.neutral700,
|
||||
width: 2,
|
||||
radius: BorderRadius.circular(
|
||||
(widget.radius ?? AppSizes.s12) + 4,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final trailing = widget.isPassword
|
||||
? ShadIconButton(
|
||||
width: 24,
|
||||
height: 24,
|
||||
backgroundColor: AppColors.neutral200,
|
||||
pressedBackgroundColor: AppColors.neutral200,
|
||||
icon: Icon(
|
||||
_obscure ? LucideIcons.eyeOff : LucideIcons.eye,
|
||||
size: 18,
|
||||
color: AppColors.neutral600,
|
||||
),
|
||||
onPressed: () {
|
||||
setState(() => _obscure = !_obscure);
|
||||
},
|
||||
)
|
||||
: widget.trailing;
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (widget.showTitle)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: AppSizes.s12),
|
||||
child: Row(
|
||||
children: [
|
||||
Text(
|
||||
widget.title,
|
||||
style: AppTextStyles.label.copyWith(
|
||||
color: AppColors.supportSteel600,
|
||||
),
|
||||
),
|
||||
if (widget.isRequired)
|
||||
const Text(
|
||||
' *',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: Colors.red,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
width: widget.width,
|
||||
child: widget.isForm
|
||||
? ShadInputFormField(
|
||||
id: widget.id,
|
||||
enabled: widget.isEnabled,
|
||||
validator: widget.validator,
|
||||
description: widget.description,
|
||||
placeholder: Text(
|
||||
widget.hint,
|
||||
style: AppTextStyles.body.copyWith(
|
||||
color: AppColors.neutral900.withOpacity(0.5),
|
||||
),
|
||||
),
|
||||
decoration: _decoration,
|
||||
obscureText: widget.isPassword && _obscure,
|
||||
maxLines: widget.maxLines,
|
||||
maxLength: widget.maxLength,
|
||||
keyboardType: widget.keyboardType,
|
||||
inputFormatters: widget.inputFormatters,
|
||||
onChanged: widget.onChanged,
|
||||
leading: widget.leading,
|
||||
onPressedOutside: (event) => FocusScope.of(context).unfocus(),
|
||||
trailing: trailing,
|
||||
inputPadding: widget.inputPadding,
|
||||
style: AppTextStyles.body,
|
||||
)
|
||||
: ShadInput(
|
||||
controller: widget.controller,
|
||||
enabled: widget.isEnabled,
|
||||
focusNode: widget.focusNode,
|
||||
readOnly: widget.readOnly,
|
||||
autofocus: widget.autofocus,
|
||||
keyboardType: widget.keyboardType,
|
||||
textInputAction: widget.textInputAction,
|
||||
maxLines: widget.maxLines,
|
||||
maxLength: widget.maxLength,
|
||||
inputFormatters: widget.inputFormatters,
|
||||
obscureText: widget.isPassword && _obscure,
|
||||
onChanged: widget.onChanged,
|
||||
padding: widget.padding,
|
||||
inputPadding: widget.inputPadding,
|
||||
onPressedOutside: (event) => FocusScope.of(context).unfocus(),
|
||||
decoration: _decoration,
|
||||
placeholder: Text(
|
||||
widget.hint,
|
||||
style: AppTextStyles.body.copyWith(
|
||||
color: AppColors.neutral900.withOpacity(0.5),
|
||||
),
|
||||
),
|
||||
style: AppTextStyles.body,
|
||||
leading: widget.leading,
|
||||
trailing: trailing,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
36
lib/app/core/widgets/app_something_wrong.dart
Normal file
36
lib/app/core/widgets/app_something_wrong.dart
Normal file
@@ -0,0 +1,36 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:frontend_eccp_mobile/app/core/constants/app_colors.dart';
|
||||
import 'package:frontend_eccp_mobile/app/core/constants/app_spacing.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'package:iconsax_plus/iconsax_plus.dart';
|
||||
|
||||
Widget somethingWrong() {
|
||||
return Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
IconsaxPlusLinear.cloud_remove,
|
||||
size: 52,
|
||||
color: AppColors.neutral400,
|
||||
),
|
||||
AppSpacing.h12,
|
||||
Text(
|
||||
'Oops, something wrong',
|
||||
style: GoogleFonts.inter(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: AppColors.neutral900,
|
||||
),
|
||||
),
|
||||
AppSpacing.h4,
|
||||
Text(
|
||||
'Please try again later',
|
||||
style: GoogleFonts.inter(
|
||||
fontSize: 12,
|
||||
color: AppColors.neutral600,
|
||||
),
|
||||
),
|
||||
AppSpacing.h12,
|
||||
],
|
||||
);
|
||||
}
|
||||
278
lib/app/core/widgets/app_textfield.dart
Normal file
278
lib/app/core/widgets/app_textfield.dart
Normal file
@@ -0,0 +1,278 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:frontend_eccp_mobile/app/core/constants/constants.dart';
|
||||
|
||||
class AppTextField extends StatefulWidget {
|
||||
const AppTextField({
|
||||
required this.title,
|
||||
required this.hint,
|
||||
required this.password,
|
||||
required this.controller,
|
||||
required this.isValid,
|
||||
required this.informationValid,
|
||||
super.key,
|
||||
this.maxLine = 1,
|
||||
this.maxLength,
|
||||
this.showTitle = true,
|
||||
this.isEnabled = true,
|
||||
this.keyboardType,
|
||||
this.radius = AppSizes.s16,
|
||||
this.phoneText,
|
||||
this.inputFormatters,
|
||||
this.width,
|
||||
this.isRequired = false,
|
||||
this.contentPaddingVertical = AppSizes.s12,
|
||||
this.prefixIcon,
|
||||
this.showPrefixIcon = false,
|
||||
this.prefixIconColor,
|
||||
this.prefixIconSize = AppIconSizes.md,
|
||||
this.prefixIconPadding = AppSizes.s12,
|
||||
this.isDense = false,
|
||||
this.minHeight,
|
||||
this.onChanged,
|
||||
this.iconRight,
|
||||
this.bgColor = const Color(0xFFF9FAFB),
|
||||
this.hintColor = const Color(0x800A0A0A),
|
||||
this.titleColor = const Color(0xFF364153),
|
||||
this.borderColor = const Color(0xFFE5E7EB),
|
||||
});
|
||||
|
||||
final String title;
|
||||
final String hint;
|
||||
final bool password;
|
||||
final TextEditingController controller;
|
||||
|
||||
final int maxLine;
|
||||
final int? maxLength;
|
||||
final double contentPaddingVertical;
|
||||
|
||||
final bool isEnabled;
|
||||
final bool showTitle;
|
||||
final bool isRequired;
|
||||
final bool isValid;
|
||||
|
||||
final String informationValid;
|
||||
final double radius;
|
||||
final double? width;
|
||||
|
||||
final String? phoneText;
|
||||
final TextInputType? keyboardType;
|
||||
final List<TextInputFormatter>? inputFormatters;
|
||||
|
||||
final Color bgColor;
|
||||
final Color titleColor;
|
||||
final Color hintColor;
|
||||
final Color borderColor;
|
||||
|
||||
final Widget? prefixIcon;
|
||||
final bool showPrefixIcon;
|
||||
final Color? prefixIconColor;
|
||||
final double prefixIconSize;
|
||||
final double prefixIconPadding;
|
||||
|
||||
final bool isDense;
|
||||
final double? minHeight;
|
||||
|
||||
final Widget? iconRight;
|
||||
final void Function(String)? onChanged;
|
||||
|
||||
@override
|
||||
State<AppTextField> createState() => _AppTextFieldState();
|
||||
}
|
||||
|
||||
class _AppTextFieldState extends State<AppTextField> {
|
||||
bool _showPass = true;
|
||||
final FocusNode _focusNode = FocusNode();
|
||||
final GlobalKey _fieldKey = GlobalKey();
|
||||
OverlayEntry? _overlayEntry;
|
||||
|
||||
void _scrollIntoView() {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||
if (!mounted) return;
|
||||
await Scrollable.ensureVisible(
|
||||
context,
|
||||
alignment: 0.25,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
curve: Curves.easeOut,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
_focusNode.addListener(() {
|
||||
if (_focusNode.hasFocus) {
|
||||
_addTapOutsideOverlay();
|
||||
_scrollIntoView();
|
||||
} else {
|
||||
_removeTapOutsideOverlay();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void _addTapOutsideOverlay() {
|
||||
_overlayEntry = OverlayEntry(
|
||||
builder: (_) => Positioned.fill(
|
||||
child: GestureDetector(
|
||||
behavior: HitTestBehavior.translucent,
|
||||
onTap: _focusNode.unfocus,
|
||||
),
|
||||
),
|
||||
);
|
||||
Overlay.of(context).insert(_overlayEntry!);
|
||||
}
|
||||
|
||||
void _removeTapOutsideOverlay() {
|
||||
_overlayEntry?.remove();
|
||||
_overlayEntry = null;
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_removeTapOutsideOverlay();
|
||||
_focusNode.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
key: _fieldKey,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (widget.showTitle)
|
||||
Padding(
|
||||
padding: AppPadding.b8,
|
||||
child: Row(
|
||||
children: [
|
||||
Text(
|
||||
widget.title,
|
||||
style: AppTextStyles.label.copyWith(
|
||||
color: widget.titleColor,
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
),
|
||||
if (widget.isRequired)
|
||||
Text(
|
||||
' *',
|
||||
style: AppTextStyles.body.copyWith(
|
||||
color: AppColors.primary500,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Container(
|
||||
width: widget.width,
|
||||
constraints: widget.minHeight != null
|
||||
? BoxConstraints(minHeight: widget.minHeight!)
|
||||
: null,
|
||||
decoration: BoxDecoration(
|
||||
color: widget.bgColor,
|
||||
borderRadius: BorderRadius.circular(widget.radius),
|
||||
border: Border.all(
|
||||
color: widget.isValid ? widget.borderColor : AppColors.primary500,
|
||||
width: 1.2,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
if (widget.showPrefixIcon && widget.prefixIcon != null)
|
||||
Padding(
|
||||
padding: EdgeInsets.only(
|
||||
left: widget.prefixIconPadding,
|
||||
right: AppSizes.s12,
|
||||
),
|
||||
child: IconTheme(
|
||||
data: IconThemeData(
|
||||
size: widget.prefixIconSize,
|
||||
color: widget.prefixIconColor ?? widget.hintColor,
|
||||
),
|
||||
child: widget.prefixIcon!,
|
||||
),
|
||||
)
|
||||
else
|
||||
AppSpacing.w16,
|
||||
if (widget.phoneText != null)
|
||||
Padding(
|
||||
padding: AppPadding.r8,
|
||||
child: Text(
|
||||
widget.phoneText!,
|
||||
style: AppTextStyles.body.copyWith(
|
||||
color: widget.hintColor,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
controller: widget.controller,
|
||||
obscureText: widget.password && _showPass,
|
||||
maxLines: widget.maxLine,
|
||||
enabled: widget.isEnabled,
|
||||
maxLength: widget.maxLength,
|
||||
keyboardType: widget.keyboardType,
|
||||
inputFormatters: widget.inputFormatters,
|
||||
style: AppTextStyles.body.copyWith(
|
||||
fontSize: widget.isDense ? 13 : 14,
|
||||
),
|
||||
decoration: InputDecoration(
|
||||
isDense: widget.isDense,
|
||||
hintText: widget.hint,
|
||||
hintStyle: AppTextStyles.body.copyWith(
|
||||
fontSize: widget.isDense ? 12 : 13,
|
||||
color: widget.hintColor,
|
||||
),
|
||||
contentPadding: EdgeInsets.symmetric(
|
||||
vertical: widget.isDense
|
||||
? AppSizes.s8
|
||||
: widget.contentPaddingVertical,
|
||||
),
|
||||
border: InputBorder.none,
|
||||
counterText: '',
|
||||
),
|
||||
onChanged: widget.onChanged,
|
||||
),
|
||||
),
|
||||
if (widget.iconRight != null)
|
||||
Padding(
|
||||
padding: AppPadding.r12,
|
||||
child: widget.iconRight,
|
||||
),
|
||||
if (widget.password)
|
||||
Padding(
|
||||
padding: AppPadding.r12,
|
||||
child: GestureDetector(
|
||||
onTap: () => setState(() => _showPass = !_showPass),
|
||||
child: Icon(
|
||||
_showPass
|
||||
? Icons.visibility_off_rounded
|
||||
: Icons.visibility_rounded,
|
||||
size: AppIconSizes.md,
|
||||
color: AppColors.neutral400,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (!widget.isValid)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
top: AppSizes.s8,
|
||||
left: AppSizes.s12,
|
||||
),
|
||||
child: Text(
|
||||
widget.informationValid,
|
||||
style: AppTextStyles.small.copyWith(
|
||||
color: AppColors.primary500,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
211
lib/app/core/widgets/bottomsheet/app_bottom_sheet.dart
Normal file
211
lib/app/core/widgets/bottomsheet/app_bottom_sheet.dart
Normal file
@@ -0,0 +1,211 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:frontend_eccp_mobile/app/core/constants/constants.dart';
|
||||
import 'package:frontend_eccp_mobile/app/core/constants/enum/appbar_type_enum.dart';
|
||||
import 'package:frontend_eccp_mobile/app/core/widgets/app_button.dart';
|
||||
import 'package:frontend_eccp_mobile/app/core/widgets/bottomsheet/appbottomsheet_controller.dart';
|
||||
|
||||
class AppBottomSheet extends StatefulWidget {
|
||||
const AppBottomSheet({
|
||||
required this.controller,
|
||||
super.key,
|
||||
this.icon,
|
||||
this.title,
|
||||
this.subTitle,
|
||||
this.content,
|
||||
this.primaryText,
|
||||
this.secondaryText,
|
||||
this.onPrimaryPressed,
|
||||
this.onSecondaryPressed,
|
||||
this.usePrimaryButton = true,
|
||||
this.useSecondaryButton = false,
|
||||
this.onOpened,
|
||||
this.onClosed,
|
||||
this.onDismissed,
|
||||
this.enableDrag = true,
|
||||
});
|
||||
|
||||
final AppBottomSheetController controller;
|
||||
|
||||
final String? icon;
|
||||
final String? title;
|
||||
final String? subTitle;
|
||||
final Widget? content;
|
||||
|
||||
final String? primaryText;
|
||||
final String? secondaryText;
|
||||
final VoidCallback? onPrimaryPressed;
|
||||
final VoidCallback? onSecondaryPressed;
|
||||
|
||||
final bool useSecondaryButton;
|
||||
final bool usePrimaryButton;
|
||||
|
||||
final VoidCallback? onOpened;
|
||||
final VoidCallback? onClosed;
|
||||
final VoidCallback? onDismissed;
|
||||
|
||||
final bool enableDrag;
|
||||
|
||||
@override
|
||||
State<AppBottomSheet> createState() => _AppBottomSheetState();
|
||||
}
|
||||
|
||||
class _AppBottomSheetState extends State<AppBottomSheet> {
|
||||
bool _dismissedBySystem = true;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
widget.onOpened?.call();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
if (_dismissedBySystem) {
|
||||
widget.onDismissed?.call();
|
||||
}
|
||||
|
||||
widget.onClosed?.call();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _closeByAction() {
|
||||
_dismissedBySystem = false;
|
||||
Navigator.pop(context);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Material(
|
||||
color: Colors.transparent,
|
||||
child: AnimatedBuilder(
|
||||
animation: widget.controller,
|
||||
builder: (_, _) {
|
||||
return Align(
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(
|
||||
maxWidth: AppSizes.maxContentWidth,
|
||||
),
|
||||
child: Container(
|
||||
padding: EdgeInsets.fromLTRB(
|
||||
AppSizes.s16,
|
||||
AppSizes.s16,
|
||||
AppSizes.s16,
|
||||
MediaQuery.of(context).viewPadding.bottom + AppSizes.s16,
|
||||
),
|
||||
decoration: const BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.vertical(
|
||||
top: Radius.circular(16),
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Center(
|
||||
child: Container(
|
||||
width: AppSizes.bottomSheetHandleWidth,
|
||||
height: AppSizes.bottomSheetHandleHeight,
|
||||
margin: const EdgeInsets.only(
|
||||
bottom: AppSizes.s16,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.neutral200,
|
||||
borderRadius: BorderRadius.circular(AppSizes.s8),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
if (widget.icon != null) ...[
|
||||
Center(
|
||||
child: widget.icon!.endsWith('.svg')
|
||||
? SvgPicture.asset(
|
||||
widget.icon!,
|
||||
width:
|
||||
MediaQuery.of(context).size.width *
|
||||
AppSizes.illustrationWidthFactor,
|
||||
)
|
||||
: Image.asset(
|
||||
widget.icon!,
|
||||
width:
|
||||
MediaQuery.of(context).size.width *
|
||||
AppSizes.illustrationWidthFactor,
|
||||
),
|
||||
),
|
||||
AppSpacing.h16,
|
||||
],
|
||||
|
||||
if (widget.title != null)
|
||||
Text(
|
||||
widget.title!,
|
||||
style: AppTextStyles.h3,
|
||||
),
|
||||
|
||||
if (widget.subTitle != null) ...[
|
||||
AppSpacing.h8,
|
||||
Text(
|
||||
widget.subTitle!,
|
||||
style: AppTextStyles.small,
|
||||
),
|
||||
],
|
||||
|
||||
if (widget.content != null) ...[
|
||||
AppSpacing.h24,
|
||||
widget.content!,
|
||||
],
|
||||
|
||||
if (widget.useSecondaryButton || widget.usePrimaryButton)
|
||||
AppSpacing.h24,
|
||||
|
||||
Row(
|
||||
children: [
|
||||
if (widget.useSecondaryButton) ...[
|
||||
Expanded(
|
||||
child: AppButton(
|
||||
radius: AppSizes.s12,
|
||||
paddingVertical: AppSizes.s12,
|
||||
backgroundColor: Colors.white,
|
||||
label: widget.secondaryText ?? 'Batal',
|
||||
type: AppButtonType.secondary,
|
||||
onPressed: widget.controller.isLoading
|
||||
? null
|
||||
: () {
|
||||
_closeByAction();
|
||||
widget.onSecondaryPressed?.call();
|
||||
},
|
||||
),
|
||||
),
|
||||
AppSpacing.w12,
|
||||
],
|
||||
|
||||
if (widget.usePrimaryButton)
|
||||
Expanded(
|
||||
child: AppButton(
|
||||
label: widget.primaryText ?? 'OK',
|
||||
radius: AppSizes.s12,
|
||||
paddingVertical: AppSizes.s12,
|
||||
isLoading: widget.controller.isLoading,
|
||||
onPressed: widget.controller.isLoading
|
||||
? null
|
||||
: () {
|
||||
widget.onPrimaryPressed?.call();
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
class AppBottomSheetController extends ChangeNotifier {
|
||||
bool _isLoading = false;
|
||||
|
||||
bool get isLoading => _isLoading;
|
||||
|
||||
void setLoading({required bool value}) {
|
||||
if (_isLoading == value) return;
|
||||
_isLoading = value;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
289
lib/app/core/widgets/layout/app_screen.dart
Normal file
289
lib/app/core/widgets/layout/app_screen.dart
Normal file
@@ -0,0 +1,289 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:frontend_eccp_mobile/app/core/constants/constants.dart';
|
||||
import 'package:pull_to_refresh_flutter3/pull_to_refresh_flutter3.dart';
|
||||
|
||||
typedef AppAsyncCallback = Future<void> Function();
|
||||
|
||||
class AppScreen extends StatelessWidget {
|
||||
const AppScreen({
|
||||
required this.body,
|
||||
super.key,
|
||||
this.isSliver = false,
|
||||
this.slivers,
|
||||
this.appBar,
|
||||
this.title,
|
||||
this.centerTitle = true,
|
||||
this.leading,
|
||||
this.actions,
|
||||
this.onBack,
|
||||
this.useWillPopScope = false,
|
||||
this.useShadowAppBar = true,
|
||||
this.scrollableBody = true,
|
||||
this.scrollController,
|
||||
this.physics,
|
||||
this.refreshController,
|
||||
this.enablePullDown = false,
|
||||
this.enablePullUp = false,
|
||||
this.onRefresh,
|
||||
this.onLoadMore,
|
||||
this.refreshHeader,
|
||||
this.refreshFooter,
|
||||
this.footers,
|
||||
this.footersPadding = AppPadding.p16,
|
||||
this.hasSafeArea = true,
|
||||
this.safeTop = true,
|
||||
this.safeBottom,
|
||||
this.safeLeft = true,
|
||||
this.safeRight = true,
|
||||
this.backgroundColor,
|
||||
this.backgroundWidgets,
|
||||
this.resizeToAvoidBottomInset,
|
||||
this.floatingActionButton,
|
||||
this.bottomNavigationBar,
|
||||
this.extendBody = false,
|
||||
this.extendBodyBehindAppBar = false,
|
||||
this.statusBarStyle,
|
||||
this.isLoading = false,
|
||||
});
|
||||
final bool isLoading;
|
||||
|
||||
final Widget body;
|
||||
|
||||
final bool isSliver;
|
||||
final List<Widget>? slivers;
|
||||
|
||||
final PreferredSizeWidget? appBar;
|
||||
final bool useShadowAppBar;
|
||||
final String? title;
|
||||
final bool centerTitle;
|
||||
final Widget? leading;
|
||||
final List<Widget>? actions;
|
||||
final VoidCallback? onBack;
|
||||
final bool useWillPopScope;
|
||||
|
||||
final bool scrollableBody;
|
||||
final ScrollController? scrollController;
|
||||
final ScrollPhysics? physics;
|
||||
|
||||
final RefreshController? refreshController;
|
||||
final bool enablePullDown;
|
||||
final bool enablePullUp;
|
||||
final AppAsyncCallback? onRefresh;
|
||||
final AppAsyncCallback? onLoadMore;
|
||||
final Widget? refreshHeader;
|
||||
final Widget? refreshFooter;
|
||||
|
||||
final List<Widget>? footers;
|
||||
final EdgeInsets footersPadding;
|
||||
|
||||
final bool hasSafeArea;
|
||||
final bool safeTop;
|
||||
final bool? safeBottom;
|
||||
final bool safeLeft;
|
||||
final bool safeRight;
|
||||
|
||||
final Color? backgroundColor;
|
||||
final List<Widget>? backgroundWidgets;
|
||||
final bool? resizeToAvoidBottomInset;
|
||||
final Widget? floatingActionButton;
|
||||
final Widget? bottomNavigationBar;
|
||||
final bool extendBody;
|
||||
final bool extendBodyBehindAppBar;
|
||||
|
||||
final SystemUiOverlayStyle? statusBarStyle;
|
||||
|
||||
Widget _wrapScrollbar(BuildContext context, Widget child) {
|
||||
return ScrollbarTheme(
|
||||
data: ScrollbarThemeData(
|
||||
thumbColor: MaterialStateProperty.all(AppColors.neutral400),
|
||||
thickness: MaterialStateProperty.all(3),
|
||||
radius: const Radius.circular(AppSizes.s24),
|
||||
),
|
||||
child: Scrollbar(
|
||||
controller: scrollController,
|
||||
interactive: true,
|
||||
thumbVisibility:
|
||||
MediaQuery.of(context).size.width > AppSizes.maxContentWidth,
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBodyContent(BuildContext context) {
|
||||
Widget content;
|
||||
|
||||
if (isSliver) {
|
||||
content = _wrapScrollbar(
|
||||
context,
|
||||
CustomScrollView(
|
||||
controller: scrollController,
|
||||
physics: physics,
|
||||
slivers: slivers!,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
content = body;
|
||||
|
||||
if (scrollableBody) {
|
||||
content = _wrapScrollbar(
|
||||
context,
|
||||
SingleChildScrollView(
|
||||
controller: scrollController,
|
||||
physics: physics,
|
||||
child: content,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (refreshController != null) {
|
||||
content = SmartRefresher(
|
||||
controller: refreshController!,
|
||||
enablePullDown: enablePullDown,
|
||||
enablePullUp: enablePullUp,
|
||||
onRefresh: onRefresh,
|
||||
onLoading: onLoadMore,
|
||||
header: refreshHeader,
|
||||
footer: refreshFooter,
|
||||
physics: physics,
|
||||
child: content,
|
||||
);
|
||||
}
|
||||
|
||||
if (footers == null) return content;
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
Expanded(child: content),
|
||||
Padding(
|
||||
padding: footersPadding,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: footers!,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBody(BuildContext context) {
|
||||
final content = backgroundWidgets == null
|
||||
? _buildBodyContent(context)
|
||||
: Stack(
|
||||
children: [
|
||||
...backgroundWidgets!,
|
||||
_buildBodyContent(context),
|
||||
],
|
||||
);
|
||||
|
||||
final safeContent = hasSafeArea
|
||||
? SafeArea(
|
||||
top: safeTop,
|
||||
left: safeLeft,
|
||||
right: safeRight,
|
||||
bottom:
|
||||
safeBottom ??
|
||||
(!kIsWeb && defaultTargetPlatform == TargetPlatform.android),
|
||||
child: content,
|
||||
)
|
||||
: content;
|
||||
|
||||
return Stack(
|
||||
children: [
|
||||
safeContent,
|
||||
_buildLoadingOverlay(),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildLoadingOverlay() {
|
||||
return AnimatedOpacity(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
opacity: isLoading ? 1 : 0,
|
||||
child: isLoading
|
||||
? Positioned.fill(
|
||||
child: ColoredBox(
|
||||
color: Colors.black.withOpacity(0.1),
|
||||
child: Center(
|
||||
child: Container(
|
||||
width: 70,
|
||||
height: 70,
|
||||
padding: AppPadding.p16,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.neutralWhite,
|
||||
borderRadius: AppRadius.r10,
|
||||
),
|
||||
child: CircularProgressIndicator(
|
||||
color: AppColors.primary900,
|
||||
strokeWidth: 3,
|
||||
backgroundColor: Colors.transparent,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
: const SizedBox(),
|
||||
);
|
||||
}
|
||||
|
||||
PreferredSizeWidget? _buildAppBar(BuildContext context) {
|
||||
if (appBar != null) return appBar;
|
||||
if (title == null) return null;
|
||||
|
||||
return AppBar(
|
||||
backgroundColor: AppColors.neutralWhite,
|
||||
surfaceTintColor: Colors.transparent,
|
||||
scrolledUnderElevation: useShadowAppBar ? 4 : 0,
|
||||
shadowColor: useShadowAppBar
|
||||
? Colors.black.withOpacity(AppOpacity.opacity15)
|
||||
: Colors.transparent,
|
||||
title: Text(
|
||||
title!,
|
||||
style: AppTextStyles.h3,
|
||||
),
|
||||
centerTitle: centerTitle,
|
||||
leading:
|
||||
leading ??
|
||||
(onBack != null
|
||||
? IconButton(
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
onPressed: onBack,
|
||||
)
|
||||
: null),
|
||||
actions: actions,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final Widget screen = AnnotatedRegion<SystemUiOverlayStyle>(
|
||||
value:
|
||||
statusBarStyle ??
|
||||
SystemUiOverlayStyle.dark.copyWith(
|
||||
statusBarColor: Colors.transparent,
|
||||
),
|
||||
child: Scaffold(
|
||||
backgroundColor: backgroundColor,
|
||||
resizeToAvoidBottomInset: resizeToAvoidBottomInset,
|
||||
appBar: _buildAppBar(context),
|
||||
body: _buildBody(context),
|
||||
floatingActionButton: floatingActionButton,
|
||||
bottomNavigationBar: bottomNavigationBar,
|
||||
extendBody: extendBody,
|
||||
extendBodyBehindAppBar: extendBodyBehindAppBar,
|
||||
),
|
||||
);
|
||||
|
||||
if (!useWillPopScope) return screen;
|
||||
|
||||
return WillPopScope(
|
||||
onWillPop: () async {
|
||||
onBack?.call();
|
||||
return false;
|
||||
},
|
||||
child: screen,
|
||||
);
|
||||
}
|
||||
}
|
||||
22
lib/app/core/widgets/pdf_viewer.dart
Normal file
22
lib/app/core/widgets/pdf_viewer.dart
Normal file
@@ -0,0 +1,22 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:frontend_eccp_mobile/app/core/constants/app_colors.dart';
|
||||
import 'package:frontend_eccp_mobile/app/core/widgets/layout/app_screen.dart';
|
||||
import 'package:syncfusion_flutter_pdfviewer/pdfviewer.dart';
|
||||
|
||||
class PdfViewerPage extends StatelessWidget {
|
||||
const PdfViewerPage({required this.url, super.key});
|
||||
|
||||
final String url;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AppScreen(
|
||||
title: 'Preview File',
|
||||
backgroundColor: AppColors.neutralWhite,
|
||||
scrollableBody: false,
|
||||
body: SfPdfViewer.network(
|
||||
url,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user