123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540 |
- import 'package:flutter/material.dart';
- import 'package:flutter/scheduler.dart';
- import "package:photos/models/search/button_result.dart";
- import 'package:photos/theme/colors.dart';
- import 'package:photos/theme/ente_theme.dart';
- import 'package:photos/theme/text_style.dart';
- import 'package:photos/ui/common/loading_widget.dart';
- import 'package:photos/ui/components/models/button_type.dart';
- import 'package:photos/ui/components/models/custom_button_style.dart';
- import 'package:photos/utils/debouncer.dart';
- enum ExecutionState {
- idle,
- inProgress,
- error,
- successful;
- }
- enum ButtonSize {
- small,
- large;
- }
- enum ButtonAction {
- first,
- second,
- third,
- fourth,
- cancel,
- error;
- }
- typedef FutureVoidCallback = Future<void> Function();
- 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({
- 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,
- });
- @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({
- 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,
- });
- @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(
- is20pts: true,
- 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,
- }) {
- Navigator.of(context).canPop()
- ? Navigator.of(context).pop(
- ButtonResult(action: buttonAction, exception: exception),
- )
- : null;
- }
- 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;
- }
- }
|