feat(ui_kit): implement file selection button with invalid cubit and dotted package

This commit is contained in:
Hugo Pointcheval 2023-02-14 11:54:32 +01:00
parent d49a4cad31
commit 4695cf0618
Signed by: hugo
GPG Key ID: 3AAC487E131E00BC
9 changed files with 670 additions and 104 deletions

View File

@ -30,6 +30,7 @@ const _background = Color(0xFF16191D);
const _disabledBackground = Color(0xFF16191D + 0x66FFFFFF);
const _disabledColors = [Color(0xFF60656A), Color(0xFF383C40)];
const _selectedColors = [Color(0xFF50CE99), Color(0xFF339572)];
const _invalidColor = Color(0xFFFB5E3C);
class Buttons extends StatelessWidget {
const Buttons({super.key});
@ -325,6 +326,47 @@ class Buttons extends StatelessWidget {
),
],
),
const Gap(20),
FileSelectionButton(
leading: const DecoratedBox(
decoration: BoxDecoration(
color: _disabled,
borderRadius: BorderRadius.all(
Radius.circular(5),
),
),
child: Padding(
padding: EdgeInsets.all(8),
child: Icon(
Icons.file_upload_outlined,
color: Colors.white,
),
),
),
title: TextWrapper(
'Ajouter un fichier',
style: Theme.of(context)
.textTheme
.titleLarge
?.copyWith(color: _disabled),
),
subTitle: TextWrapper.text('Taille max: 20 Mo'),
normalStyle: const FileSelectionButtonStyle(
backgroundColors: MultiColor.single(_disabledBackground),
foregroundColors: MultiColor.single(_disabled),
borderColors: MultiColor.single(_disabled),
),
hoveredStyle: const FileSelectionButtonStyle(
backgroundColors: MultiColor.single(_disabledBackground),
foregroundColors: MultiColor.single(_disabled),
borderColors: MultiColor.single(Colors.white),
),
invalidStyle: const FileSelectionButtonStyle(
backgroundColors: MultiColor.single(_disabledBackground),
foregroundColors: MultiColor.single(_invalidColor),
borderColors: MultiColor.single(_invalidColor),
),
)
],
),
),

View File

@ -14,6 +14,7 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
export './file_selection_button/file_selection_button.dart';
export './flat_button/flat_button.dart';
export './simple_icon_button/simple_icon_button.dart';
export './symbol_button/symbol_button.dart';

View File

@ -0,0 +1,73 @@
// Copyright (C) 2023 WYATT GROUP
// Please see the AUTHORS file for details.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
import 'dart:async';
import 'dart:math';
import 'package:wyatt_ui_components/wyatt_wyatt_ui_components.dart';
import 'package:wyatt_ui_kit/src/components/buttons/cubit/button_cubit.dart';
class InvalidButtonCubit extends ButtonCubit {
@override
FutureOr<void> onClickUpIn() async {
if (state.isDisabled || state.isFreezed) {
return;
}
emit(
state.copyWith(
state: ControlState.hovered,
// TODO(hpcl): change this
invalid: Random().nextBool(),
),
);
}
@override
FutureOr<void> onClickUpOut() async {
if (state.isDisabled || state.isFreezed) {
return;
}
emit(
state.copyWith(
state: ControlState.normal,
// TODO(hpcl): change this
invalid: Random().nextBool(),
),
);
}
FutureOr<void> invalidate() async {
if (state.isDisabled || state.isFreezed) {
return;
}
emit(
state.copyWith(invalid: true),
);
}
FutureOr<void> fix() async {
if (state.isDisabled || state.isFreezed) {
return;
}
emit(
state.copyWith(invalid: false),
);
}
}

View File

@ -37,4 +37,24 @@ class SelectableButtonCubit extends ButtonCubit {
}
emit(state.copyWith(state: ControlState.normal, selected: !state.selected));
}
FutureOr<void> select() async {
if (state.isDisabled || state.isFreezed) {
return;
}
emit(
state.copyWith(selected: true),
);
}
FutureOr<void> unselect() async {
if (state.isDisabled || state.isFreezed) {
return;
}
emit(
state.copyWith(selected: false),
);
}
}

View File

@ -0,0 +1,96 @@
// Copyright (C) 2023 WYATT GROUP
// Please see the AUTHORS file for details.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
import 'package:flutter/material.dart' hide ButtonStyle;
import 'package:wyatt_component_copy_with_extension/component_copy_with_extension.dart';
import 'package:wyatt_ui_components/wyatt_wyatt_ui_components.dart';
import 'package:wyatt_ui_kit/src/components/buttons/cubit/invalid_button_cubit.dart';
import 'package:wyatt_ui_kit/src/components/buttons/file_selection_button/file_selection_button_screen.dart';
import 'package:wyatt_ui_kit/src/core/mixin/export_bloc_mixin.dart';
part 'file_selection_button.g.dart';
@ComponentCopyWithExtension()
class FileSelectionButton extends FileSelectionButtonComponent
with $FileSelectionButtonCWMixin, ExportBloc<InvalidButtonCubit> {
FileSelectionButton({
super.leading,
super.title,
super.subTitle,
super.disabledStyle,
super.normalStyle,
super.hoveredStyle,
super.focusedStyle,
super.tappedStyle,
super.selectedStyle,
super.invalidStyle,
super.onPressed,
super.mainAxisSize,
super.key,
});
final InvalidButtonCubit _cubit = InvalidButtonCubit();
@override
InvalidButtonCubit get bloc => _cubit;
@override
FileSelectionButtonStyle? get disabledStyle =>
super.disabledStyle as FileSelectionButtonStyle?;
@override
FileSelectionButtonStyle? get normalStyle =>
super.normalStyle as FileSelectionButtonStyle?;
@override
FileSelectionButtonStyle? get hoveredStyle =>
super.hoveredStyle as FileSelectionButtonStyle?;
@override
FileSelectionButtonStyle? get focusedStyle =>
super.focusedStyle as FileSelectionButtonStyle?;
@override
FileSelectionButtonStyle? get tappedStyle =>
super.tappedStyle as FileSelectionButtonStyle?;
@override
FileSelectionButtonStyle? get selectedStyle =>
super.selectedStyle as FileSelectionButtonStyle?;
@override
FileSelectionButtonStyle? get invalidStyle =>
super.invalidStyle as FileSelectionButtonStyle?;
@override
Widget build(BuildContext context) => exportBloc(
child: FileSelectionButtonScreen(
leading: leading,
title: title,
subTitle: subTitle,
disabledStyle: disabledStyle,
normalStyle: normalStyle,
hoveredStyle: hoveredStyle,
focusedStyle: focusedStyle,
tappedStyle: tappedStyle,
selectedStyle: selectedStyle,
invalidStyle: invalidStyle,
onPressed: onPressed,
mainAxisSize: mainAxisSize,
key: key,
),
);
}

View File

@ -0,0 +1,85 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'file_selection_button.dart';
// **************************************************************************
// ComponentCopyWithGenerator
// **************************************************************************
class $FileSelectionButtonCWProxyImpl
implements $FileSelectionButtonComponentCWProxy {
const $FileSelectionButtonCWProxyImpl(this._value);
final FileSelectionButton _value;
@override
FileSelectionButton mainAxisSize(MainAxisSize? mainAxisSize) =>
this(mainAxisSize: mainAxisSize);
@override
FileSelectionButton leading(Widget? leading) => this(leading: leading);
@override
FileSelectionButton title(TextWrapper? title) => this(title: title);
@override
FileSelectionButton subTitle(TextWrapper? subTitle) =>
this(subTitle: subTitle);
@override
FileSelectionButton disabledStyle(ButtonStyle? disabledStyle) =>
this(disabledStyle: disabledStyle);
@override
FileSelectionButton normalStyle(ButtonStyle? normalStyle) =>
this(normalStyle: normalStyle);
@override
FileSelectionButton hoveredStyle(ButtonStyle? hoveredStyle) =>
this(hoveredStyle: hoveredStyle);
@override
FileSelectionButton focusedStyle(ButtonStyle? focusedStyle) =>
this(focusedStyle: focusedStyle);
@override
FileSelectionButton tappedStyle(ButtonStyle? tappedStyle) =>
this(tappedStyle: tappedStyle);
@override
FileSelectionButton selectedStyle(ButtonStyle? selectedStyle) =>
this(selectedStyle: selectedStyle);
@override
FileSelectionButton invalidStyle(ButtonStyle? invalidStyle) =>
this(invalidStyle: invalidStyle);
@override
FileSelectionButton onPressed(void Function(ControlState)? onPressed) =>
this(onPressed: onPressed);
@override
FileSelectionButton key(Key? key) => this(key: key);
@override
FileSelectionButton call({
MainAxisSize? mainAxisSize,
Widget? leading,
TextWrapper? title,
TextWrapper? subTitle,
ButtonStyle? disabledStyle,
ButtonStyle? normalStyle,
ButtonStyle? hoveredStyle,
ButtonStyle? focusedStyle,
ButtonStyle? tappedStyle,
ButtonStyle? selectedStyle,
ButtonStyle? invalidStyle,
void Function(ControlState)? onPressed,
Key? key,
}) =>
FileSelectionButton(
leading: leading ?? _value.leading,
title: title ?? _value.title,
subTitle: subTitle ?? _value.subTitle,
disabledStyle: disabledStyle ?? _value.disabledStyle,
normalStyle: normalStyle ?? _value.normalStyle,
hoveredStyle: hoveredStyle ?? _value.hoveredStyle,
focusedStyle: focusedStyle ?? _value.focusedStyle,
tappedStyle: tappedStyle ?? _value.tappedStyle,
selectedStyle: selectedStyle ?? _value.selectedStyle,
invalidStyle: invalidStyle ?? _value.invalidStyle,
onPressed: onPressed ?? _value.onPressed,
mainAxisSize: mainAxisSize ?? _value.mainAxisSize,
key: key ?? _value.key,
);
}
mixin $FileSelectionButtonCWMixin on Component {
$FileSelectionButtonComponentCWProxy get copyWith =>
$FileSelectionButtonCWProxyImpl(this as FileSelectionButton);
}

View File

@ -0,0 +1,251 @@
// Copyright (C) 2023 WYATT GROUP
// Please see the AUTHORS file for details.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
import 'package:dotted_border/dotted_border.dart';
import 'package:flutter/material.dart' hide ButtonStyle;
import 'package:flutter/services.dart';
import 'package:gap/gap.dart';
import 'package:wyatt_bloc_helper/wyatt_bloc_helper.dart';
import 'package:wyatt_ui_components/wyatt_wyatt_ui_components.dart';
import 'package:wyatt_ui_kit/src/components/buttons/cubit/button_cubit.dart';
import 'package:wyatt_ui_kit/src/components/buttons/cubit/invalid_button_cubit.dart';
import 'package:wyatt_ui_kit/src/components/gradients/gradient_text.dart';
import 'package:wyatt_ui_kit/src/core/extensions/theme_extensions.dart';
import 'package:wyatt_ui_kit/src/core/helpers/linear_gradient_helper.dart';
class FileSelectionButtonScreen
extends CubitScreen<InvalidButtonCubit, ButtonState> {
const FileSelectionButtonScreen({
this.leading,
this.title,
this.subTitle,
this.disabledStyle,
this.normalStyle,
this.hoveredStyle,
this.focusedStyle,
this.tappedStyle,
this.selectedStyle,
this.invalidStyle,
this.onPressed,
this.mainAxisSize,
super.key,
});
final Widget? leading;
final TextWrapper? title;
final TextWrapper? subTitle;
final FileSelectionButtonStyle? disabledStyle;
final FileSelectionButtonStyle? normalStyle;
final FileSelectionButtonStyle? hoveredStyle;
final FileSelectionButtonStyle? focusedStyle;
final FileSelectionButtonStyle? tappedStyle;
final FileSelectionButtonStyle? selectedStyle;
final FileSelectionButtonStyle? invalidStyle;
final void Function(ControlState state)? onPressed;
final MainAxisSize? mainAxisSize;
@override
InvalidButtonCubit create(BuildContext context) => InvalidButtonCubit();
@override
Widget onBuild(BuildContext context, ButtonState state) {
// Set a default style
FileSelectionButtonStyle? style =
normalStyle ?? const FileSelectionButtonStyle();
switch (state.state) {
case ControlState.disabled:
style = disabledStyle ?? style;
break;
case ControlState.hovered:
style = hoveredStyle ?? style;
break;
case ControlState.tapped:
style = tappedStyle ?? style;
break;
case ControlState.focused:
style = focusedStyle ?? style;
break;
case ControlState.normal:
// already done
break;
}
if (state.isSelected) {
style = selectedStyle ?? style;
}
if (state.isInvalid) {
style = invalidStyle ?? style;
}
print(state);
return Focus(
onFocusChange: (hasFocus) =>
hasFocus ? bloc(context).onFocus() : bloc(context).onUnfocus(),
onKeyEvent: (focusNode, event) {
if (event.logicalKey == LogicalKeyboardKey.enter && state.isFocused) {
onPressed?.call(state.state);
bloc(context).onClickUpOut();
return KeyEventResult.handled;
}
return KeyEventResult.ignored;
},
child: MouseRegion(
cursor: SystemMouseCursors.click,
onEnter: (event) {
bloc(context).onMouseEnter();
},
onExit: (event) {
bloc(context).onMouseLeave();
},
child: GestureDetector(
onTapDown: (details) {
bloc(context).onClickDown();
},
onTapUp: (details) {
onPressed?.call(state.state);
bloc(context).onClickUpIn();
},
onTapCancel: () {
onPressed?.call(state.state);
bloc(context).onClickUpOut();
},
child: DottedBorder(
padding: EdgeInsets.zero,
dashPattern: const [5, 5],
strokeWidth: style.stroke ?? 3,
color: style.borderColors?.color ?? context.colorScheme.primary,
borderType: BorderType.RRect,
radius: Radius.circular(style.radius ?? 0),
strokeCap: StrokeCap.square,
child: DecoratedBox(
decoration: BoxDecoration(
color: style.backgroundColors?.color ??
context.colorScheme.primary,
// if no gradient colors => no default value
gradient: (style.backgroundColors?.isGradient ?? false)
? LinearGradient(
colors: style.backgroundColors!.colors,
)
: null,
boxShadow: [
if (style.shadow != null) ...[style.shadow!]
],
borderRadius: BorderRadius.all(
Radius.circular(style.radius ?? 0),
),
),
child: ConstrainedBox(
constraints: const BoxConstraints(
minWidth: 200,
minHeight: 50,
), // min sizes for Material buttons
child: Padding(
padding: EdgeInsets.all(style.padding ?? 0),
child: Row(
mainAxisSize: mainAxisSize ?? MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
if (leading != null) ...[
leading ?? const SizedBox.shrink(),
Gap(style.padding ?? 10),
],
// Choose color
// label.style.color ??
// buttonStyle.foregroundColor.color ??
// context.textTheme.titleLarge.color
//
// Choose gradient
// label.gradient ??
// buttonStyle.foregroundColor.colors ??
// null
Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (title != null) ...[
Builder(
builder: (context) {
final color = title?.style?.color ??
style?.foregroundColors?.color ??
context.textTheme.titleLarge?.color;
final buttonStyleGradient =
(style?.foregroundColors?.isGradient ??
false)
? style?.foregroundColors?.colors
: null;
final gradient =
title?.gradient ?? buttonStyleGradient;
return Text(
title!.text,
style: (title!.style ??
context.textTheme.titleLarge)
?.copyWith(color: color),
).toGradient(
LinearGradientHelper.fromNullableColors(
gradient,
),
);
},
),
],
if (subTitle != null) ...[
Builder(
builder: (context) {
final color = subTitle?.style?.color ??
style?.foregroundColors?.color ??
context.textTheme.bodyMedium?.color;
final buttonStyleGradient =
(style?.foregroundColors?.isGradient ??
false)
? style?.foregroundColors?.colors
: null;
final gradient =
subTitle?.gradient ?? buttonStyleGradient;
return Text(
subTitle!.text,
style: (subTitle!.style ??
context.textTheme.bodyMedium)
?.copyWith(color: color),
).toGradient(
LinearGradientHelper.fromNullableColors(
gradient,
),
);
},
),
],
],
),
],
),
),
),
),
),
),
),
);
}
}

View File

@ -111,117 +111,114 @@ class FlatButtonScreen extends CubitScreen<ButtonCubit, ButtonState> {
onPressed?.call(state.state);
bloc(context).onClickUpOut();
},
child: Material(
color: const Color(0x00000000),
child: Ink(
decoration: BoxDecoration(
color: style.backgroundColors?.color ??
context.colorScheme.primary,
// If no border color => no default value
border: (style.borderColors != null)
? (style.borderColors?.isGradient ?? false)
? GradientBoxBorder(
gradient: LinearGradient(
colors: style.borderColors!.colors,
),
width: style.stroke ?? 2,
)
: Border.all(
color: style.borderColors!.color,
width: style.stroke ?? 2,
)
: null,
// if no gradient colors => no default value
gradient: (style.backgroundColors?.isGradient ?? false)
? LinearGradient(
colors: style.backgroundColors!.colors,
)
: null,
boxShadow: [
if (style.shadow != null) ...[style.shadow!]
],
borderRadius: BorderRadius.all(
Radius.circular(style.radius ?? 0),
),
child: DecoratedBox(
decoration: BoxDecoration(
color: style.backgroundColors?.color ??
context.colorScheme.primary,
// If no border color => no default value
border: (style.borderColors != null)
? (style.borderColors?.isGradient ?? false)
? GradientBoxBorder(
gradient: LinearGradient(
colors: style.borderColors!.colors,
),
width: style.stroke ?? 2,
)
: Border.all(
color: style.borderColors!.color,
width: style.stroke ?? 2,
)
: null,
// if no gradient colors => no default value
gradient: (style.backgroundColors?.isGradient ?? false)
? LinearGradient(
colors: style.backgroundColors!.colors,
)
: null,
boxShadow: [
if (style.shadow != null) ...[style.shadow!]
],
borderRadius: BorderRadius.all(
Radius.circular(style.radius ?? 0),
),
child: ConstrainedBox(
constraints: const BoxConstraints(
minWidth: 88,
minHeight: 36,
), // min sizes for Material buttons
child: Padding(
padding: EdgeInsets.all(style.padding ?? 0),
child: Row(
mainAxisSize: mainAxisSize ?? MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
),
child: ConstrainedBox(
constraints: const BoxConstraints(
minWidth: 88,
minHeight: 36,
), // min sizes for Material buttons
child: Padding(
padding: EdgeInsets.all(style.padding ?? 0),
child: Row(
mainAxisSize: mainAxisSize ?? MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Builder(
builder: (context) {
final color = style?.foregroundColors?.color;
if (color != null) {
return ColorFiltered(
colorFilter:
ColorFilter.mode(color, BlendMode.srcIn),
child: prefix ?? const SizedBox.shrink(),
);
} else {
return prefix ?? const SizedBox.shrink();
}
},
),
Gap(style.padding ?? 10),
// Choose color
// label.style.color ??
// buttonStyle.foregroundColor.color ??
// context.textTheme.titleLarge.color
//
// Choose gradient
// label.gradient ??
// buttonStyle.foregroundColor.colors ??
// null
if (label != null) ...[
Builder(
builder: (context) {
final color = style?.foregroundColors?.color;
if (color != null) {
return ColorFiltered(
colorFilter:
ColorFilter.mode(color, BlendMode.srcIn),
child: prefix ?? const SizedBox.shrink(),
);
} else {
return prefix ?? const SizedBox.shrink();
}
},
),
Gap(style.padding ?? 10),
// Choose color
// label.style.color ??
// buttonStyle.foregroundColor.color ??
// context.textTheme.titleLarge.color
//
// Choose gradient
// label.gradient ??
// buttonStyle.foregroundColor.colors ??
// null
if (label != null) ...[
Builder(
builder: (context) {
final color = label?.style?.color ??
style?.foregroundColors?.color ??
context.textTheme.titleLarge?.color;
final buttonStyleGradient =
(style?.foregroundColors?.isGradient ?? false)
? style?.foregroundColors?.colors
: null;
final gradient =
label?.gradient ?? buttonStyleGradient;
final color = label?.style?.color ??
style?.foregroundColors?.color ??
context.textTheme.titleLarge?.color;
final buttonStyleGradient =
(style?.foregroundColors?.isGradient ?? false)
? style?.foregroundColors?.colors
: null;
final gradient =
label?.gradient ?? buttonStyleGradient;
return Text(
label!.text,
style:
(label!.style ?? context.textTheme.titleLarge)
?.copyWith(color: color),
).toGradient(
LinearGradientHelper.fromNullableColors(
gradient,
),
);
},
),
],
Gap(style.padding ?? 10),
Builder(
builder: (context) {
final color = style?.foregroundColors?.color;
if (color != null) {
return ColorFiltered(
colorFilter:
ColorFilter.mode(color, BlendMode.srcIn),
child: suffix ?? const SizedBox.shrink(),
);
} else {
return suffix ?? const SizedBox.shrink();
}
return Text(
label!.text,
style:
(label!.style ?? context.textTheme.titleLarge)
?.copyWith(color: color),
).toGradient(
LinearGradientHelper.fromNullableColors(
gradient,
),
);
},
),
],
),
Gap(style.padding ?? 10),
Builder(
builder: (context) {
final color = style?.foregroundColors?.color;
if (color != null) {
return ColorFiltered(
colorFilter:
ColorFilter.mode(color, BlendMode.srcIn),
child: suffix ?? const SizedBox.shrink(),
);
} else {
return suffix ?? const SizedBox.shrink();
}
},
),
],
),
),
),

View File

@ -9,6 +9,7 @@ environment:
sdk: ">=2.19.0 <3.0.0"
dependencies:
dotted_border: ^2.0.0+3
equatable: ^2.0.5
flutter: { sdk: flutter }
flutter_animate: ^3.0.0