Haptisches Feedback hinzufügen #216

Merged
flixcoo merged 14 commits from feature/215-haptisches-feedback-hinzufügen into development 2026-05-14 13:09:01 +00:00
26 changed files with 328 additions and 68 deletions

View File

@@ -1,4 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:tallee/presentation/widgets/buttons/haptic_back_button.dart';
import 'package:tallee/presentation/widgets/buttons/haptic_close_button.dart';
/// Theme class that defines colors, border radius, padding, and decorations /// Theme class that defines colors, border radius, padding, and decorations
class CustomTheme { class CustomTheme {
@@ -83,6 +85,11 @@ class CustomTheme {
iconTheme: IconThemeData(color: textColor), iconTheme: IconThemeData(color: textColor),
); );
static final ActionIconThemeData actionIconTheme = ActionIconThemeData(
flixcoo marked this conversation as resolved
Review

Bitte überprüfen: So wie ich getestet haben kann man an diversen Stellen die Buttons weglassen, wenn sie nur navigation übernehmen. Bitte alle Buttons in den App Bars entfernen, wenn das tatsächlich möglich ist.

Bitte überprüfen: So wie ich getestet haben kann man an diversen Stellen die Buttons weglassen, wenn sie nur navigation übernehmen. Bitte alle Buttons in den App Bars entfernen, wenn das tatsächlich möglich ist.
Review

hab einmal per search alles mit appbar angeguckt, die buttons sind nur da explizit drin, wo sie auch gebraucht werden, weil sie z.B. mit einer variable den screen poppen

hab einmal per search alles mit appbar angeguckt, die buttons sind nur da explizit drin, wo sie auch gebraucht werden, weil sie z.B. mit einer variable den screen poppen
Review

Alles klar, dann passt

Alles klar, dann passt
Review

aber grundsätzlich hast du recht, standardmäßig ist der zurück button immer da

aber grundsätzlich hast du recht, standardmäßig ist der zurück button immer da
backButtonIconBuilder: (context) => const HapticBackButton(),
closeButtonIconBuilder: (context) => const HapticCloseButton(),
);
static const SearchBarThemeData searchBarTheme = SearchBarThemeData( static const SearchBarThemeData searchBarTheme = SearchBarThemeData(
textStyle: WidgetStatePropertyAll(TextStyle(color: textColor)), textStyle: WidgetStatePropertyAll(TextStyle(color: textColor)),
hintStyle: WidgetStatePropertyAll(TextStyle(color: hintColor)), hintStyle: WidgetStatePropertyAll(TextStyle(color: hintColor)),

View File

@@ -42,6 +42,7 @@ class GameTracker extends StatelessWidget {
scaffoldBackgroundColor: CustomTheme.backgroundColor, scaffoldBackgroundColor: CustomTheme.backgroundColor,
// themes // themes
appBarTheme: CustomTheme.appBarTheme, appBarTheme: CustomTheme.appBarTheme,
actionIconTheme: CustomTheme.actionIconTheme,
inputDecorationTheme: CustomTheme.inputDecorationTheme, inputDecorationTheme: CustomTheme.inputDecorationTheme,
searchBarTheme: CustomTheme.searchBarTheme, searchBarTheme: CustomTheme.searchBarTheme,
radioTheme: CustomTheme.radioTheme, radioTheme: CustomTheme.radioTheme,

View File

@@ -1,4 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:tallee/core/adaptive_page_route.dart'; import 'package:tallee/core/adaptive_page_route.dart';
import 'package:tallee/core/custom_theme.dart'; import 'package:tallee/core/custom_theme.dart';
import 'package:tallee/l10n/generated/app_localizations.dart'; import 'package:tallee/l10n/generated/app_localizations.dart';
@@ -6,6 +7,7 @@ import 'package:tallee/presentation/views/main_menu/group_view/group_view.dart';
import 'package:tallee/presentation/views/main_menu/match_view/match_view.dart'; import 'package:tallee/presentation/views/main_menu/match_view/match_view.dart';
import 'package:tallee/presentation/views/main_menu/settings_view/settings_view.dart'; import 'package:tallee/presentation/views/main_menu/settings_view/settings_view.dart';
import 'package:tallee/presentation/views/main_menu/statistics_view.dart'; import 'package:tallee/presentation/views/main_menu/statistics_view.dart';
import 'package:tallee/presentation/widgets/buttons/haptic_icon_button.dart';
import 'package:tallee/presentation/widgets/navbar_item.dart'; import 'package:tallee/presentation/widgets/navbar_item.dart';
class CustomNavigationBar extends StatefulWidget { class CustomNavigationBar extends StatefulWidget {
@@ -53,10 +55,10 @@ class _CustomNavigationBarState extends State<CustomNavigationBar>
backgroundColor: CustomTheme.backgroundColor, backgroundColor: CustomTheme.backgroundColor,
scrolledUnderElevation: 0, scrolledUnderElevation: 0,
actions: [ actions: [
IconButton( HapticIconButton(
onPressed: () async { onPressed: () async {
await Navigator.push( final navigator = Navigator.of(context);
context, await navigator.push(
sneeex marked this conversation as resolved
Review

Warum rufst du hier extra nochmal HapticFeedback.selectionClick() auf?

Warum rufst du hier extra nochmal `HapticFeedback.selectionClick()` auf?
Review

war wohl ausversehen, vmtl noch aus dem testing, habs entfernt

war wohl ausversehen, vmtl noch aus dem testing, habs entfernt
adaptivePageRoute(builder: (_) => const SettingsView()), adaptivePageRoute(builder: (_) => const SettingsView()),
); );
setState(() { setState(() {
@@ -125,7 +127,8 @@ class _CustomNavigationBarState extends State<CustomNavigationBar>
} }
/// Handles tab tap events. Updates the current [index] state. /// Handles tab tap events. Updates the current [index] state.
void onTabTapped(int index) { void onTabTapped(int index) async {
await HapticFeedback.selectionClick();
setState(() { setState(() {
currentIndex = index; currentIndex = index;
}); });

View File

@@ -1,4 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:tallee/core/constants.dart'; import 'package:tallee/core/constants.dart';
import 'package:tallee/core/custom_theme.dart'; import 'package:tallee/core/custom_theme.dart';
@@ -133,8 +134,14 @@ class _CreateGroupViewState extends State<CreateGroupView> {
if (!mounted) return; if (!mounted) return;
if (success) { if (success) {
await HapticFeedback.successNotification();
if (mounted) {
Navigator.pop(context, updatedGroup); Navigator.pop(context, updatedGroup);
}
} else { } else {
if (mounted) {
await HapticFeedback.errorNotification();
}
showSnackbar( showSnackbar(
message: widget.groupToEdit == null message: widget.groupToEdit == null
? loc.error_creating_group ? loc.error_creating_group

View File

@@ -12,6 +12,7 @@ import 'package:tallee/data/models/player.dart';
import 'package:tallee/l10n/generated/app_localizations.dart'; import 'package:tallee/l10n/generated/app_localizations.dart';
import 'package:tallee/presentation/views/main_menu/group_view/create_group_view.dart'; import 'package:tallee/presentation/views/main_menu/group_view/create_group_view.dart';
import 'package:tallee/presentation/widgets/app_skeleton.dart'; import 'package:tallee/presentation/widgets/app_skeleton.dart';
import 'package:tallee/presentation/widgets/buttons/haptic_icon_button.dart';
import 'package:tallee/presentation/widgets/buttons/main_menu_button.dart'; import 'package:tallee/presentation/widgets/buttons/main_menu_button.dart';
import 'package:tallee/presentation/widgets/colored_icon_container.dart'; import 'package:tallee/presentation/widgets/colored_icon_container.dart';
import 'package:tallee/presentation/widgets/dialog/custom_alert_dialog.dart'; import 'package:tallee/presentation/widgets/dialog/custom_alert_dialog.dart';
@@ -65,7 +66,7 @@ class _GroupDetailViewState extends State<GroupDetailView> {
appBar: AppBar( appBar: AppBar(
title: Text(loc.group_profile), title: Text(loc.group_profile),
actions: [ actions: [
IconButton( HapticIconButton(
icon: const Icon(Icons.delete), icon: const Icon(Icons.delete),
onPressed: () async { onPressed: () async {
showDialog<bool>( showDialog<bool>(

View File

@@ -7,6 +7,7 @@ import 'package:tallee/data/db/database.dart';
import 'package:tallee/data/models/game.dart'; import 'package:tallee/data/models/game.dart';
import 'package:tallee/l10n/generated/app_localizations.dart'; import 'package:tallee/l10n/generated/app_localizations.dart';
import 'package:tallee/presentation/views/main_menu/match_view/create_match/create_game_view.dart'; import 'package:tallee/presentation/views/main_menu/match_view/create_match/create_game_view.dart';
import 'package:tallee/presentation/widgets/buttons/haptic_icon_button.dart';
import 'package:tallee/presentation/widgets/text_input/custom_search_bar.dart'; import 'package:tallee/presentation/widgets/text_input/custom_search_bar.dart';
import 'package:tallee/presentation/widgets/tiles/game_tile.dart'; import 'package:tallee/presentation/widgets/tiles/game_tile.dart';
import 'package:tallee/presentation/widgets/top_centered_message.dart'; import 'package:tallee/presentation/widgets/top_centered_message.dart';
@@ -70,7 +71,7 @@ class _ChooseGameViewState extends State<ChooseGameView> {
backgroundColor: CustomTheme.backgroundColor, backgroundColor: CustomTheme.backgroundColor,
resizeToAvoidBottomInset: false, resizeToAvoidBottomInset: false,
appBar: AppBar( appBar: AppBar(
leading: IconButton( leading: HapticIconButton(
icon: const Icon(Icons.arrow_back_ios), icon: const Icon(Icons.arrow_back_ios),
onPressed: () { onPressed: () {
Navigator.of(context).pop( Navigator.of(context).pop(
@@ -83,7 +84,7 @@ class _ChooseGameViewState extends State<ChooseGameView> {
}, },
), ),
actions: [ actions: [
IconButton( HapticIconButton(
icon: const Icon(Icons.add), icon: const Icon(Icons.add),
onPressed: () async { onPressed: () async {
final result = await Navigator.push( final result = await Navigator.push(

View File

@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:tallee/core/custom_theme.dart'; import 'package:tallee/core/custom_theme.dart';
import 'package:tallee/data/models/group.dart'; import 'package:tallee/data/models/group.dart';
import 'package:tallee/l10n/generated/app_localizations.dart'; import 'package:tallee/l10n/generated/app_localizations.dart';
import 'package:tallee/presentation/widgets/buttons/haptic_icon_button.dart';
import 'package:tallee/presentation/widgets/text_input/custom_search_bar.dart'; import 'package:tallee/presentation/widgets/text_input/custom_search_bar.dart';
import 'package:tallee/presentation/widgets/tiles/group_tile.dart'; import 'package:tallee/presentation/widgets/tiles/group_tile.dart';
import 'package:tallee/presentation/widgets/top_centered_message.dart'; import 'package:tallee/presentation/widgets/top_centered_message.dart';
@@ -45,7 +46,7 @@ class _ChooseGroupViewState extends State<ChooseGroupView> {
backgroundColor: CustomTheme.backgroundColor, backgroundColor: CustomTheme.backgroundColor,
resizeToAvoidBottomInset: false, resizeToAvoidBottomInset: false,
appBar: AppBar( appBar: AppBar(
leading: IconButton( leading: HapticIconButton(
icon: const Icon(Icons.arrow_back_ios), icon: const Icon(Icons.arrow_back_ios),
onPressed: () { onPressed: () {
Navigator.of(context).pop( Navigator.of(context).pop(
@@ -111,7 +112,10 @@ class _ChooseGroupViewState extends State<ChooseGroupView> {
padding: const EdgeInsets.only(bottom: 85), padding: const EdgeInsets.only(bottom: 85),
itemCount: filteredGroups.length, itemCount: filteredGroups.length,
itemBuilder: (BuildContext context, int index) { itemBuilder: (BuildContext context, int index) {
return GestureDetector( return GroupTile(
group: filteredGroups[index],
isHighlighted:
selectedGroupId == filteredGroups[index].id,
onTap: () { onTap: () {
setState(() { setState(() {
if (selectedGroupId != filteredGroups[index].id) { if (selectedGroupId != filteredGroups[index].id) {
@@ -121,11 +125,6 @@ class _ChooseGroupViewState extends State<ChooseGroupView> {
} }
}); });
}, },
child: GroupTile(
group: filteredGroups[index],
isHighlighted:
selectedGroupId == filteredGroups[index].id,
),
); );
}, },
), ),

View File

@@ -1,6 +1,7 @@
import 'dart:math'; import 'dart:math';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_popup/flutter_popup.dart'; import 'package:flutter_popup/flutter_popup.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:tallee/core/common.dart'; import 'package:tallee/core/common.dart';
@@ -12,6 +13,7 @@ import 'package:tallee/data/models/game.dart';
import 'package:tallee/data/models/group.dart'; import 'package:tallee/data/models/group.dart';
import 'package:tallee/l10n/generated/app_localizations.dart'; import 'package:tallee/l10n/generated/app_localizations.dart';
import 'package:tallee/presentation/widgets/buttons/custom_width_button.dart'; import 'package:tallee/presentation/widgets/buttons/custom_width_button.dart';
import 'package:tallee/presentation/widgets/buttons/haptic_icon_button.dart';
import 'package:tallee/presentation/widgets/dialog/custom_alert_dialog.dart'; import 'package:tallee/presentation/widgets/dialog/custom_alert_dialog.dart';
import 'package:tallee/presentation/widgets/dialog/custom_dialog_action.dart'; import 'package:tallee/presentation/widgets/dialog/custom_dialog_action.dart';
import 'package:tallee/presentation/widgets/text_input/text_input_field.dart'; import 'package:tallee/presentation/widgets/text_input/text_input_field.dart';
@@ -120,7 +122,7 @@ class _CreateGameViewState extends State<CreateGameView> {
title: Text(isEditing ? loc.edit_game : loc.create_game), title: Text(isEditing ? loc.edit_game : loc.create_game),
actions: [ actions: [
if (isEditMode()) if (isEditMode())
IconButton( HapticIconButton(
icon: const Icon(Icons.delete), icon: const Icon(Icons.delete),
onPressed: () async { onPressed: () async {
if (!context.mounted) return; if (!context.mounted) return;
@@ -329,6 +331,12 @@ class _CreateGameViewState extends State<CreateGameView> {
contentPadding: const EdgeInsets.symmetric(horizontal: 0, vertical: 10), contentPadding: const EdgeInsets.symmetric(horizontal: 0, vertical: 10),
barrierColor: Colors.transparent, barrierColor: Colors.transparent,
contentDecoration: CustomTheme.standardBoxDecoration, contentDecoration: CustomTheme.standardBoxDecoration,
onBeforePopup: () async {
await HapticFeedback.selectionClick();
},
onAfterPopup: () async {
await HapticFeedback.selectionClick();
},
content: StatefulBuilder( content: StatefulBuilder(
builder: (context, setPopupState) => SizedBox( builder: (context, setPopupState) => SizedBox(
width: 280, width: 280,
@@ -338,7 +346,8 @@ class _CreateGameViewState extends State<CreateGameView> {
children: List.generate( children: List.generate(
_rulesets.length, _rulesets.length,
(index) => GestureDetector( (index) => GestureDetector(
onTap: () { onTap: () async {
await HapticFeedback.selectionClick();
setState(() { setState(() {
selectedRuleset = _rulesets[index].$1; selectedRuleset = _rulesets[index].$1;
}); });
@@ -412,6 +421,12 @@ class _CreateGameViewState extends State<CreateGameView> {
contentPadding: const EdgeInsets.symmetric(horizontal: 0, vertical: 10), contentPadding: const EdgeInsets.symmetric(horizontal: 0, vertical: 10),
barrierColor: Colors.transparent, barrierColor: Colors.transparent,
contentDecoration: CustomTheme.standardBoxDecoration, contentDecoration: CustomTheme.standardBoxDecoration,
onBeforePopup: () async {
await HapticFeedback.selectionClick();
},
onAfterPopup: () async {
await HapticFeedback.selectionClick();
},
content: StatefulBuilder( content: StatefulBuilder(
builder: (context, setPopupState) => SizedBox( builder: (context, setPopupState) => SizedBox(
width: 150, width: 150,
@@ -421,7 +436,8 @@ class _CreateGameViewState extends State<CreateGameView> {
children: List.generate( children: List.generate(
_colors.length, _colors.length,
(index) => GestureDetector( (index) => GestureDetector(
onTap: () { onTap: () async {
await HapticFeedback.selectionClick();
setState(() { setState(() {
selectedColor = _colors[index].$1; selectedColor = _colors[index].$1;
}); });

View File

@@ -172,7 +172,6 @@ class _CreateMatchViewState extends State<CreateMatchView> {
) ?? ) ??
false, false,
); );
selectedGroup = await Navigator.of(context).push( selectedGroup = await Navigator.of(context).push(
adaptivePageRoute( adaptivePageRoute(
builder: (context) => ChooseGroupView( builder: (context) => ChooseGroupView(

View File

@@ -11,6 +11,7 @@ import 'package:tallee/data/models/match.dart';
import 'package:tallee/l10n/generated/app_localizations.dart'; import 'package:tallee/l10n/generated/app_localizations.dart';
import 'package:tallee/presentation/views/main_menu/match_view/create_match/create_match_view.dart'; import 'package:tallee/presentation/views/main_menu/match_view/create_match/create_match_view.dart';
import 'package:tallee/presentation/views/main_menu/match_view/match_result_view.dart'; import 'package:tallee/presentation/views/main_menu/match_view/match_result_view.dart';
import 'package:tallee/presentation/widgets/buttons/haptic_icon_button.dart';
import 'package:tallee/presentation/widgets/buttons/main_menu_button.dart'; import 'package:tallee/presentation/widgets/buttons/main_menu_button.dart';
import 'package:tallee/presentation/widgets/colored_icon_container.dart'; import 'package:tallee/presentation/widgets/colored_icon_container.dart';
import 'package:tallee/presentation/widgets/dialog/custom_alert_dialog.dart'; import 'package:tallee/presentation/widgets/dialog/custom_alert_dialog.dart';
@@ -60,7 +61,7 @@ class _MatchDetailViewState extends State<MatchDetailView> {
appBar: AppBar( appBar: AppBar(
title: Text(loc.match_profile), title: Text(loc.match_profile),
actions: [ actions: [
IconButton( HapticIconButton(
icon: const Icon(Icons.delete), icon: const Icon(Icons.delete),
onPressed: () async { onPressed: () async {
showDialog<bool>( showDialog<bool>(

View File

@@ -1,4 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:tallee/core/custom_theme.dart'; import 'package:tallee/core/custom_theme.dart';
import 'package:tallee/core/enums.dart'; import 'package:tallee/core/enums.dart';
@@ -8,6 +9,7 @@ import 'package:tallee/data/models/player.dart';
import 'package:tallee/data/models/score_entry.dart'; import 'package:tallee/data/models/score_entry.dart';
import 'package:tallee/l10n/generated/app_localizations.dart'; import 'package:tallee/l10n/generated/app_localizations.dart';
import 'package:tallee/presentation/widgets/buttons/custom_width_button.dart'; import 'package:tallee/presentation/widgets/buttons/custom_width_button.dart';
import 'package:tallee/presentation/widgets/buttons/haptic_icon_button.dart';
import 'package:tallee/presentation/widgets/tiles/match_result_view/custom_checkbox_list_tile.dart'; import 'package:tallee/presentation/widgets/tiles/match_result_view/custom_checkbox_list_tile.dart';
import 'package:tallee/presentation/widgets/tiles/match_result_view/custom_radio_list_tile.dart'; import 'package:tallee/presentation/widgets/tiles/match_result_view/custom_radio_list_tile.dart';
import 'package:tallee/presentation/widgets/tiles/match_result_view/live_edit_list_tile.dart'; import 'package:tallee/presentation/widgets/tiles/match_result_view/live_edit_list_tile.dart';
@@ -112,7 +114,7 @@ class _MatchResultViewState extends State<MatchResultView> {
return Scaffold( return Scaffold(
backgroundColor: CustomTheme.backgroundColor, backgroundColor: CustomTheme.backgroundColor,
appBar: AppBar( appBar: AppBar(
leading: IconButton( leading: HapticIconButton(
icon: const Icon(Icons.close), icon: const Icon(Icons.close),
onPressed: () { onPressed: () {
widget.onWinnerChanged?.call(); widget.onWinnerChanged?.call();
@@ -204,6 +206,7 @@ class _MatchResultViewState extends State<MatchResultView> {
: RadioGroup<Player>( : RadioGroup<Player>(
groupValue: _selectedPlayer, groupValue: _selectedPlayer,
onChanged: (Player? value) async { onChanged: (Player? value) async {
await HapticFeedback.selectionClick();
setState(() { setState(() {
_selectedPlayer = value; _selectedPlayer = value;
}); });
@@ -217,6 +220,7 @@ class _MatchResultViewState extends State<MatchResultView> {
text: allPlayers[index].name, text: allPlayers[index].name,
value: allPlayers[index], value: allPlayers[index],
onContainerTap: (value) async { onContainerTap: (value) async {
await HapticFeedback.selectionClick();
setState(() { setState(() {
// Check if the already selected player is the same as the newly tapped player. // Check if the already selected player is the same as the newly tapped player.
if (_selectedPlayer == value) { if (_selectedPlayer == value) {
@@ -338,6 +342,12 @@ class _MatchResultViewState extends State<MatchResultView> {
}, },
); );
}, },
onReorderStart: (int n) async {
await HapticFeedback.selectionClick();
},
onReorderEnd: (int n) async {
await HapticFeedback.selectionClick();
},
onReorder: (int oldIndex, int newIndex) { onReorder: (int oldIndex, int newIndex) {
setState(() { setState(() {
if (newIndex > oldIndex) { if (newIndex > oldIndex) {

View File

@@ -2,6 +2,7 @@ import 'dart:io';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:package_info_plus/package_info_plus.dart'; import 'package:package_info_plus/package_info_plus.dart';
@@ -9,6 +10,7 @@ import 'package:tallee/core/custom_theme.dart';
import 'package:tallee/core/enums.dart'; import 'package:tallee/core/enums.dart';
import 'package:tallee/l10n/generated/app_localizations.dart'; import 'package:tallee/l10n/generated/app_localizations.dart';
import 'package:tallee/presentation/views/main_menu/settings_view/licenses_view.dart'; import 'package:tallee/presentation/views/main_menu/settings_view/licenses_view.dart';
import 'package:tallee/presentation/widgets/buttons/haptic_icon_button.dart';
import 'package:tallee/presentation/widgets/dialog/custom_alert_dialog.dart'; import 'package:tallee/presentation/widgets/dialog/custom_alert_dialog.dart';
import 'package:tallee/presentation/widgets/dialog/custom_dialog_action.dart'; import 'package:tallee/presentation/widgets/dialog/custom_dialog_action.dart';
import 'package:tallee/presentation/widgets/tiles/settings_list_tile.dart'; import 'package:tallee/presentation/widgets/tiles/settings_list_tile.dart';
@@ -197,19 +199,23 @@ class _SettingsViewState extends State<SettingsView> {
padding: const EdgeInsets.only(bottom: 12), padding: const EdgeInsets.only(bottom: 12),
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
spacing: 40, spacing: 10,
children: [ children: [
GestureDetector( HapticIconButton(
child: const Icon(Icons.language), color: CustomTheme.textColor,
onTap: () => { icon: const Icon(Icons.language),
sneeex marked this conversation as resolved Outdated

Kann man diese drei Icon Buttons nicht direkt durch HapticIconButton ersetzen?

Kann man diese drei Icon Buttons nicht direkt durch `HapticIconButton` ersetzen?

Kann man, aber der hat doch nen ganz anderes Design oder nicht?

Kann man, aber der hat doch nen ganz anderes Design oder nicht?

Hab’s mir jetzt nicht genau angeschaut aber ist doch eigentlich auch nur n clickable Icon.

Hab’s mir jetzt nicht genau angeschaut aber ist doch eigentlich auch nur n clickable Icon.
onPressed: () async => {
await HapticFeedback.lightImpact(),
launchUrl( launchUrl(
Uri.parse('https://liquid-dev.de'), Uri.parse('https://liquid-dev.de'),
), ),
}, },
), ),
GestureDetector( HapticIconButton(
child: const FaIcon(FontAwesomeIcons.github), color: CustomTheme.textColor,
onTap: () => { icon: const FaIcon(FontAwesomeIcons.github),
onPressed: () async => {
await HapticFeedback.lightImpact(),
launchUrl( launchUrl(
Uri.parse( Uri.parse(
'https://github.com/liquiddevelopmentde', 'https://github.com/liquiddevelopmentde',
@@ -217,15 +223,19 @@ class _SettingsViewState extends State<SettingsView> {
), ),
}, },
), ),
GestureDetector( HapticIconButton(
child: Icon( color: CustomTheme.textColor,
icon: Icon(
Platform.isIOS Platform.isIOS
? CupertinoIcons.mail_solid ? CupertinoIcons.mail_solid
: Icons.email, : Icons.email,
), ),
onTap: () => launchUrl( onPressed: () async => {
await HapticFeedback.lightImpact(),
launchUrl(
Uri.parse('mailto:hi@liquid-dev.de'), Uri.parse('mailto:hi@liquid-dev.de'),
), ),
},
), ),
], ],
), ),
@@ -266,23 +276,44 @@ class _SettingsViewState extends State<SettingsView> {
void showImportSnackBar({ void showImportSnackBar({
required BuildContext context, required BuildContext context,
required ImportResult result, required ImportResult result,
}) { }) async {
final loc = AppLocalizations.of(context); final loc = AppLocalizations.of(context);
switch (result) { switch (result) {
case ImportResult.success: case ImportResult.success:
showSnackbar(context: context, message: loc.data_successfully_imported); await HapticFeedback.successNotification();
if (context.mounted) {
showSnackbar(
context: context,
message: loc.data_successfully_imported,
);
}
case ImportResult.invalidSchema: case ImportResult.invalidSchema:
await HapticFeedback.errorNotification();
if (context.mounted) {
showSnackbar(context: context, message: loc.invalid_schema); showSnackbar(context: context, message: loc.invalid_schema);
}
case ImportResult.fileReadError: case ImportResult.fileReadError:
await HapticFeedback.errorNotification();
if (context.mounted) {
showSnackbar(context: context, message: loc.error_reading_file); showSnackbar(context: context, message: loc.error_reading_file);
}
case ImportResult.canceled: case ImportResult.canceled:
await HapticFeedback.errorNotification();
if (context.mounted) {
showSnackbar(context: context, message: loc.import_canceled); showSnackbar(context: context, message: loc.import_canceled);
}
case ImportResult.formatException: case ImportResult.formatException:
await HapticFeedback.errorNotification();
if (context.mounted) {
showSnackbar(context: context, message: loc.format_exception); showSnackbar(context: context, message: loc.format_exception);
}
case ImportResult.unknownException: case ImportResult.unknownException:
await HapticFeedback.errorNotification();
if (context.mounted) {
showSnackbar(context: context, message: loc.unknown_exception); showSnackbar(context: context, message: loc.unknown_exception);
} }
} }
}
/// Displays a snackbar based on the export result. /// Displays a snackbar based on the export result.
/// ///
@@ -291,17 +322,29 @@ class _SettingsViewState extends State<SettingsView> {
void showExportSnackBar({ void showExportSnackBar({
required BuildContext context, required BuildContext context,
required ExportResult result, required ExportResult result,
}) { }) async {
final loc = AppLocalizations.of(context); final loc = AppLocalizations.of(context);
switch (result) { switch (result) {
case ExportResult.success: case ExportResult.success:
showSnackbar(context: context, message: loc.data_successfully_exported); await HapticFeedback.successNotification();
if (context.mounted) {
showSnackbar(
context: context,
message: loc.data_successfully_exported,
);
}
case ExportResult.canceled: case ExportResult.canceled:
await HapticFeedback.errorNotification();
if (context.mounted) {
showSnackbar(context: context, message: loc.export_canceled); showSnackbar(context: context, message: loc.export_canceled);
}
case ExportResult.unknownException: case ExportResult.unknownException:
await HapticFeedback.errorNotification();
if (context.mounted) {
showSnackbar(context: context, message: loc.unknown_exception); showSnackbar(context: context, message: loc.unknown_exception);
} }
} }
}
/// Displays a snackbar with the given message and optional action. /// Displays a snackbar with the given message and optional action.
/// ///

View File

@@ -1,4 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:tallee/core/custom_theme.dart'; import 'package:tallee/core/custom_theme.dart';
import 'package:tallee/core/enums.dart'; import 'package:tallee/core/enums.dart';
@@ -48,7 +49,12 @@ class CustomWidthButton extends StatelessWidget {
)!; )!;
return ElevatedButton( return ElevatedButton(
onPressed: onPressed, onPressed: onPressed == null
? null
: () async {
await HapticFeedback.selectionClick();
onPressed!.call();
},
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
foregroundColor: textcolor, foregroundColor: textcolor,
disabledForegroundColor: disabledTextColor, disabledForegroundColor: disabledTextColor,
@@ -78,7 +84,12 @@ class CustomWidthButton extends StatelessWidget {
: Color.lerp(CustomTheme.primaryColor, Colors.black, 0.5)!; : Color.lerp(CustomTheme.primaryColor, Colors.black, 0.5)!;
return OutlinedButton( return OutlinedButton(
onPressed: onPressed, onPressed: onPressed == null
? null
: () async {
await HapticFeedback.selectionClick();
onPressed!.call();
},
style: OutlinedButton.styleFrom( style: OutlinedButton.styleFrom(
foregroundColor: textcolor, foregroundColor: textcolor,
disabledForegroundColor: disabledTextColor, disabledForegroundColor: disabledTextColor,
@@ -110,7 +121,12 @@ class CustomWidthButton extends StatelessWidget {
disabledBackgroundColor = Colors.transparent; disabledBackgroundColor = Colors.transparent;
return TextButton( return TextButton(
onPressed: onPressed, onPressed: onPressed == null
? null
: () async {
await HapticFeedback.selectionClick();
onPressed!.call();
},
style: TextButton.styleFrom( style: TextButton.styleFrom(
foregroundColor: textcolor, foregroundColor: textcolor,
disabledForegroundColor: disabledTextColor, disabledForegroundColor: disabledTextColor,

View File

@@ -0,0 +1,23 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:tallee/presentation/widgets/buttons/haptic_icon_button.dart';
class HapticBackButton extends StatelessWidget {
const HapticBackButton({super.key});
@override
Widget build(BuildContext context) {
final iconData = switch (defaultTargetPlatform) {
TargetPlatform.iOS ||
TargetPlatform.macOS => Icons.arrow_back_ios_new_rounded,
_ => Icons.arrow_back_rounded,
};
return HapticIconButton(
icon: Icon(iconData),
onPressed: () async {
Navigator.of(context).maybePop();
},
);
}
}

View File

@@ -0,0 +1,23 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:tallee/presentation/widgets/buttons/haptic_icon_button.dart';
class HapticCloseButton extends StatelessWidget {
const HapticCloseButton({super.key});
@override
Widget build(BuildContext context) {
final iconData = switch (defaultTargetPlatform) {
TargetPlatform.iOS || TargetPlatform.macOS => CupertinoIcons.xmark,
_ => Icons.close_rounded,
};
return HapticIconButton(
icon: Icon(iconData),
onPressed: () async {
Navigator.of(context).maybePop();
},
);
}
}

View File

@@ -0,0 +1,51 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
class HapticIconButton extends StatelessWidget {
sneeex marked this conversation as resolved
Review

Kannst du bitte noch bei den drei neuen Buttons den Splash-Effekt entfernen und eine Scale Animation hinzufügen, ähnlich wie bei main menu button? Ggf. ersetzt du dann IconButton() einfach durch einen Container(), vllt ist das sogar einfacher

Kannst du bitte noch bei den drei neuen Buttons den Splash-Effekt entfernen und eine Scale Animation hinzufügen, ähnlich wie bei main menu button? Ggf. ersetzt du dann `IconButton()` einfach durch einen `Container()`, vllt ist das sogar einfacher
Review

du meinst bei allen IconButtons? weil das ist ja dann nicht nur die drei buttons in den settings, sondern alle back buttons auch. Und was für ein scale meinst du? icon scale? button scale insgesamt? und wieso? finde das eigentlich glaube nicht so geil

du meinst bei allen IconButtons? weil das ist ja dann nicht nur die drei buttons in den settings, sondern alle back buttons auch. Und was für ein scale meinst du? icon scale? button scale insgesamt? und wieso? finde das eigentlich glaube nicht so geil
Review

Ja bei allen Icon Buttons.
Meinetwegen auch ohne Scale Effekt, aber auf jeden Fall ohne Splash. Und mit Scale meinte ich dass das Icon sich in der Größe verändert, wie beim MainMenuButton

Ja bei allen Icon Buttons. Meinetwegen auch ohne Scale Effekt, aber auf jeden Fall ohne Splash. Und mit Scale meinte ich dass das Icon sich in der Größe verändert, wie beim `MainMenuButton`
Review

Ja bei allen Icon Buttons.
Meinetwegen auch ohne Scale Effekt, aber auf jeden Fall ohne Splash. Und mit Scale meinte ich dass das Icon sich in der Größe verändert, wie beim MainMenuButton

Beim Main Menu Button ist aber der ganze Button mit Scale und nicht nur das Icon

> Ja bei allen Icon Buttons. > Meinetwegen auch ohne Scale Effekt, aber auf jeden Fall ohne Splash. Und mit Scale meinte ich dass das Icon sich in der Größe verändert, wie beim `MainMenuButton` Beim Main Menu Button ist aber der ganze Button mit Scale und nicht nur das Icon
Review

Der Icon Button ist ja aber nur n Icon, der hat ja keinen Hintergrund

Der Icon Button ist ja aber nur n Icon, der hat ja keinen Hintergrund
const HapticIconButton({
super.key,
required this.icon,
required this.onPressed,
this.iconSize,
this.color,
this.padding,
this.alignment,
this.constraints,
this.style,
this.isSelected,
this.selectedIcon,
});
final Widget icon;
final VoidCallback? onPressed;
final double? iconSize;
final Color? color;
final EdgeInsetsGeometry? padding;
final AlignmentGeometry? alignment;
final BoxConstraints? constraints;
final ButtonStyle? style;
final bool? isSelected;
final Widget? selectedIcon;
@override
Widget build(BuildContext context) {
return IconButton(
iconSize: iconSize,
highlightColor: Colors.transparent, //disable splash animation
color: color,
padding: padding,
sneeex marked this conversation as resolved
Review

Nicht sinnvoll, wir sind ja auf mobile, da gibts ja keinen hover state

Nicht sinnvoll, wir sind ja auf mobile, da gibts ja keinen hover state
alignment: alignment ?? Alignment.center,
constraints: constraints,
style: style,
isSelected: isSelected,
selectedIcon: selectedIcon,
icon: icon,
onPressed: onPressed == null
? null
: () async {
await HapticFeedback.selectionClick();
onPressed!.call();
},
);
}
}

View File

@@ -1,6 +1,7 @@
import 'dart:async'; import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
class MainMenuButton extends StatefulWidget { class MainMenuButton extends StatefulWidget {
/// A button for the main menu with an optional icon and a press animation. /// A button for the main menu with an optional icon and a press animation.
@@ -65,19 +66,27 @@ class _MainMenuButtonState extends State<MainMenuButton>
onTapDown: (_) { onTapDown: (_) {
_animationController.forward(); _animationController.forward();
if (widget.onLongPressed != null) { if (widget.onLongPressed != null) {
_longPressTimer = Timer(const Duration(milliseconds: 400), () { _longPressTimer = Timer(
const Duration(milliseconds: 400),
() async {
_isLongPressing = true; _isLongPressing = true;
widget.onLongPressed?.call(); widget.onLongPressed?.call();
await HapticFeedback.heavyImpact();
_repeatTimer = Timer.periodic( _repeatTimer = Timer.periodic(
const Duration(milliseconds: 250), const Duration(milliseconds: 250),
(_) => widget.onLongPressed?.call(), (_) async {
widget.onLongPressed?.call();
await HapticFeedback.heavyImpact();
},
);
},
sneeex marked this conversation as resolved Outdated

Warum ist hier nur auf dem Short Tab eine Vibration und nicht auf dem Long Tap?

Warum ist hier nur auf dem Short Tab eine Vibration und nicht auf dem Long Tap?

nicht gecheckt, habs geändert

nicht gecheckt, habs geändert
); );
});
} }
}, },
onTapUp: (_) async { onTapUp: (_) async {
_cancelTimers(); _cancelTimers();
if (mounted && !_isLongPressing) { if (mounted && !_isLongPressing) {
await HapticFeedback.selectionClick();
widget.onPressed(); widget.onPressed();
} }
_isLongPressing = false; _isLongPressing = false;

View File

@@ -1,4 +1,5 @@
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter/services.dart';
import 'package:tallee/core/enums.dart'; import 'package:tallee/core/enums.dart';
import 'package:tallee/presentation/widgets/buttons/animated_dialog_button.dart'; import 'package:tallee/presentation/widgets/buttons/animated_dialog_button.dart';
@@ -26,7 +27,10 @@ class CustomDialogAction extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return AnimatedDialogButton( return AnimatedDialogButton(
onPressed: onPressed, onPressed: () async {
await HapticFeedback.selectionClick();
onPressed.call();
},
buttonText: text, buttonText: text,
buttonType: buttonType, buttonType: buttonType,
isDescructive: isDestructive, isDescructive: isDestructive,

View File

@@ -1,4 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:tallee/core/common.dart'; import 'package:tallee/core/common.dart';
import 'package:tallee/core/constants.dart'; import 'package:tallee/core/constants.dart';
@@ -143,7 +144,8 @@ class _PlayerSelectionState extends State<PlayerSelection> {
text: player.name, text: player.name,
suffixText: getNameCountText(player), suffixText: getNameCountText(player),
onIconTap: () { onIconTap: () {
setState(() { setState(() async {
await HapticFeedback.selectionClick();
// Removes the player from the selection and notifies the parent. // Removes the player from the selection and notifies the parent.
selectedPlayers.remove(player); selectedPlayers.remove(player);
widget.onChanged([...selectedPlayers]); widget.onChanged([...selectedPlayers]);
@@ -197,7 +199,8 @@ class _PlayerSelectionState extends State<PlayerSelection> {
text: suggestedPlayers[index].name, text: suggestedPlayers[index].name,
suffixText: getNameCountText(suggestedPlayers[index]), suffixText: getNameCountText(suggestedPlayers[index]),
icon: Icons.add, icon: Icons.add,
onPressed: () { onPressed: () async {
await HapticFeedback.selectionClick();
setState(() { setState(() {
// If the player is not already selected // If the player is not already selected
if (!selectedPlayers.contains( if (!selectedPlayers.contains(
@@ -294,8 +297,10 @@ class _PlayerSelectionState extends State<PlayerSelection> {
if (success) { if (success) {
_handleSuccessfulPlayerCreation(createdPlayer); _handleSuccessfulPlayerCreation(createdPlayer);
await HapticFeedback.successNotification();
showSnackBarMessage(loc.successfully_added_player(playerName)); showSnackBarMessage(loc.successfully_added_player(playerName));
} else { } else {
await HapticFeedback.errorNotification();
showSnackBarMessage(loc.could_not_add_player(playerName)); showSnackBarMessage(loc.could_not_add_player(playerName));
} }
} }

View File

@@ -1,4 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:tallee/core/custom_theme.dart'; import 'package:tallee/core/custom_theme.dart';
class ChooseTile extends StatefulWidget { class ChooseTile extends StatefulWidget {
@@ -30,7 +31,14 @@ class _ChooseTileState extends State<ChooseTile> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return GestureDetector( return GestureDetector(
onTap: widget.onPressed, onTap: widget.onPressed != null
? () async {
await HapticFeedback.selectionClick();
if (widget.onPressed != null) {
widget.onPressed!.call();
}
}
: null,
child: Container( child: Container(
margin: CustomTheme.tileMargin, margin: CustomTheme.tileMargin,
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),

View File

@@ -1,4 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:tallee/core/common.dart'; import 'package:tallee/core/common.dart';
import 'package:tallee/core/custom_theme.dart'; import 'package:tallee/core/custom_theme.dart';
import 'package:tallee/core/enums.dart'; import 'package:tallee/core/enums.dart';
@@ -53,8 +54,18 @@ class GameTile extends StatelessWidget {
final gameColor = badgeColor ?? getColorFromGameColor(GameColor.orange); final gameColor = badgeColor ?? getColorFromGameColor(GameColor.orange);
return GestureDetector( return GestureDetector(
onTap: onTap, onTap: () async {
onLongPress: onLongPress, await HapticFeedback.selectionClick();
if (onTap != null) {
onTap!.call();
}
},
onLongPress: () async {
await HapticFeedback.heavyImpact();
if (onLongPress != null) {
onLongPress!.call();
}
},
child: AnimatedContainer( child: AnimatedContainer(
margin: const EdgeInsets.symmetric(vertical: 10, horizontal: 10), margin: const EdgeInsets.symmetric(vertical: 10, horizontal: 10),
decoration: !isHighlighted decoration: !isHighlighted

View File

@@ -1,4 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:tallee/core/common.dart'; import 'package:tallee/core/common.dart';
import 'package:tallee/core/custom_theme.dart'; import 'package:tallee/core/custom_theme.dart';
import 'package:tallee/data/models/group.dart'; import 'package:tallee/data/models/group.dart';
@@ -33,7 +34,12 @@ class _GroupTileState extends State<GroupTile> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return GestureDetector( return GestureDetector(
onTap: widget.onTap, onTap: () async {
await HapticFeedback.selectionClick();
if (widget.onTap != null) {
widget.onTap!.call();
}
},
child: AnimatedContainer( child: AnimatedContainer(
margin: CustomTheme.standardMargin, margin: CustomTheme.standardMargin,
padding: const EdgeInsets.symmetric(vertical: 5, horizontal: 10), padding: const EdgeInsets.symmetric(vertical: 5, horizontal: 10),

View File

@@ -1,4 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:tallee/core/custom_theme.dart'; import 'package:tallee/core/custom_theme.dart';
import 'package:tallee/presentation/views/main_menu/settings_view/licenses/license_detail_view.dart'; import 'package:tallee/presentation/views/main_menu/settings_view/licenses/license_detail_view.dart';
import 'package:tallee/presentation/views/main_menu/settings_view/licenses/oss_licenses.dart'; import 'package:tallee/presentation/views/main_menu/settings_view/licenses/oss_licenses.dart';
@@ -15,8 +16,10 @@ class LicenseTile extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return GestureDetector( return GestureDetector(
onTap: () { onTap: () async {
Navigator.of(context).push( final navigator = Navigator.of(context);
await HapticFeedback.selectionClick();
navigator.push(
MaterialPageRoute( MaterialPageRoute(
builder: (context) => LicenseDetailView(package: package), builder: (context) => LicenseDetailView(package: package),
), ),

View File

@@ -1,4 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:tallee/core/custom_theme.dart'; import 'package:tallee/core/custom_theme.dart';
class CustomCheckboxListTile extends StatelessWidget { class CustomCheckboxListTile extends StatelessWidget {
@@ -16,7 +17,10 @@ class CustomCheckboxListTile extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return GestureDetector( return GestureDetector(
onTap: () => onChanged(!value), onTap: () async {
await HapticFeedback.selectionClick();
onChanged(!value);
},
child: Container( child: Container(
margin: const EdgeInsets.symmetric(horizontal: 5, vertical: 5), margin: const EdgeInsets.symmetric(horizontal: 5, vertical: 5),
padding: const EdgeInsets.symmetric(horizontal: 2), padding: const EdgeInsets.symmetric(horizontal: 2),
@@ -29,7 +33,8 @@ class CustomCheckboxListTile extends StatelessWidget {
children: [ children: [
Checkbox( Checkbox(
value: value, value: value,
onChanged: (bool? v) { onChanged: (bool? v) async {
await HapticFeedback.selectionClick();
if (v == null) return; if (v == null) return;
onChanged(v); onChanged(v);
}, },

View File

@@ -1,6 +1,7 @@
import 'dart:core' hide Match; import 'dart:core' hide Match;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:tallee/core/common.dart'; import 'package:tallee/core/common.dart';
import 'package:tallee/core/custom_theme.dart'; import 'package:tallee/core/custom_theme.dart';
@@ -51,7 +52,10 @@ class _MatchTileState extends State<MatchTile> {
final loc = AppLocalizations.of(context); final loc = AppLocalizations.of(context);
return GestureDetector( return GestureDetector(
onTap: widget.onTap, onTap: () async {
await HapticFeedback.selectionClick();
widget.onTap.call();
},
child: Container( child: Container(
margin: EdgeInsets.zero, margin: EdgeInsets.zero,
width: widget.width, width: widget.width,

View File

@@ -1,4 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:tallee/core/custom_theme.dart'; import 'package:tallee/core/custom_theme.dart';
import 'package:tallee/presentation/widgets/colored_icon_container.dart'; import 'package:tallee/presentation/widgets/colored_icon_container.dart';
@@ -36,7 +37,10 @@ class SettingsListTile extends StatelessWidget {
child: SizedBox( child: SizedBox(
width: MediaQuery.of(context).size.width * 0.95, width: MediaQuery.of(context).size.width * 0.95,
child: GestureDetector( child: GestureDetector(
onTap: onPressed ?? () {}, onTap: () async {
await HapticFeedback.selectionClick();
onPressed?.call();
},
child: Container( child: Container(
margin: EdgeInsets.zero, margin: EdgeInsets.zero,
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 12), padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 12),