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),