From 1d20127af4e5fd025a1204bb1131bcf2763131a0 Mon Sep 17 00:00:00 2001 From: Mathis Kirchner Date: Sun, 10 May 2026 23:04:43 +0200 Subject: [PATCH 01/13] feat: add haptic feedback for various user interactions --- lib/core/custom_theme.dart | 7 ++++ lib/main.dart | 1 + .../main_menu/custom_navigation_bar.dart | 9 ++-- .../group_view/create_group_view.dart | 3 ++ .../group_view/group_detail_view.dart | 12 +++++- .../main_menu/group_view/group_view.dart | 2 + .../settings_view/settings_view.dart | 41 +++++++++++++++---- .../widgets/buttons/haptic_back_button.dart | 24 +++++++++++ .../widgets/buttons/haptic_close_button.dart | 24 +++++++++++ .../widgets/player_selection.dart | 6 ++- .../widgets/tiles/group_tile.dart | 6 ++- .../widgets/tiles/license_tile.dart | 7 +++- .../widgets/tiles/settings_list_tile.dart | 6 ++- 13 files changed, 131 insertions(+), 17 deletions(-) create mode 100644 lib/presentation/widgets/buttons/haptic_back_button.dart create mode 100644 lib/presentation/widgets/buttons/haptic_close_button.dart diff --git a/lib/core/custom_theme.dart b/lib/core/custom_theme.dart index b32ce63..bb6d4b4 100644 --- a/lib/core/custom_theme.dart +++ b/lib/core/custom_theme.dart @@ -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( + backButtonIconBuilder: (context) => const HapticBackButton(), + closeButtonIconBuilder: (context) => const HapticCloseButton(), + ); + static const SearchBarThemeData searchBarTheme = SearchBarThemeData( textStyle: WidgetStatePropertyAll(TextStyle(color: textColor)), hintStyle: WidgetStatePropertyAll(TextStyle(color: hintColor)), diff --git a/lib/main.dart b/lib/main.dart index f159ef7..36eab62 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -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, diff --git a/lib/presentation/views/main_menu/custom_navigation_bar.dart b/lib/presentation/views/main_menu/custom_navigation_bar.dart index bf6ded3..366ecbd 100644 --- a/lib/presentation/views/main_menu/custom_navigation_bar.dart +++ b/lib/presentation/views/main_menu/custom_navigation_bar.dart @@ -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'; @@ -55,8 +56,9 @@ class _CustomNavigationBarState extends State actions: [ IconButton( onPressed: () async { - await Navigator.push( - context, + final navigator = Navigator.of(context); + await HapticFeedback.selectionClick(); + await navigator.push( adaptivePageRoute(builder: (_) => const SettingsView()), ); setState(() { @@ -125,7 +127,8 @@ class _CustomNavigationBarState extends State } /// Handles tab tap events. Updates the current [index] state. - void onTabTapped(int index) { + void onTabTapped(int index) async { + await HapticFeedback.selectionClick(); setState(() { currentIndex = index; }); diff --git a/lib/presentation/views/main_menu/group_view/create_group_view.dart b/lib/presentation/views/main_menu/group_view/create_group_view.dart index b4a5b97..55f59aa 100644 --- a/lib/presentation/views/main_menu/group_view/create_group_view.dart +++ b/lib/presentation/views/main_menu/group_view/create_group_view.dart @@ -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,10 @@ class _CreateGroupViewState extends State { if (!mounted) return; if (success) { + await HapticFeedback.successNotification(); Navigator.pop(context, updatedGroup); } else { + await HapticFeedback.errorNotification(); showSnackbar( message: widget.groupToEdit == null ? loc.error_creating_group diff --git a/lib/presentation/views/main_menu/group_view/group_detail_view.dart b/lib/presentation/views/main_menu/group_view/group_detail_view.dart index 3d5e805..746d0ee 100644 --- a/lib/presentation/views/main_menu/group_view/group_detail_view.dart +++ b/lib/presentation/views/main_menu/group_view/group_detail_view.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:intl/intl.dart'; import 'package:provider/provider.dart'; import 'package:tallee/core/adaptive_page_route.dart'; @@ -68,6 +69,7 @@ class _GroupDetailViewState extends State { IconButton( icon: const Icon(Icons.delete), onPressed: () async { + await HapticFeedback.selectionClick(); showDialog( context: context, builder: (context) => CustomAlertDialog( @@ -75,11 +77,17 @@ class _GroupDetailViewState extends State { content: Text(loc.this_cannot_be_undone), actions: [ CustomDialogAction( - onPressed: () => Navigator.of(context).pop(true), + onPressed: () async { + await HapticFeedback.warningNotification(); + Navigator.of(context).pop(true); + }, text: loc.delete, ), CustomDialogAction( - onPressed: () => Navigator.of(context).pop(false), + onPressed: () async { + await HapticFeedback.selectionClick(); + Navigator.of(context).pop(false); + }, buttonType: ButtonType.secondary, text: loc.cancel, ), diff --git a/lib/presentation/views/main_menu/group_view/group_view.dart b/lib/presentation/views/main_menu/group_view/group_view.dart index c8a9398..923344e 100644 --- a/lib/presentation/views/main_menu/group_view/group_view.dart +++ b/lib/presentation/views/main_menu/group_view/group_view.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:provider/provider.dart'; import 'package:tallee/core/adaptive_page_route.dart'; import 'package:tallee/core/constants.dart'; @@ -102,6 +103,7 @@ class _GroupViewState extends State { text: loc.create_group, icon: Icons.group_add, onPressed: () async { + await HapticFeedback.selectionClick(); await Navigator.push( context, adaptivePageRoute( diff --git a/lib/presentation/views/main_menu/settings_view/settings_view.dart b/lib/presentation/views/main_menu/settings_view/settings_view.dart index 8e1cbdc..c393ae5 100644 --- a/lib/presentation/views/main_menu/settings_view/settings_view.dart +++ b/lib/presentation/views/main_menu/settings_view/settings_view.dart @@ -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'; @@ -201,7 +202,8 @@ class _SettingsViewState extends State { children: [ GestureDetector( child: const Icon(Icons.language), - onTap: () => { + onTap: () async => { + await HapticFeedback.lightImpact(), launchUrl( Uri.parse('https://liquid-dev.de'), ), @@ -209,7 +211,8 @@ class _SettingsViewState extends State { ), GestureDetector( child: const FaIcon(FontAwesomeIcons.github), - onTap: () => { + onTap: () async => { + await HapticFeedback.lightImpact(), launchUrl( Uri.parse( 'https://github.com/liquiddevelopmentde', @@ -223,9 +226,12 @@ class _SettingsViewState extends State { ? CupertinoIcons.mail_solid : Icons.email, ), - onTap: () => launchUrl( - Uri.parse('mailto:hi@liquid-dev.de'), - ), + onTap: () async => { + await HapticFeedback.lightImpact(), + launchUrl( + Uri.parse('mailto:hi@liquid-dev.de'), + ), + }, ), ], ), @@ -266,20 +272,38 @@ class _SettingsViewState extends State { void showImportSnackBar({ required BuildContext context, required ImportResult result, - }) { + }) async { final loc = AppLocalizations.of(context); switch (result) { case ImportResult.success: + if (context.mounted) { + await HapticFeedback.successNotification(); + } showSnackbar(context: context, message: loc.data_successfully_imported); case ImportResult.invalidSchema: + if (context.mounted) { + await HapticFeedback.errorNotification(); + } showSnackbar(context: context, message: loc.invalid_schema); case ImportResult.fileReadError: + if (context.mounted) { + await HapticFeedback.errorNotification(); + } showSnackbar(context: context, message: loc.error_reading_file); case ImportResult.canceled: + if (context.mounted) { + await HapticFeedback.errorNotification(); + } showSnackbar(context: context, message: loc.import_canceled); case ImportResult.formatException: + if (context.mounted) { + await HapticFeedback.errorNotification(); + } showSnackbar(context: context, message: loc.format_exception); case ImportResult.unknownException: + if (context.mounted) { + await HapticFeedback.errorNotification(); + } showSnackbar(context: context, message: loc.unknown_exception); } } @@ -291,13 +315,16 @@ class _SettingsViewState extends State { void showExportSnackBar({ required BuildContext context, required ExportResult result, - }) { + }) async { final loc = AppLocalizations.of(context); switch (result) { case ExportResult.success: + await HapticFeedback.successNotification(); showSnackbar(context: context, message: loc.data_successfully_exported); case ExportResult.canceled: + await HapticFeedback.errorNotification(); showSnackbar(context: context, message: loc.export_canceled); + await HapticFeedback.errorNotification(); case ExportResult.unknownException: showSnackbar(context: context, message: loc.unknown_exception); } diff --git a/lib/presentation/widgets/buttons/haptic_back_button.dart b/lib/presentation/widgets/buttons/haptic_back_button.dart new file mode 100644 index 0000000..6f3f76a --- /dev/null +++ b/lib/presentation/widgets/buttons/haptic_back_button.dart @@ -0,0 +1,24 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.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 IconButton( + icon: Icon(iconData), + onPressed: () async { + await HapticFeedback.mediumImpact(); + Navigator.of(context).maybePop(); + }, + ); + } +} diff --git a/lib/presentation/widgets/buttons/haptic_close_button.dart b/lib/presentation/widgets/buttons/haptic_close_button.dart new file mode 100644 index 0000000..bbd8455 --- /dev/null +++ b/lib/presentation/widgets/buttons/haptic_close_button.dart @@ -0,0 +1,24 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.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 IconButton( + icon: Icon(iconData), + onPressed: () async { + await HapticFeedback.mediumImpact(); + Navigator.of(context).maybePop(); + }, + ); + } +} diff --git a/lib/presentation/widgets/player_selection.dart b/lib/presentation/widgets/player_selection.dart index cdcc2ed..b13e098 100644 --- a/lib/presentation/widgets/player_selection.dart +++ b/lib/presentation/widgets/player_selection.dart @@ -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'; @@ -197,7 +198,8 @@ class _PlayerSelectionState extends State { 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 +296,10 @@ class _PlayerSelectionState extends State { if (success) { _handleSuccessfulPlayerCreation(createdPlayer); + await HapticFeedback.successNotification(); showSnackBarMessage(loc.successfully_added_player(playerName)); } else { + await HapticFeedback.errorNotification(); showSnackBarMessage(loc.could_not_add_player(playerName)); } } diff --git a/lib/presentation/widgets/tiles/group_tile.dart b/lib/presentation/widgets/tiles/group_tile.dart index f4ace65..9ce5a6b 100644 --- a/lib/presentation/widgets/tiles/group_tile.dart +++ b/lib/presentation/widgets/tiles/group_tile.dart @@ -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,10 @@ class _GroupTileState extends State { @override Widget build(BuildContext context) { return GestureDetector( - onTap: widget.onTap, + onTap: () async { + await HapticFeedback.selectionClick(); + widget.onTap?.call(); + }, child: AnimatedContainer( margin: CustomTheme.standardMargin, padding: const EdgeInsets.symmetric(vertical: 5, horizontal: 10), diff --git a/lib/presentation/widgets/tiles/license_tile.dart b/lib/presentation/widgets/tiles/license_tile.dart index 9289ed5..b9663d0 100644 --- a/lib/presentation/widgets/tiles/license_tile.dart +++ b/lib/presentation/widgets/tiles/license_tile.dart @@ -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), ), diff --git a/lib/presentation/widgets/tiles/settings_list_tile.dart b/lib/presentation/widgets/tiles/settings_list_tile.dart index de805cd..92a5116 100644 --- a/lib/presentation/widgets/tiles/settings_list_tile.dart +++ b/lib/presentation/widgets/tiles/settings_list_tile.dart @@ -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), From bc59d1d91ce1fbdd65f9e5d18f6a34ef24f1e401 Mon Sep 17 00:00:00 2001 From: Mathis Kirchner Date: Mon, 11 May 2026 10:27:35 +0200 Subject: [PATCH 02/13] feat: add haptic feedback to more user interactions --- .../main_menu/custom_navigation_bar.dart | 3 +- .../group_view/create_group_view.dart | 8 ++- .../group_view/group_detail_view.dart | 15 ++---- .../main_menu/group_view/group_view.dart | 2 - .../create_match/choose_game_view.dart | 5 +- .../create_match/choose_group_view.dart | 3 +- .../create_match/create_game_view.dart | 3 +- .../match_view/match_detail_view.dart | 3 +- .../match_view/match_result_view.dart | 12 ++++- .../settings_view/settings_view.dart | 44 +++++++++------ .../widgets/buttons/custom_width_button.dart | 22 ++++++-- .../widgets/buttons/haptic_back_button.dart | 5 +- .../widgets/buttons/haptic_close_button.dart | 5 +- .../widgets/buttons/haptic_icon_button.dart | 54 +++++++++++++++++++ .../widgets/buttons/main_menu_button.dart | 2 + .../widgets/dialog/custom_dialog_action.dart | 6 ++- .../widgets/player_selection.dart | 3 +- .../widgets/tiles/choose_tile.dart | 8 ++- .../custom_checkbox_list_tile.dart | 9 +++- .../widgets/tiles/match_tile.dart | 6 ++- 20 files changed, 165 insertions(+), 53 deletions(-) create mode 100644 lib/presentation/widgets/buttons/haptic_icon_button.dart diff --git a/lib/presentation/views/main_menu/custom_navigation_bar.dart b/lib/presentation/views/main_menu/custom_navigation_bar.dart index 366ecbd..6e9e6d0 100644 --- a/lib/presentation/views/main_menu/custom_navigation_bar.dart +++ b/lib/presentation/views/main_menu/custom_navigation_bar.dart @@ -7,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 { @@ -54,7 +55,7 @@ class _CustomNavigationBarState extends State backgroundColor: CustomTheme.backgroundColor, scrolledUnderElevation: 0, actions: [ - IconButton( + HapticIconButton( onPressed: () async { final navigator = Navigator.of(context); await HapticFeedback.selectionClick(); diff --git a/lib/presentation/views/main_menu/group_view/create_group_view.dart b/lib/presentation/views/main_menu/group_view/create_group_view.dart index 55f59aa..84efbe1 100644 --- a/lib/presentation/views/main_menu/group_view/create_group_view.dart +++ b/lib/presentation/views/main_menu/group_view/create_group_view.dart @@ -135,9 +135,13 @@ class _CreateGroupViewState extends State { if (success) { await HapticFeedback.successNotification(); - Navigator.pop(context, updatedGroup); + if (mounted) { + Navigator.pop(context, updatedGroup); + } } else { - await HapticFeedback.errorNotification(); + if (mounted) { + await HapticFeedback.errorNotification(); + } showSnackbar( message: widget.groupToEdit == null ? loc.error_creating_group diff --git a/lib/presentation/views/main_menu/group_view/group_detail_view.dart b/lib/presentation/views/main_menu/group_view/group_detail_view.dart index 746d0ee..c417ec4 100644 --- a/lib/presentation/views/main_menu/group_view/group_detail_view.dart +++ b/lib/presentation/views/main_menu/group_view/group_detail_view.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:intl/intl.dart'; import 'package:provider/provider.dart'; import 'package:tallee/core/adaptive_page_route.dart'; @@ -13,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'; @@ -66,10 +66,9 @@ class _GroupDetailViewState extends State { appBar: AppBar( title: Text(loc.group_profile), actions: [ - IconButton( + HapticIconButton( icon: const Icon(Icons.delete), onPressed: () async { - await HapticFeedback.selectionClick(); showDialog( context: context, builder: (context) => CustomAlertDialog( @@ -77,17 +76,11 @@ class _GroupDetailViewState extends State { content: Text(loc.this_cannot_be_undone), actions: [ CustomDialogAction( - onPressed: () async { - await HapticFeedback.warningNotification(); - Navigator.of(context).pop(true); - }, + onPressed: () => Navigator.of(context).pop(true), text: loc.delete, ), CustomDialogAction( - onPressed: () async { - await HapticFeedback.selectionClick(); - Navigator.of(context).pop(false); - }, + onPressed: () => Navigator.of(context).pop(false), buttonType: ButtonType.secondary, text: loc.cancel, ), diff --git a/lib/presentation/views/main_menu/group_view/group_view.dart b/lib/presentation/views/main_menu/group_view/group_view.dart index 923344e..c8a9398 100644 --- a/lib/presentation/views/main_menu/group_view/group_view.dart +++ b/lib/presentation/views/main_menu/group_view/group_view.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:provider/provider.dart'; import 'package:tallee/core/adaptive_page_route.dart'; import 'package:tallee/core/constants.dart'; @@ -103,7 +102,6 @@ class _GroupViewState extends State { text: loc.create_group, icon: Icons.group_add, onPressed: () async { - await HapticFeedback.selectionClick(); await Navigator.push( context, adaptivePageRoute( diff --git a/lib/presentation/views/main_menu/match_view/create_match/choose_game_view.dart b/lib/presentation/views/main_menu/match_view/create_match/choose_game_view.dart index c019213..3c51cab 100644 --- a/lib/presentation/views/main_menu/match_view/create_match/choose_game_view.dart +++ b/lib/presentation/views/main_menu/match_view/create_match/choose_game_view.dart @@ -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 { 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 { }, ), actions: [ - IconButton( + HapticIconButton( icon: const Icon(Icons.add), onPressed: () async { final result = await Navigator.push( diff --git a/lib/presentation/views/main_menu/match_view/create_match/choose_group_view.dart b/lib/presentation/views/main_menu/match_view/create_match/choose_group_view.dart index c7471d8..1caa101 100644 --- a/lib/presentation/views/main_menu/match_view/create_match/choose_group_view.dart +++ b/lib/presentation/views/main_menu/match_view/create_match/choose_group_view.dart @@ -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 { backgroundColor: CustomTheme.backgroundColor, resizeToAvoidBottomInset: false, appBar: AppBar( - leading: IconButton( + leading: HapticIconButton( icon: const Icon(Icons.arrow_back_ios), onPressed: () { Navigator.of(context).pop( diff --git a/lib/presentation/views/main_menu/match_view/create_match/create_game_view.dart b/lib/presentation/views/main_menu/match_view/create_match/create_game_view.dart index 3156476..4b4a977 100644 --- a/lib/presentation/views/main_menu/match_view/create_match/create_game_view.dart +++ b/lib/presentation/views/main_menu/match_view/create_match/create_game_view.dart @@ -12,6 +12,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 +121,7 @@ class _CreateGameViewState extends State { 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; diff --git a/lib/presentation/views/main_menu/match_view/match_detail_view.dart b/lib/presentation/views/main_menu/match_view/match_detail_view.dart index 73d534d..b34f9df 100644 --- a/lib/presentation/views/main_menu/match_view/match_detail_view.dart +++ b/lib/presentation/views/main_menu/match_view/match_detail_view.dart @@ -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 { appBar: AppBar( title: Text(loc.match_profile), actions: [ - IconButton( + HapticIconButton( icon: const Icon(Icons.delete), onPressed: () async { showDialog( diff --git a/lib/presentation/views/main_menu/match_view/match_result_view.dart b/lib/presentation/views/main_menu/match_view/match_result_view.dart index ba138d6..47f91c5 100644 --- a/lib/presentation/views/main_menu/match_view/match_result_view.dart +++ b/lib/presentation/views/main_menu/match_view/match_result_view.dart @@ -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 { 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 { : RadioGroup( groupValue: _selectedPlayer, onChanged: (Player? value) async { + await HapticFeedback.selectionClick(); setState(() { _selectedPlayer = value; }); @@ -217,6 +220,7 @@ class _MatchResultViewState extends State { 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 { }, ); }, + onReorderStart: (int n) async { + await HapticFeedback.selectionClick(); + }, + onReorderEnd: (int n) async { + await HapticFeedback.selectionClick(); + }, onReorder: (int oldIndex, int newIndex) { setState(() { if (newIndex > oldIndex) { diff --git a/lib/presentation/views/main_menu/settings_view/settings_view.dart b/lib/presentation/views/main_menu/settings_view/settings_view.dart index c393ae5..d873b7a 100644 --- a/lib/presentation/views/main_menu/settings_view/settings_view.dart +++ b/lib/presentation/views/main_menu/settings_view/settings_view.dart @@ -276,35 +276,38 @@ class _SettingsViewState extends State { final loc = AppLocalizations.of(context); switch (result) { case ImportResult.success: + await HapticFeedback.successNotification(); if (context.mounted) { - await HapticFeedback.successNotification(); + showSnackbar( + context: context, + message: loc.data_successfully_imported, + ); } - showSnackbar(context: context, message: loc.data_successfully_imported); case ImportResult.invalidSchema: + await HapticFeedback.errorNotification(); if (context.mounted) { - await HapticFeedback.errorNotification(); + showSnackbar(context: context, message: loc.invalid_schema); } - showSnackbar(context: context, message: loc.invalid_schema); case ImportResult.fileReadError: + await HapticFeedback.errorNotification(); if (context.mounted) { - await HapticFeedback.errorNotification(); + showSnackbar(context: context, message: loc.error_reading_file); } - showSnackbar(context: context, message: loc.error_reading_file); case ImportResult.canceled: + await HapticFeedback.errorNotification(); if (context.mounted) { - await HapticFeedback.errorNotification(); + showSnackbar(context: context, message: loc.import_canceled); } - showSnackbar(context: context, message: loc.import_canceled); case ImportResult.formatException: + await HapticFeedback.errorNotification(); if (context.mounted) { - await HapticFeedback.errorNotification(); + showSnackbar(context: context, message: loc.format_exception); } - showSnackbar(context: context, message: loc.format_exception); case ImportResult.unknownException: + await HapticFeedback.errorNotification(); if (context.mounted) { - await HapticFeedback.errorNotification(); + showSnackbar(context: context, message: loc.unknown_exception); } - showSnackbar(context: context, message: loc.unknown_exception); } } @@ -320,13 +323,22 @@ class _SettingsViewState extends State { switch (result) { case ExportResult.success: await HapticFeedback.successNotification(); - showSnackbar(context: context, message: loc.data_successfully_exported); + if (context.mounted) { + showSnackbar( + context: context, + message: loc.data_successfully_exported, + ); + } case ExportResult.canceled: await HapticFeedback.errorNotification(); - 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); + } } } diff --git a/lib/presentation/widgets/buttons/custom_width_button.dart b/lib/presentation/widgets/buttons/custom_width_button.dart index 4fde6f8..556b784 100644 --- a/lib/presentation/widgets/buttons/custom_width_button.dart +++ b/lib/presentation/widgets/buttons/custom_width_button.dart @@ -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, diff --git a/lib/presentation/widgets/buttons/haptic_back_button.dart b/lib/presentation/widgets/buttons/haptic_back_button.dart index 6f3f76a..4b672bf 100644 --- a/lib/presentation/widgets/buttons/haptic_back_button.dart +++ b/lib/presentation/widgets/buttons/haptic_back_button.dart @@ -1,6 +1,6 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; +import 'package:tallee/presentation/widgets/buttons/haptic_icon_button.dart'; class HapticBackButton extends StatelessWidget { const HapticBackButton({super.key}); @@ -13,10 +13,9 @@ class HapticBackButton extends StatelessWidget { _ => Icons.arrow_back_rounded, }; - return IconButton( + return HapticIconButton( icon: Icon(iconData), onPressed: () async { - await HapticFeedback.mediumImpact(); Navigator.of(context).maybePop(); }, ); diff --git a/lib/presentation/widgets/buttons/haptic_close_button.dart b/lib/presentation/widgets/buttons/haptic_close_button.dart index bbd8455..f9e2f8b 100644 --- a/lib/presentation/widgets/buttons/haptic_close_button.dart +++ b/lib/presentation/widgets/buttons/haptic_close_button.dart @@ -1,7 +1,7 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; +import 'package:tallee/presentation/widgets/buttons/haptic_icon_button.dart'; class HapticCloseButton extends StatelessWidget { const HapticCloseButton({super.key}); @@ -13,10 +13,9 @@ class HapticCloseButton extends StatelessWidget { _ => Icons.close_rounded, }; - return IconButton( + return HapticIconButton( icon: Icon(iconData), onPressed: () async { - await HapticFeedback.mediumImpact(); Navigator.of(context).maybePop(); }, ); diff --git a/lib/presentation/widgets/buttons/haptic_icon_button.dart b/lib/presentation/widgets/buttons/haptic_icon_button.dart new file mode 100644 index 0000000..8da8e9f --- /dev/null +++ b/lib/presentation/widgets/buttons/haptic_icon_button.dart @@ -0,0 +1,54 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +class HapticIconButton extends StatelessWidget { + const HapticIconButton({ + super.key, + required this.icon, + required this.onPressed, + this.tooltip, + this.iconSize, + this.color, + this.padding, + this.alignment, + this.constraints, + this.style, + this.isSelected, + this.selectedIcon, + }); + + final Widget icon; + final VoidCallback? onPressed; + + final String? tooltip; + 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( + tooltip: tooltip, + iconSize: iconSize, + color: color, + padding: padding, + alignment: alignment ?? Alignment.center, + constraints: constraints, + style: style, + isSelected: isSelected, + selectedIcon: selectedIcon, + icon: icon, + onPressed: onPressed == null + ? null + : () async { + await HapticFeedback.selectionClick(); + onPressed!.call(); + }, + ); + } +} diff --git a/lib/presentation/widgets/buttons/main_menu_button.dart b/lib/presentation/widgets/buttons/main_menu_button.dart index c5c7a34..708c0f4 100644 --- a/lib/presentation/widgets/buttons/main_menu_button.dart +++ b/lib/presentation/widgets/buttons/main_menu_button.dart @@ -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. @@ -78,6 +79,7 @@ class _MainMenuButtonState extends State onTapUp: (_) async { _cancelTimers(); if (mounted && !_isLongPressing) { + await HapticFeedback.selectionClick(); widget.onPressed(); } _isLongPressing = false; diff --git a/lib/presentation/widgets/dialog/custom_dialog_action.dart b/lib/presentation/widgets/dialog/custom_dialog_action.dart index 47024dc..26dc40d 100644 --- a/lib/presentation/widgets/dialog/custom_dialog_action.dart +++ b/lib/presentation/widgets/dialog/custom_dialog_action.dart @@ -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, diff --git a/lib/presentation/widgets/player_selection.dart b/lib/presentation/widgets/player_selection.dart index b13e098..00d6c11 100644 --- a/lib/presentation/widgets/player_selection.dart +++ b/lib/presentation/widgets/player_selection.dart @@ -144,7 +144,8 @@ class _PlayerSelectionState extends State { 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]); diff --git a/lib/presentation/widgets/tiles/choose_tile.dart b/lib/presentation/widgets/tiles/choose_tile.dart index 41cc7f0..f69aefc 100644 --- a/lib/presentation/widgets/tiles/choose_tile.dart +++ b/lib/presentation/widgets/tiles/choose_tile.dart @@ -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,12 @@ class _ChooseTileState extends State { @override Widget build(BuildContext context) { return GestureDetector( - onTap: widget.onPressed, + onTap: () async { + await HapticFeedback.vibrate(); + if (widget.onPressed != null) { + widget.onPressed!.call(); + } + }, child: Container( margin: CustomTheme.tileMargin, padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), diff --git a/lib/presentation/widgets/tiles/match_result_view/custom_checkbox_list_tile.dart b/lib/presentation/widgets/tiles/match_result_view/custom_checkbox_list_tile.dart index 77c9242..bb6c933 100644 --- a/lib/presentation/widgets/tiles/match_result_view/custom_checkbox_list_tile.dart +++ b/lib/presentation/widgets/tiles/match_result_view/custom_checkbox_list_tile.dart @@ -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); }, diff --git a/lib/presentation/widgets/tiles/match_tile.dart b/lib/presentation/widgets/tiles/match_tile.dart index 018c896..6a81dc3 100644 --- a/lib/presentation/widgets/tiles/match_tile.dart +++ b/lib/presentation/widgets/tiles/match_tile.dart @@ -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 { 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, From f1899bfe449185187fbc00c01660f13599cf5db5 Mon Sep 17 00:00:00 2001 From: Mathis Kirchner Date: Mon, 11 May 2026 10:59:48 +0200 Subject: [PATCH 03/13] feat: add haptic feedback to even more user interactions --- .../create_match/choose_group_view.dart | 10 ++++------ .../create_match/create_game_view.dart | 19 +++++++++++++++++-- .../create_match/create_match_view.dart | 1 - .../widgets/tiles/choose_tile.dart | 14 ++++++++------ lib/presentation/widgets/tiles/game_tile.dart | 15 +++++++++++++-- .../widgets/tiles/group_tile.dart | 4 +++- 6 files changed, 45 insertions(+), 18 deletions(-) diff --git a/lib/presentation/views/main_menu/match_view/create_match/choose_group_view.dart b/lib/presentation/views/main_menu/match_view/create_match/choose_group_view.dart index 1caa101..2ef8b68 100644 --- a/lib/presentation/views/main_menu/match_view/create_match/choose_group_view.dart +++ b/lib/presentation/views/main_menu/match_view/create_match/choose_group_view.dart @@ -112,7 +112,10 @@ class _ChooseGroupViewState extends State { 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) { @@ -122,11 +125,6 @@ class _ChooseGroupViewState extends State { } }); }, - child: GroupTile( - group: filteredGroups[index], - isHighlighted: - selectedGroupId == filteredGroups[index].id, - ), ); }, ), diff --git a/lib/presentation/views/main_menu/match_view/create_match/create_game_view.dart b/lib/presentation/views/main_menu/match_view/create_match/create_game_view.dart index 4b4a977..998f4e1 100644 --- a/lib/presentation/views/main_menu/match_view/create_match/create_game_view.dart +++ b/lib/presentation/views/main_menu/match_view/create_match/create_game_view.dart @@ -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'; @@ -330,6 +331,12 @@ class _CreateGameViewState extends State { 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, @@ -339,7 +346,8 @@ class _CreateGameViewState extends State { children: List.generate( _rulesets.length, (index) => GestureDetector( - onTap: () { + onTap: () async { + await HapticFeedback.selectionClick(); setState(() { selectedRuleset = _rulesets[index].$1; }); @@ -413,6 +421,12 @@ class _CreateGameViewState extends State { 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, @@ -422,7 +436,8 @@ class _CreateGameViewState extends State { children: List.generate( _colors.length, (index) => GestureDetector( - onTap: () { + onTap: () async { + await HapticFeedback.selectionClick(); setState(() { selectedColor = _colors[index].$1; }); diff --git a/lib/presentation/views/main_menu/match_view/create_match/create_match_view.dart b/lib/presentation/views/main_menu/match_view/create_match/create_match_view.dart index fd98691..85bb936 100644 --- a/lib/presentation/views/main_menu/match_view/create_match/create_match_view.dart +++ b/lib/presentation/views/main_menu/match_view/create_match/create_match_view.dart @@ -172,7 +172,6 @@ class _CreateMatchViewState extends State { ) ?? false, ); - selectedGroup = await Navigator.of(context).push( adaptivePageRoute( builder: (context) => ChooseGroupView( diff --git a/lib/presentation/widgets/tiles/choose_tile.dart b/lib/presentation/widgets/tiles/choose_tile.dart index f69aefc..39c3631 100644 --- a/lib/presentation/widgets/tiles/choose_tile.dart +++ b/lib/presentation/widgets/tiles/choose_tile.dart @@ -31,12 +31,14 @@ class _ChooseTileState extends State { @override Widget build(BuildContext context) { return GestureDetector( - onTap: () async { - await HapticFeedback.vibrate(); - if (widget.onPressed != null) { - widget.onPressed!.call(); - } - }, + 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), diff --git a/lib/presentation/widgets/tiles/game_tile.dart b/lib/presentation/widgets/tiles/game_tile.dart index 1d494b9..80881d0 100644 --- a/lib/presentation/widgets/tiles/game_tile.dart +++ b/lib/presentation/widgets/tiles/game_tile.dart @@ -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.vibrate(); + if (onLongPress != null) { + onLongPress!.call(); + } + }, child: AnimatedContainer( margin: const EdgeInsets.symmetric(vertical: 10, horizontal: 10), decoration: !isHighlighted diff --git a/lib/presentation/widgets/tiles/group_tile.dart b/lib/presentation/widgets/tiles/group_tile.dart index 9ce5a6b..f6c406e 100644 --- a/lib/presentation/widgets/tiles/group_tile.dart +++ b/lib/presentation/widgets/tiles/group_tile.dart @@ -36,7 +36,9 @@ class _GroupTileState extends State { return GestureDetector( onTap: () async { await HapticFeedback.selectionClick(); - widget.onTap?.call(); + if (widget.onTap != null) { + widget.onTap!.call(); + } }, child: AnimatedContainer( margin: CustomTheme.standardMargin, From b82cca939fc64a8037e74e65f8abb563c8b0f633 Mon Sep 17 00:00:00 2001 From: Mathis Kirchner Date: Wed, 13 May 2026 12:37:11 +0200 Subject: [PATCH 04/13] remove unnecessary click --- lib/presentation/views/main_menu/custom_navigation_bar.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/presentation/views/main_menu/custom_navigation_bar.dart b/lib/presentation/views/main_menu/custom_navigation_bar.dart index 6e9e6d0..7e5434b 100644 --- a/lib/presentation/views/main_menu/custom_navigation_bar.dart +++ b/lib/presentation/views/main_menu/custom_navigation_bar.dart @@ -58,7 +58,6 @@ class _CustomNavigationBarState extends State HapticIconButton( onPressed: () async { final navigator = Navigator.of(context); - await HapticFeedback.selectionClick(); await navigator.push( adaptivePageRoute(builder: (_) => const SettingsView()), ); From 7e35ccae6bc4d91903afd4fce897524b3ea68926 Mon Sep 17 00:00:00 2001 From: Mathis Kirchner Date: Wed, 13 May 2026 12:37:19 +0200 Subject: [PATCH 05/13] add vibration to long tap --- .../widgets/buttons/main_menu_button.dart | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/lib/presentation/widgets/buttons/main_menu_button.dart b/lib/presentation/widgets/buttons/main_menu_button.dart index 708c0f4..497fea0 100644 --- a/lib/presentation/widgets/buttons/main_menu_button.dart +++ b/lib/presentation/widgets/buttons/main_menu_button.dart @@ -66,14 +66,18 @@ class _MainMenuButtonState extends State 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; + await HapticFeedback.vibrate(); + widget.onLongPressed?.call(); + _repeatTimer = Timer.periodic( + const Duration(milliseconds: 250), + (_) => widget.onLongPressed?.call(), + ); + }, + ); } }, onTapUp: (_) async { From a8369249b41fff05df941d5fbc84354611ee8ab2 Mon Sep 17 00:00:00 2001 From: Mathis Kirchner Date: Wed, 13 May 2026 12:44:39 +0200 Subject: [PATCH 06/13] change settings_view buttons to haptic icon button --- .../settings_view/settings_view.dart | 24 +++++++++++-------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/lib/presentation/views/main_menu/settings_view/settings_view.dart b/lib/presentation/views/main_menu/settings_view/settings_view.dart index d873b7a..78e1b1b 100644 --- a/lib/presentation/views/main_menu/settings_view/settings_view.dart +++ b/lib/presentation/views/main_menu/settings_view/settings_view.dart @@ -10,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'; @@ -198,20 +199,22 @@ class _SettingsViewState extends State { padding: const EdgeInsets.only(bottom: 12), child: Row( mainAxisAlignment: MainAxisAlignment.center, - spacing: 40, + spacing: 10, children: [ - GestureDetector( - child: const Icon(Icons.language), - onTap: () async => { + 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: () async => { + HapticIconButton( + color: CustomTheme.textColor, + icon: const FaIcon(FontAwesomeIcons.github), + onPressed: () async => { await HapticFeedback.lightImpact(), launchUrl( Uri.parse( @@ -220,13 +223,14 @@ class _SettingsViewState extends State { ), }, ), - GestureDetector( - child: Icon( + HapticIconButton( + color: CustomTheme.textColor, + icon: Icon( Platform.isIOS ? CupertinoIcons.mail_solid : Icons.email, ), - onTap: () async => { + onPressed: () async => { await HapticFeedback.lightImpact(), launchUrl( Uri.parse('mailto:hi@liquid-dev.de'), From f5fbb3ecc4326fcd7f5085da6114833cdeec6ced Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Wed, 13 May 2026 13:48:44 +0200 Subject: [PATCH 07/13] fix: long press feedback on every call --- lib/presentation/widgets/buttons/main_menu_button.dart | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/lib/presentation/widgets/buttons/main_menu_button.dart b/lib/presentation/widgets/buttons/main_menu_button.dart index 497fea0..c300eeb 100644 --- a/lib/presentation/widgets/buttons/main_menu_button.dart +++ b/lib/presentation/widgets/buttons/main_menu_button.dart @@ -70,11 +70,14 @@ class _MainMenuButtonState extends State const Duration(milliseconds: 400), () async { _isLongPressing = true; - await HapticFeedback.vibrate(); widget.onLongPressed?.call(); + await HapticFeedback.heavyImpact(); _repeatTimer = Timer.periodic( const Duration(milliseconds: 250), - (_) => widget.onLongPressed?.call(), + (_) async { + widget.onLongPressed?.call(); + await HapticFeedback.heavyImpact(); + }, ); }, ); From e2882c1c6a9ce65dbcd30be61fa58c041a3fdb51 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Wed, 13 May 2026 13:54:01 +0200 Subject: [PATCH 08/13] changed long press vibration --- lib/presentation/widgets/tiles/game_tile.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/presentation/widgets/tiles/game_tile.dart b/lib/presentation/widgets/tiles/game_tile.dart index 80881d0..ee5acf0 100644 --- a/lib/presentation/widgets/tiles/game_tile.dart +++ b/lib/presentation/widgets/tiles/game_tile.dart @@ -61,7 +61,7 @@ class GameTile extends StatelessWidget { } }, onLongPress: () async { - await HapticFeedback.vibrate(); + await HapticFeedback.heavyImpact(); if (onLongPress != null) { onLongPress!.call(); } From f87a873362253af920b5eb0db5fac5d0a1ac0b0c Mon Sep 17 00:00:00 2001 From: Mathis Kirchner Date: Wed, 13 May 2026 15:38:19 +0200 Subject: [PATCH 09/13] remove tooltip attribute --- lib/presentation/widgets/buttons/haptic_icon_button.dart | 3 --- 1 file changed, 3 deletions(-) diff --git a/lib/presentation/widgets/buttons/haptic_icon_button.dart b/lib/presentation/widgets/buttons/haptic_icon_button.dart index 8da8e9f..f85800c 100644 --- a/lib/presentation/widgets/buttons/haptic_icon_button.dart +++ b/lib/presentation/widgets/buttons/haptic_icon_button.dart @@ -6,7 +6,6 @@ class HapticIconButton extends StatelessWidget { super.key, required this.icon, required this.onPressed, - this.tooltip, this.iconSize, this.color, this.padding, @@ -20,7 +19,6 @@ class HapticIconButton extends StatelessWidget { final Widget icon; final VoidCallback? onPressed; - final String? tooltip; final double? iconSize; final Color? color; final EdgeInsetsGeometry? padding; @@ -33,7 +31,6 @@ class HapticIconButton extends StatelessWidget { @override Widget build(BuildContext context) { return IconButton( - tooltip: tooltip, iconSize: iconSize, color: color, padding: padding, From 0d497236b0514fd816697c33047340020f90cdf2 Mon Sep 17 00:00:00 2001 From: Mathis Kirchner Date: Wed, 13 May 2026 15:55:38 +0200 Subject: [PATCH 10/13] remove space --- lib/presentation/widgets/buttons/haptic_icon_button.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/presentation/widgets/buttons/haptic_icon_button.dart b/lib/presentation/widgets/buttons/haptic_icon_button.dart index f85800c..81c2cf0 100644 --- a/lib/presentation/widgets/buttons/haptic_icon_button.dart +++ b/lib/presentation/widgets/buttons/haptic_icon_button.dart @@ -18,7 +18,6 @@ class HapticIconButton extends StatelessWidget { final Widget icon; final VoidCallback? onPressed; - final double? iconSize; final Color? color; final EdgeInsetsGeometry? padding; From f5b24cb923a5b019f26ed7776f9db545574dd058 Mon Sep 17 00:00:00 2001 From: Mathis Kirchner Date: Wed, 13 May 2026 16:31:24 +0200 Subject: [PATCH 11/13] disable button splash --- lib/presentation/widgets/buttons/haptic_icon_button.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/presentation/widgets/buttons/haptic_icon_button.dart b/lib/presentation/widgets/buttons/haptic_icon_button.dart index 81c2cf0..abd8f62 100644 --- a/lib/presentation/widgets/buttons/haptic_icon_button.dart +++ b/lib/presentation/widgets/buttons/haptic_icon_button.dart @@ -31,6 +31,7 @@ class HapticIconButton extends StatelessWidget { Widget build(BuildContext context) { return IconButton( iconSize: iconSize, + highlightColor: Colors.transparent, //disable splash animation color: color, padding: padding, alignment: alignment ?? Alignment.center, From 1d1c02a575bfae999ee882904a4991ce649d5fda Mon Sep 17 00:00:00 2001 From: "Gitea Actions [bot]" Date: Thu, 14 May 2026 13:09:54 +0000 Subject: [PATCH 12/13] Updated version number [skip ci] --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 8857c57..cb0bb83 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: tallee description: "Tracking App for Card Games" publish_to: 'none' -version: 0.0.32+266 +version: 0.0.33+267 environment: sdk: ^3.8.1 From 31dc899741aa6d89aad9758b6d90ed25cfe7a849 Mon Sep 17 00:00:00 2001 From: "Gitea Actions [bot]" Date: Thu, 14 May 2026 13:10:36 +0000 Subject: [PATCH 13/13] Updated licenses [skip ci] --- .../views/main_menu/settings_view/licenses/oss_licenses.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/presentation/views/main_menu/settings_view/licenses/oss_licenses.dart b/lib/presentation/views/main_menu/settings_view/licenses/oss_licenses.dart index 1cab79e..7e699f6 100644 --- a/lib/presentation/views/main_menu/settings_view/licenses/oss_licenses.dart +++ b/lib/presentation/views/main_menu/settings_view/licenses/oss_licenses.dart @@ -37811,12 +37811,12 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.''', ); -/// tallee 0.0.32+266 +/// tallee 0.0.33+267 const _tallee = Package( name: 'tallee', description: 'Tracking App for Card Games', authors: [], - version: '0.0.32+266', + version: '0.0.33+267', spdxIdentifiers: ['LGPL-3.0'], isMarkdown: false, isSdk: false,