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: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
class CustomTheme {
@@ -83,6 +85,11 @@ class CustomTheme {
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(
textStyle: WidgetStatePropertyAll(TextStyle(color: textColor)),
hintStyle: WidgetStatePropertyAll(TextStyle(color: hintColor)),

View File

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

View File

@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:tallee/core/adaptive_page_route.dart';
import 'package:tallee/core/custom_theme.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/settings_view/settings_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';
class CustomNavigationBar extends StatefulWidget {
@@ -53,10 +55,10 @@ class _CustomNavigationBarState extends State<CustomNavigationBar>
backgroundColor: CustomTheme.backgroundColor,
scrolledUnderElevation: 0,
actions: [
IconButton(
HapticIconButton(
onPressed: () async {
await Navigator.push(
context,
final navigator = Navigator.of(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()),
);
setState(() {
@@ -125,7 +127,8 @@ class _CustomNavigationBarState extends State<CustomNavigationBar>
}
/// Handles tab tap events. Updates the current [index] state.
void onTabTapped(int index) {
void onTabTapped(int index) async {
await HapticFeedback.selectionClick();
setState(() {
currentIndex = index;
});

View File

@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:provider/provider.dart';
import 'package:tallee/core/constants.dart';
import 'package:tallee/core/custom_theme.dart';
@@ -133,8 +134,14 @@ class _CreateGroupViewState extends State<CreateGroupView> {
if (!mounted) return;
if (success) {
Navigator.pop(context, updatedGroup);
await HapticFeedback.successNotification();
if (mounted) {
Navigator.pop(context, updatedGroup);
}
} else {
if (mounted) {
await HapticFeedback.errorNotification();
}
showSnackbar(
message: widget.groupToEdit == null
? 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/presentation/views/main_menu/group_view/create_group_view.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/colored_icon_container.dart';
import 'package:tallee/presentation/widgets/dialog/custom_alert_dialog.dart';
@@ -65,7 +66,7 @@ class _GroupDetailViewState extends State<GroupDetailView> {
appBar: AppBar(
title: Text(loc.group_profile),
actions: [
IconButton(
HapticIconButton(
icon: const Icon(Icons.delete),
onPressed: () async {
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/l10n/generated/app_localizations.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/tiles/game_tile.dart';
import 'package:tallee/presentation/widgets/top_centered_message.dart';
@@ -70,7 +71,7 @@ class _ChooseGameViewState extends State<ChooseGameView> {
backgroundColor: CustomTheme.backgroundColor,
resizeToAvoidBottomInset: false,
appBar: AppBar(
leading: IconButton(
leading: HapticIconButton(
icon: const Icon(Icons.arrow_back_ios),
onPressed: () {
Navigator.of(context).pop(
@@ -83,7 +84,7 @@ class _ChooseGameViewState extends State<ChooseGameView> {
},
),
actions: [
IconButton(
HapticIconButton(
icon: const Icon(Icons.add),
onPressed: () async {
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/data/models/group.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/tiles/group_tile.dart';
import 'package:tallee/presentation/widgets/top_centered_message.dart';
@@ -45,7 +46,7 @@ class _ChooseGroupViewState extends State<ChooseGroupView> {
backgroundColor: CustomTheme.backgroundColor,
resizeToAvoidBottomInset: false,
appBar: AppBar(
leading: IconButton(
leading: HapticIconButton(
icon: const Icon(Icons.arrow_back_ios),
onPressed: () {
Navigator.of(context).pop(
@@ -111,7 +112,10 @@ class _ChooseGroupViewState extends State<ChooseGroupView> {
padding: const EdgeInsets.only(bottom: 85),
itemCount: filteredGroups.length,
itemBuilder: (BuildContext context, int index) {
return GestureDetector(
return GroupTile(
group: filteredGroups[index],
isHighlighted:
selectedGroupId == filteredGroups[index].id,
onTap: () {
setState(() {
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 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_popup/flutter_popup.dart';
import 'package:provider/provider.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/l10n/generated/app_localizations.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_dialog_action.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),
actions: [
if (isEditMode())
IconButton(
HapticIconButton(
icon: const Icon(Icons.delete),
onPressed: () async {
if (!context.mounted) return;
@@ -329,6 +331,12 @@ class _CreateGameViewState extends State<CreateGameView> {
contentPadding: const EdgeInsets.symmetric(horizontal: 0, vertical: 10),
barrierColor: Colors.transparent,
contentDecoration: CustomTheme.standardBoxDecoration,
onBeforePopup: () async {
await HapticFeedback.selectionClick();
},
onAfterPopup: () async {
await HapticFeedback.selectionClick();
},
content: StatefulBuilder(
builder: (context, setPopupState) => SizedBox(
width: 280,
@@ -338,7 +346,8 @@ class _CreateGameViewState extends State<CreateGameView> {
children: List.generate(
_rulesets.length,
(index) => GestureDetector(
onTap: () {
onTap: () async {
await HapticFeedback.selectionClick();
setState(() {
selectedRuleset = _rulesets[index].$1;
});
@@ -412,6 +421,12 @@ class _CreateGameViewState extends State<CreateGameView> {
contentPadding: const EdgeInsets.symmetric(horizontal: 0, vertical: 10),
barrierColor: Colors.transparent,
contentDecoration: CustomTheme.standardBoxDecoration,
onBeforePopup: () async {
await HapticFeedback.selectionClick();
},
onAfterPopup: () async {
await HapticFeedback.selectionClick();
},
content: StatefulBuilder(
builder: (context, setPopupState) => SizedBox(
width: 150,
@@ -421,7 +436,8 @@ class _CreateGameViewState extends State<CreateGameView> {
children: List.generate(
_colors.length,
(index) => GestureDetector(
onTap: () {
onTap: () async {
await HapticFeedback.selectionClick();
setState(() {
selectedColor = _colors[index].$1;
});

View File

@@ -172,7 +172,6 @@ class _CreateMatchViewState extends State<CreateMatchView> {
) ??
false,
);
selectedGroup = await Navigator.of(context).push(
adaptivePageRoute(
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/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/widgets/buttons/haptic_icon_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/dialog/custom_alert_dialog.dart';
@@ -60,7 +61,7 @@ class _MatchDetailViewState extends State<MatchDetailView> {
appBar: AppBar(
title: Text(loc.match_profile),
actions: [
IconButton(
HapticIconButton(
icon: const Icon(Icons.delete),
onPressed: () async {
showDialog<bool>(

View File

@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:provider/provider.dart';
import 'package:tallee/core/custom_theme.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/l10n/generated/app_localizations.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_radio_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(
backgroundColor: CustomTheme.backgroundColor,
appBar: AppBar(
leading: IconButton(
leading: HapticIconButton(
icon: const Icon(Icons.close),
onPressed: () {
widget.onWinnerChanged?.call();
@@ -204,6 +206,7 @@ class _MatchResultViewState extends State<MatchResultView> {
: RadioGroup<Player>(
groupValue: _selectedPlayer,
onChanged: (Player? value) async {
await HapticFeedback.selectionClick();
setState(() {
_selectedPlayer = value;
});
@@ -217,6 +220,7 @@ class _MatchResultViewState extends State<MatchResultView> {
text: allPlayers[index].name,
value: allPlayers[index],
onContainerTap: (value) async {
await HapticFeedback.selectionClick();
setState(() {
// Check if the already selected player is the same as the newly tapped player.
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) {
setState(() {
if (newIndex > oldIndex) {

View File

@@ -2,6 +2,7 @@ import 'dart:io';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:intl/intl.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/l10n/generated/app_localizations.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_dialog_action.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),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
spacing: 40,
spacing: 10,
children: [
GestureDetector(
child: const Icon(Icons.language),
onTap: () => {
HapticIconButton(
color: CustomTheme.textColor,
icon: const Icon(Icons.language),
onPressed: () async => {
await HapticFeedback.lightImpact(),
launchUrl(
Uri.parse('https://liquid-dev.de'),
),
},
),
GestureDetector(
child: const FaIcon(FontAwesomeIcons.github),
onTap: () => {
HapticIconButton(
color: CustomTheme.textColor,
icon: const FaIcon(FontAwesomeIcons.github),
onPressed: () async => {
await HapticFeedback.lightImpact(),
launchUrl(
Uri.parse(
'https://github.com/liquiddevelopmentde',
@@ -217,15 +223,19 @@ class _SettingsViewState extends State<SettingsView> {
),
},
),
GestureDetector(
child: Icon(
HapticIconButton(
color: CustomTheme.textColor,
icon: Icon(
Platform.isIOS
? CupertinoIcons.mail_solid
: Icons.email,
),
onTap: () => launchUrl(
Uri.parse('mailto:hi@liquid-dev.de'),
),
onPressed: () async => {
await HapticFeedback.lightImpact(),
launchUrl(
Uri.parse('mailto:hi@liquid-dev.de'),
),
},
),
],
),
@@ -266,21 +276,42 @@ class _SettingsViewState extends State<SettingsView> {
void showImportSnackBar({
required BuildContext context,
required ImportResult result,
}) {
}) async {
final loc = AppLocalizations.of(context);
switch (result) {
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:
showSnackbar(context: context, message: loc.invalid_schema);
await HapticFeedback.errorNotification();
if (context.mounted) {
showSnackbar(context: context, message: loc.invalid_schema);
}
case ImportResult.fileReadError:
showSnackbar(context: context, message: loc.error_reading_file);
await HapticFeedback.errorNotification();
if (context.mounted) {
showSnackbar(context: context, message: loc.error_reading_file);
}
case ImportResult.canceled:
showSnackbar(context: context, message: loc.import_canceled);
await HapticFeedback.errorNotification();
if (context.mounted) {
showSnackbar(context: context, message: loc.import_canceled);
}
case ImportResult.formatException:
showSnackbar(context: context, message: loc.format_exception);
await HapticFeedback.errorNotification();
if (context.mounted) {
showSnackbar(context: context, message: loc.format_exception);
}
case ImportResult.unknownException:
showSnackbar(context: context, message: loc.unknown_exception);
await HapticFeedback.errorNotification();
if (context.mounted) {
showSnackbar(context: context, message: loc.unknown_exception);
}
}
}
@@ -291,15 +322,27 @@ class _SettingsViewState extends State<SettingsView> {
void showExportSnackBar({
required BuildContext context,
required ExportResult result,
}) {
}) async {
final loc = AppLocalizations.of(context);
switch (result) {
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:
showSnackbar(context: context, message: loc.export_canceled);
await HapticFeedback.errorNotification();
if (context.mounted) {
showSnackbar(context: context, message: loc.export_canceled);
}
case ExportResult.unknownException:
showSnackbar(context: context, message: loc.unknown_exception);
await HapticFeedback.errorNotification();
if (context.mounted) {
showSnackbar(context: context, message: loc.unknown_exception);
}
}
}

View File

@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:tallee/core/custom_theme.dart';
import 'package:tallee/core/enums.dart';
@@ -48,7 +49,12 @@ class CustomWidthButton extends StatelessWidget {
)!;
return ElevatedButton(
onPressed: onPressed,
onPressed: onPressed == null
? null
: () async {
await HapticFeedback.selectionClick();
onPressed!.call();
},
style: ElevatedButton.styleFrom(
foregroundColor: textcolor,
disabledForegroundColor: disabledTextColor,
@@ -78,7 +84,12 @@ class CustomWidthButton extends StatelessWidget {
: Color.lerp(CustomTheme.primaryColor, Colors.black, 0.5)!;
return OutlinedButton(
onPressed: onPressed,
onPressed: onPressed == null
? null
: () async {
await HapticFeedback.selectionClick();
onPressed!.call();
},
style: OutlinedButton.styleFrom(
foregroundColor: textcolor,
disabledForegroundColor: disabledTextColor,
@@ -110,7 +121,12 @@ class CustomWidthButton extends StatelessWidget {
disabledBackgroundColor = Colors.transparent;
return TextButton(
onPressed: onPressed,
onPressed: onPressed == null
? null
: () async {
await HapticFeedback.selectionClick();
onPressed!.call();
},
style: TextButton.styleFrom(
foregroundColor: textcolor,
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 'package:flutter/material.dart';
import 'package:flutter/services.dart';
class MainMenuButton extends StatefulWidget {
/// A button for the main menu with an optional icon and a press animation.
@@ -65,19 +66,27 @@ class _MainMenuButtonState extends State<MainMenuButton>
onTapDown: (_) {
_animationController.forward();
if (widget.onLongPressed != null) {
_longPressTimer = Timer(const Duration(milliseconds: 400), () {
_isLongPressing = true;
widget.onLongPressed?.call();
_repeatTimer = Timer.periodic(
const Duration(milliseconds: 250),
(_) => widget.onLongPressed?.call(),
);
});
_longPressTimer = Timer(
const Duration(milliseconds: 400),
() async {
_isLongPressing = true;
widget.onLongPressed?.call();
await HapticFeedback.heavyImpact();
_repeatTimer = Timer.periodic(
const Duration(milliseconds: 250),
(_) async {
widget.onLongPressed?.call();
await HapticFeedback.heavyImpact();
},
);
},
);
}
},
onTapUp: (_) async {
_cancelTimers();
if (mounted && !_isLongPressing) {
await HapticFeedback.selectionClick();
widget.onPressed();
}
_isLongPressing = false;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.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/oss_licenses.dart';
@@ -15,8 +16,10 @@ class LicenseTile extends StatelessWidget {
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () {
Navigator.of(context).push(
onTap: () async {
final navigator = Navigator.of(context);
await HapticFeedback.selectionClick();
navigator.push(
MaterialPageRoute(
builder: (context) => LicenseDetailView(package: package),
),

View File

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

View File

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

View File

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