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..7e5434b 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'; @@ -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 backgroundColor: CustomTheme.backgroundColor, scrolledUnderElevation: 0, actions: [ - IconButton( + HapticIconButton( onPressed: () async { - await Navigator.push( - context, + final navigator = Navigator.of(context); + 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..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 @@ -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 { 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 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..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 @@ -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 { appBar: AppBar( title: Text(loc.group_profile), actions: [ - IconButton( + HapticIconButton( icon: const Icon(Icons.delete), onPressed: () async { showDialog( 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..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 @@ -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( @@ -111,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) { @@ -121,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 3156476..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'; @@ -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 { 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 { 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 { 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 { 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 { 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/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/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, 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..78e1b1b 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'; @@ -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 { 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 { ), }, ), - 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 { 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 { 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); + } } } 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 new file mode 100644 index 0000000..4b672bf --- /dev/null +++ b/lib/presentation/widgets/buttons/haptic_back_button.dart @@ -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(); + }, + ); + } +} 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..f9e2f8b --- /dev/null +++ b/lib/presentation/widgets/buttons/haptic_close_button.dart @@ -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(); + }, + ); + } +} 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..abd8f62 --- /dev/null +++ b/lib/presentation/widgets/buttons/haptic_icon_button.dart @@ -0,0 +1,51 @@ +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.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, + 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..c300eeb 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. @@ -65,19 +66,27 @@ 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; + 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; 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 cdcc2ed..00d6c11 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'; @@ -143,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]); @@ -197,7 +199,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 +297,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/choose_tile.dart b/lib/presentation/widgets/tiles/choose_tile.dart index 41cc7f0..39c3631 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,14 @@ class _ChooseTileState extends State { @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), diff --git a/lib/presentation/widgets/tiles/game_tile.dart b/lib/presentation/widgets/tiles/game_tile.dart index 1d494b9..ee5acf0 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.heavyImpact(); + 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 f4ace65..f6c406e 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,12 @@ class _GroupTileState extends State { @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), 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/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, 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), 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