Copy more common components

This commit is contained in:
Neeraj Gupta 2023-04-08 17:10:10 +05:30
parent 33b141916f
commit 537b3d2e4d
No known key found for this signature in database
GPG key ID: 3C5A1684DC1729E1
4 changed files with 775 additions and 0 deletions

View file

@ -0,0 +1,526 @@
import 'package:ente_auth/models/execution_states.dart';
import 'package:ente_auth/models/typedefs.dart';
import 'package:ente_auth/theme/colors.dart';
import 'package:ente_auth/theme/ente_theme.dart';
import 'package:ente_auth/theme/text_style.dart';
import 'package:ente_auth/ui/common/loading_widget.dart';
import 'package:ente_auth/ui/components/models/button_result.dart';
import 'package:ente_auth/ui/components/models/button_type.dart';
import 'package:ente_auth/ui/components/models/custom_button_style.dart';
import 'package:ente_auth/utils/debouncer.dart';
import "package:ente_auth/utils/dialog_util.dart";
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
enum ButtonSize { small, large }
enum ButtonAction { first, second, third, fourth, cancel, error }
class ButtonWidget extends StatelessWidget {
final IconData? icon;
final String? labelText;
final ButtonType buttonType;
final FutureVoidCallback? onTap;
final bool isDisabled;
final ButtonSize buttonSize;
///Setting this flag to true will show a success confirmation as a 'check'
///icon once the onTap(). This is expected to be used only if time taken to
///execute onTap() takes less than debouce time.
final bool shouldShowSuccessConfirmation;
///Setting this flag to false will restrict the loading and success states of
///the button from surfacing on the UI. The ExecutionState of the button will
///change irrespective of the value of this flag. Only that it won't be
///surfaced on the UI
final bool shouldSurfaceExecutionStates;
/// iconColor should only be specified when we do not want to honor the default
/// iconColor based on buttonType. Most of the items, default iconColor is what
/// we need unless we want to pop out the icon in a non-primary button type
final Color? iconColor;
///Button action will only work if isInAlert is true
final ButtonAction? buttonAction;
///setting this flag to true will make the button appear like how it would
///on dark theme irrespective of the app's theme.
final bool shouldStickToDarkTheme;
///isInAlert is to dismiss the alert if the action on the button is completed.
///This should be set to true if the alert which uses this button needs to
///return the Button's action.
final bool isInAlert;
/// progressStatus can be used to display information about the action
/// progress when ExecutionState is in Progress.
final ValueNotifier<String>? progressStatus;
const ButtonWidget({
Key? key,
required this.buttonType,
this.buttonSize = ButtonSize.large,
this.icon,
this.labelText,
this.onTap,
this.shouldStickToDarkTheme = false,
this.isDisabled = false,
this.buttonAction,
this.isInAlert = false,
this.iconColor,
this.shouldSurfaceExecutionStates = true,
this.progressStatus,
this.shouldShowSuccessConfirmation = false,
}) : super(key: key);
@override
Widget build(BuildContext context) {
final colorScheme =
shouldStickToDarkTheme ? darkScheme : getEnteColorScheme(context);
final inverseColorScheme = shouldStickToDarkTheme
? lightScheme
: getEnteColorScheme(context, inverse: true);
final textTheme =
shouldStickToDarkTheme ? darkTextTheme : getEnteTextTheme(context);
final inverseTextTheme = shouldStickToDarkTheme
? lightTextTheme
: getEnteTextTheme(context, inverse: true);
final buttonStyle = CustomButtonStyle(
//Dummy default values since we need to keep these properties non-nullable
defaultButtonColor: Colors.transparent,
defaultBorderColor: Colors.transparent,
defaultIconColor: Colors.transparent,
defaultLabelStyle: textTheme.body,
);
buttonStyle.defaultButtonColor = buttonType.defaultButtonColor(colorScheme);
buttonStyle.pressedButtonColor = buttonType.pressedButtonColor(colorScheme);
buttonStyle.disabledButtonColor =
buttonType.disabledButtonColor(colorScheme, buttonSize);
buttonStyle.defaultBorderColor =
buttonType.defaultBorderColor(colorScheme, buttonSize);
buttonStyle.pressedBorderColor = buttonType.pressedBorderColor(
colorScheme: colorScheme,
buttonSize: buttonSize,
);
buttonStyle.disabledBorderColor =
buttonType.disabledBorderColor(colorScheme, buttonSize);
buttonStyle.defaultIconColor = iconColor ??
buttonType.defaultIconColor(
colorScheme: colorScheme,
inverseColorScheme: inverseColorScheme,
);
buttonStyle.pressedIconColor =
buttonType.pressedIconColor(colorScheme, buttonSize);
buttonStyle.disabledIconColor =
buttonType.disabledIconColor(colorScheme, buttonSize);
buttonStyle.defaultLabelStyle = buttonType.defaultLabelStyle(
textTheme: textTheme,
inverseTextTheme: inverseTextTheme,
);
buttonStyle.pressedLabelStyle =
buttonType.pressedLabelStyle(textTheme, colorScheme, buttonSize);
buttonStyle.disabledLabelStyle =
buttonType.disabledLabelStyle(textTheme, colorScheme);
buttonStyle.checkIconColor = buttonType.checkIconColor(colorScheme);
return ButtonChildWidget(
buttonStyle: buttonStyle,
buttonType: buttonType,
isDisabled: isDisabled,
buttonSize: buttonSize,
isInAlert: isInAlert,
onTap: onTap,
labelText: labelText,
icon: icon,
buttonAction: buttonAction,
shouldSurfaceExecutionStates: shouldSurfaceExecutionStates,
progressStatus: progressStatus,
shouldShowSuccessConfirmation: shouldShowSuccessConfirmation,
);
}
}
class ButtonChildWidget extends StatefulWidget {
final CustomButtonStyle buttonStyle;
final FutureVoidCallback? onTap;
final ButtonType buttonType;
final String? labelText;
final IconData? icon;
final bool isDisabled;
final ButtonSize buttonSize;
final ButtonAction? buttonAction;
final bool isInAlert;
final bool shouldSurfaceExecutionStates;
final ValueNotifier<String>? progressStatus;
final bool shouldShowSuccessConfirmation;
const ButtonChildWidget({
Key? key,
required this.buttonStyle,
required this.buttonType,
required this.isDisabled,
required this.buttonSize,
required this.isInAlert,
required this.shouldSurfaceExecutionStates,
required this.shouldShowSuccessConfirmation,
this.progressStatus,
this.onTap,
this.labelText,
this.icon,
this.buttonAction,
}) : super(key: key);
@override
State<ButtonChildWidget> createState() => _ButtonChildWidgetState();
}
class _ButtonChildWidgetState extends State<ButtonChildWidget> {
late Color buttonColor;
late Color borderColor;
late Color iconColor;
late TextStyle labelStyle;
late Color checkIconColor;
late Color loadingIconColor;
ValueNotifier<String>? progressStatus;
///This is used to store the width of the button in idle state (small button)
///to be used as width for the button when the loading/succes states comes.
double? widthOfButton;
final _debouncer = Debouncer(const Duration(milliseconds: 300));
ExecutionState executionState = ExecutionState.idle;
Exception? _exception;
@override
void initState() {
_setButtonTheme();
super.initState();
}
@override
void didUpdateWidget(covariant ButtonChildWidget oldWidget) {
_setButtonTheme();
super.didUpdateWidget(oldWidget);
}
@override
Widget build(BuildContext context) {
if (executionState == ExecutionState.successful) {
Future.delayed(Duration(seconds: widget.isInAlert ? 1 : 2), () {
setState(() {
executionState = ExecutionState.idle;
});
});
}
return GestureDetector(
onTap: _shouldRegisterGestures ? _onTap : null,
onTapDown: _shouldRegisterGestures ? _onTapDown : null,
onTapUp: _shouldRegisterGestures ? _onTapUp : null,
onTapCancel: _shouldRegisterGestures ? _onTapCancel : null,
child: Container(
decoration: BoxDecoration(
borderRadius: const BorderRadius.all(Radius.circular(4)),
border: widget.buttonType == ButtonType.tertiaryCritical
? Border.all(color: borderColor)
: null,
),
child: AnimatedContainer(
duration: const Duration(milliseconds: 16),
width: widget.buttonSize == ButtonSize.large ? double.infinity : null,
decoration: BoxDecoration(
borderRadius: const BorderRadius.all(Radius.circular(4)),
color: buttonColor,
),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 14, horizontal: 16),
child: AnimatedSwitcher(
duration: const Duration(milliseconds: 175),
switchInCurve: Curves.easeInOutExpo,
switchOutCurve: Curves.easeInOutExpo,
child: executionState == ExecutionState.idle ||
!widget.shouldSurfaceExecutionStates
? widget.buttonType.hasTrailingIcon
? Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
widget.labelText == null
? const SizedBox.shrink()
: Flexible(
child: Padding(
padding: widget.icon == null
? const EdgeInsets.symmetric(
horizontal: 8,
)
: const EdgeInsets.only(right: 16),
child: Text(
widget.labelText!,
overflow: TextOverflow.ellipsis,
maxLines: 2,
style: labelStyle,
),
),
),
widget.icon == null
? const SizedBox.shrink()
: Icon(
widget.icon,
size: 20,
color: iconColor,
),
],
)
: Builder(
builder: (context) {
SchedulerBinding.instance.addPostFrameCallback(
(timeStamp) {
final box =
context.findRenderObject() as RenderBox;
widthOfButton = box.size.width;
},
);
return Row(
mainAxisSize:
widget.buttonSize == ButtonSize.large
? MainAxisSize.max
: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
widget.icon == null
? const SizedBox.shrink()
: Icon(
widget.icon,
size: 20,
color: iconColor,
),
widget.icon == null || widget.labelText == null
? const SizedBox.shrink()
: const SizedBox(width: 8),
widget.labelText == null
? const SizedBox.shrink()
: Flexible(
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 8,
),
child: Text(
widget.labelText!,
style: labelStyle,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
),
)
],
);
},
)
: executionState == ExecutionState.inProgress
? SizedBox(
width: widthOfButton,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
progressStatus == null
? const SizedBox.shrink()
: ValueListenableBuilder<String>(
valueListenable: progressStatus!,
builder: (
BuildContext context,
String value,
Widget? child,
) {
return Padding(
padding:
const EdgeInsets.only(right: 8.0),
child: Text(
value,
style: lightTextTheme.smallBold,
),
);
},
),
EnteLoadingWidget(
padding: 3,
color: loadingIconColor,
),
],
),
)
: executionState == ExecutionState.successful
? SizedBox(
width: widthOfButton,
child: Icon(
Icons.check_outlined,
size: 20,
color: checkIconColor,
),
)
: const SizedBox.shrink(), //fallback
),
),
),
),
);
}
void _setButtonTheme() {
progressStatus = widget.progressStatus;
checkIconColor = widget.buttonStyle.checkIconColor ??
widget.buttonStyle.defaultIconColor;
loadingIconColor = widget.buttonStyle.defaultIconColor;
if (widget.isDisabled) {
buttonColor = widget.buttonStyle.disabledButtonColor ??
widget.buttonStyle.defaultButtonColor;
borderColor = widget.buttonStyle.disabledBorderColor ??
widget.buttonStyle.defaultBorderColor;
iconColor = widget.buttonStyle.disabledIconColor ??
widget.buttonStyle.defaultIconColor;
labelStyle = widget.buttonStyle.disabledLabelStyle ??
widget.buttonStyle.defaultLabelStyle;
} else {
buttonColor = widget.buttonStyle.defaultButtonColor;
borderColor = widget.buttonStyle.defaultBorderColor;
iconColor = widget.buttonStyle.defaultIconColor;
labelStyle = widget.buttonStyle.defaultLabelStyle;
}
}
bool get _shouldRegisterGestures =>
!widget.isDisabled && executionState == ExecutionState.idle;
void _onTap() async {
if (widget.onTap != null) {
_debouncer.run(
() => Future(() {
setState(() {
executionState = ExecutionState.inProgress;
});
}),
);
await widget.onTap!.call().then(
(value) {
_exception = null;
},
onError: (error, stackTrace) {
executionState = ExecutionState.error;
_exception = error as Exception;
_debouncer.cancelDebounce();
},
);
widget.shouldShowSuccessConfirmation && _debouncer.isActive()
? executionState = ExecutionState.successful
: null;
_debouncer.cancelDebounce();
if (executionState == ExecutionState.successful) {
setState(() {});
}
// when the time taken by widget.onTap is approximately equal to the debounce
// time, the callback is getting executed when/after the if condition
// below is executing/executed which results in execution state stuck at
// idle state. This Future is for delaying the execution of the if
// condition so that the calback in the debouncer finishes execution before.
await Future.delayed(const Duration(milliseconds: 5));
}
if (executionState == ExecutionState.inProgress ||
executionState == ExecutionState.error) {
if (executionState == ExecutionState.inProgress) {
if (mounted) {
setState(() {
executionState = ExecutionState.successful;
Future.delayed(
Duration(
seconds: widget.shouldSurfaceExecutionStates
? (widget.isInAlert ? 1 : 2)
: 0,
), () {
widget.isInAlert
? _popWithButtonAction(
context,
buttonAction: widget.buttonAction,
)
: null;
if (mounted) {
setState(() {
executionState = ExecutionState.idle;
});
}
});
});
}
}
if (executionState == ExecutionState.error) {
setState(() {
executionState = ExecutionState.idle;
widget.isInAlert
? Future.delayed(
const Duration(seconds: 0),
() => _popWithButtonAction(
context,
buttonAction: ButtonAction.error,
exception: _exception,
),
)
: null;
});
}
} else {
if (widget.isInAlert) {
Future.delayed(
Duration(seconds: widget.shouldShowSuccessConfirmation ? 1 : 0),
() =>
_popWithButtonAction(context, buttonAction: widget.buttonAction),
);
}
}
}
void _popWithButtonAction(
BuildContext context, {
required ButtonAction? buttonAction,
Exception? exception,
}) {
if (Navigator.of(context).canPop()) {
Navigator.of(context).pop(ButtonResult(widget.buttonAction, exception));
} else if (exception != null) {
//This is to show the execution was unsuccessful if the dialog is manually
//closed before the execution completes.
showGenericErrorDialog(context: context);
}
}
void _onTapDown(details) {
setState(() {
buttonColor = widget.buttonStyle.pressedButtonColor ??
widget.buttonStyle.defaultButtonColor;
borderColor = widget.buttonStyle.pressedBorderColor ??
widget.buttonStyle.defaultBorderColor;
iconColor = widget.buttonStyle.pressedIconColor ??
widget.buttonStyle.defaultIconColor;
labelStyle = widget.buttonStyle.pressedLabelStyle ??
widget.buttonStyle.defaultLabelStyle;
});
}
void _onTapUp(details) {
Future.delayed(
const Duration(milliseconds: 84),
() => setState(() {
setAllStylesToDefault();
}),
);
}
void _onTapCancel() {
setState(() {
setAllStylesToDefault();
});
}
void setAllStylesToDefault() {
buttonColor = widget.buttonStyle.defaultButtonColor;
borderColor = widget.buttonStyle.defaultBorderColor;
iconColor = widget.buttonStyle.defaultIconColor;
labelStyle = widget.buttonStyle.defaultLabelStyle;
}
}

View file

@ -0,0 +1,11 @@
import 'package:ente_auth/ui/components/buttons/button_widget.dart';
class ButtonResult {
///action can be null when action for the button that is returned when popping
///the widget (dialog, actionSheet) which uses a ButtonWidget isn't
///relevant/useful and so is not assigned a value when an instance of
///ButtonWidget is created.
final ButtonAction? action;
final Exception? exception;
ButtonResult([this.action, this.exception]);
}

View file

@ -0,0 +1,205 @@
import 'package:ente_auth/theme/colors.dart';
import 'package:ente_auth/theme/text_style.dart';
import 'package:ente_auth/ui/components/buttons/button_widget.dart';
import 'package:flutter/material.dart';
enum ButtonType {
primary,
secondary,
neutral,
trailingIcon,
critical,
tertiaryCritical,
trailingIconPrimary,
trailingIconSecondary,
tertiary;
bool get isPrimary =>
this == ButtonType.primary || this == ButtonType.trailingIconPrimary;
bool get hasTrailingIcon =>
this == ButtonType.trailingIcon ||
this == ButtonType.trailingIconPrimary ||
this == ButtonType.trailingIconSecondary;
bool get isSecondary =>
this == ButtonType.secondary || this == ButtonType.trailingIconSecondary;
bool get isCritical =>
this == ButtonType.critical || this == ButtonType.tertiaryCritical;
bool get isNeutral =>
this == ButtonType.neutral || this == ButtonType.trailingIcon;
Color defaultButtonColor(EnteColorScheme colorScheme) {
if (isPrimary) {
return colorScheme.primary500;
}
if (isSecondary) {
return colorScheme.fillFaint;
}
if (this == ButtonType.neutral || this == ButtonType.trailingIcon) {
return colorScheme.fillBase;
}
if (this == ButtonType.critical) {
return colorScheme.warning700;
}
if (this == ButtonType.tertiaryCritical) {
return Colors.transparent;
}
return Colors.transparent;
}
//Returning null to fallback to default color
Color? pressedButtonColor(EnteColorScheme colorScheme) {
if (isPrimary) {
return colorScheme.primary700;
}
if (isSecondary) {
return colorScheme.fillFaintPressed;
}
if (isNeutral) {
return colorScheme.fillBasePressed;
}
if (this == ButtonType.critical) {
return colorScheme.warning800;
}
return null;
}
//Returning null to fallback to default color
Color? disabledButtonColor(
EnteColorScheme colorScheme,
ButtonSize buttonSize,
) {
if (buttonSize == ButtonSize.small &&
(this == ButtonType.primary ||
this == ButtonType.neutral ||
this == ButtonType.critical)) {
return colorScheme.fillMuted;
}
if (isPrimary || this == ButtonType.critical || isNeutral) {
return colorScheme.fillFaint;
}
return null;
}
Color defaultBorderColor(EnteColorScheme colorScheme, ButtonSize buttonSize) {
if (this == ButtonType.tertiaryCritical && buttonSize == ButtonSize.large) {
return colorScheme.warning700;
}
return Colors.transparent;
}
//Returning null to fallback to default color
Color? pressedBorderColor({
required EnteColorScheme colorScheme,
required ButtonSize buttonSize,
}) {
if (this == ButtonType.tertiaryCritical && buttonSize == ButtonSize.large) {
return colorScheme.warning700;
}
return null;
}
//Returning null to fallback to default color
Color? disabledBorderColor(
EnteColorScheme colorScheme,
ButtonSize buttonSize,
) {
if (this == ButtonType.tertiaryCritical && buttonSize == ButtonSize.large) {
return colorScheme.strokeMuted;
}
return null;
}
Color defaultIconColor({
required EnteColorScheme colorScheme,
required EnteColorScheme inverseColorScheme,
}) {
if (isPrimary || this == ButtonType.critical) {
return strokeBaseDark;
}
if (this == ButtonType.neutral || this == ButtonType.trailingIcon) {
return inverseColorScheme.strokeBase;
}
if (this == ButtonType.tertiaryCritical) {
return colorScheme.warning500;
}
//fallback
return colorScheme.strokeBase;
}
//Returning null to fallback to default color
Color? pressedIconColor(EnteColorScheme colorScheme, ButtonSize buttonSize) {
if (this == ButtonType.tertiaryCritical) {
return colorScheme.warning700;
}
if (this == ButtonType.tertiary && buttonSize == ButtonSize.small) {
return colorScheme.fillBasePressed;
}
return null;
}
//Returning null to fallback to default color
Color? disabledIconColor(EnteColorScheme colorScheme, ButtonSize buttonSize) {
if (isPrimary ||
isSecondary ||
isNeutral ||
buttonSize == ButtonSize.small) {
return colorScheme.strokeMuted;
}
if (isCritical) {
return colorScheme.strokeFaint;
}
return null;
}
TextStyle defaultLabelStyle({
required EnteTextTheme textTheme,
required EnteTextTheme inverseTextTheme,
}) {
if (isPrimary || this == ButtonType.critical) {
return textTheme.bodyBold.copyWith(color: textBaseDark);
}
if (this == ButtonType.neutral || this == ButtonType.trailingIcon) {
return inverseTextTheme.bodyBold;
}
if (this == ButtonType.tertiaryCritical) {
return textTheme.bodyBold.copyWith(color: warning500);
}
//fallback
return textTheme.bodyBold;
}
//Returning null to fallback to default color
TextStyle? pressedLabelStyle(
EnteTextTheme textTheme,
EnteColorScheme colorScheme,
ButtonSize buttonSize,
) {
if (this == ButtonType.tertiaryCritical) {
return textTheme.bodyBold.copyWith(color: colorScheme.warning700);
}
if (this == ButtonType.tertiary && buttonSize == ButtonSize.small) {
return textTheme.bodyBold.copyWith(color: colorScheme.fillBasePressed);
}
return null;
}
//Returning null to fallback to default color
TextStyle? disabledLabelStyle(
EnteTextTheme textTheme,
EnteColorScheme colorScheme,
) {
return textTheme.bodyBold.copyWith(color: colorScheme.textFaint);
}
//Returning null to fallback to default color
Color? checkIconColor(EnteColorScheme colorScheme) {
if (isSecondary) {
return colorScheme.primary500;
}
return null;
}
}

View file

@ -0,0 +1,33 @@
import 'package:flutter/material.dart';
class CustomButtonStyle {
Color defaultButtonColor;
Color? pressedButtonColor;
Color? disabledButtonColor;
Color defaultBorderColor;
Color? pressedBorderColor;
Color? disabledBorderColor;
Color defaultIconColor;
Color? pressedIconColor;
Color? disabledIconColor;
TextStyle defaultLabelStyle;
TextStyle? pressedLabelStyle;
TextStyle? disabledLabelStyle;
Color? checkIconColor;
CustomButtonStyle({
required this.defaultButtonColor,
this.pressedButtonColor,
this.disabledButtonColor,
required this.defaultBorderColor,
this.pressedBorderColor,
this.disabledBorderColor,
required this.defaultIconColor,
this.pressedIconColor,
this.disabledIconColor,
required this.defaultLabelStyle,
this.pressedLabelStyle,
this.disabledLabelStyle,
this.checkIconColor,
});
}