Merge pull request 'Haptisches Feedback hinzufügen' (#216) from feature/215-haptisches-feedback-hinzufügen into development
All checks were successful
Push Pipeline / test (push) Successful in 47s
Push Pipeline / update_version (push) Successful in 6s
Push Pipeline / generate_licenses (push) Successful in 39s
Push Pipeline / format (push) Successful in 1m6s
Push Pipeline / build (push) Successful in 5m33s

Reviewed-on: #216
Reviewed-by: Felix Kirchner <flixcoo@noreply.git.yannick-weigert.de>
This commit was merged in pull request #216.
This commit is contained in:
2026-05-14 13:09:01 +00:00
26 changed files with 328 additions and 68 deletions

View File

@@ -1,4 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:tallee/presentation/widgets/buttons/haptic_back_button.dart';
import 'package:tallee/presentation/widgets/buttons/haptic_close_button.dart';
/// Theme class that defines colors, border radius, padding, and decorations /// Theme class that defines colors, border radius, padding, and decorations
class CustomTheme { class CustomTheme {
@@ -83,6 +85,11 @@ class CustomTheme {
iconTheme: IconThemeData(color: textColor), iconTheme: IconThemeData(color: textColor),
); );
static final ActionIconThemeData actionIconTheme = ActionIconThemeData(
backButtonIconBuilder: (context) => const HapticBackButton(),
closeButtonIconBuilder: (context) => const HapticCloseButton(),
);
static const SearchBarThemeData searchBarTheme = SearchBarThemeData( static const SearchBarThemeData searchBarTheme = SearchBarThemeData(
textStyle: WidgetStatePropertyAll(TextStyle(color: textColor)), textStyle: WidgetStatePropertyAll(TextStyle(color: textColor)),
hintStyle: WidgetStatePropertyAll(TextStyle(color: hintColor)), hintStyle: WidgetStatePropertyAll(TextStyle(color: hintColor)),

View File

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

View File

@@ -1,4 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:tallee/core/adaptive_page_route.dart'; import 'package:tallee/core/adaptive_page_route.dart';
import 'package:tallee/core/custom_theme.dart'; import 'package:tallee/core/custom_theme.dart';
import 'package:tallee/l10n/generated/app_localizations.dart'; import 'package:tallee/l10n/generated/app_localizations.dart';
@@ -6,6 +7,7 @@ import 'package:tallee/presentation/views/main_menu/group_view/group_view.dart';
import 'package:tallee/presentation/views/main_menu/match_view/match_view.dart'; import 'package:tallee/presentation/views/main_menu/match_view/match_view.dart';
import 'package:tallee/presentation/views/main_menu/settings_view/settings_view.dart'; import 'package:tallee/presentation/views/main_menu/settings_view/settings_view.dart';
import 'package:tallee/presentation/views/main_menu/statistics_view.dart'; import 'package:tallee/presentation/views/main_menu/statistics_view.dart';
import 'package:tallee/presentation/widgets/buttons/haptic_icon_button.dart';
import 'package:tallee/presentation/widgets/navbar_item.dart'; import 'package:tallee/presentation/widgets/navbar_item.dart';
class CustomNavigationBar extends StatefulWidget { class CustomNavigationBar extends StatefulWidget {
@@ -53,10 +55,10 @@ class _CustomNavigationBarState extends State<CustomNavigationBar>
backgroundColor: CustomTheme.backgroundColor, backgroundColor: CustomTheme.backgroundColor,
scrolledUnderElevation: 0, scrolledUnderElevation: 0,
actions: [ actions: [
IconButton( HapticIconButton(
onPressed: () async { onPressed: () async {
await Navigator.push( final navigator = Navigator.of(context);
context, await navigator.push(
adaptivePageRoute(builder: (_) => const SettingsView()), adaptivePageRoute(builder: (_) => const SettingsView()),
); );
setState(() { setState(() {
@@ -125,7 +127,8 @@ class _CustomNavigationBarState extends State<CustomNavigationBar>
} }
/// Handles tab tap events. Updates the current [index] state. /// Handles tab tap events. Updates the current [index] state.
void onTabTapped(int index) { void onTabTapped(int index) async {
await HapticFeedback.selectionClick();
setState(() { setState(() {
currentIndex = index; currentIndex = index;
}); });

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,6 +2,7 @@ import 'dart:io';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:package_info_plus/package_info_plus.dart'; import 'package:package_info_plus/package_info_plus.dart';
@@ -9,6 +10,7 @@ import 'package:tallee/core/custom_theme.dart';
import 'package:tallee/core/enums.dart'; import 'package:tallee/core/enums.dart';
import 'package:tallee/l10n/generated/app_localizations.dart'; import 'package:tallee/l10n/generated/app_localizations.dart';
import 'package:tallee/presentation/views/main_menu/settings_view/licenses_view.dart'; import 'package:tallee/presentation/views/main_menu/settings_view/licenses_view.dart';
import 'package:tallee/presentation/widgets/buttons/haptic_icon_button.dart';
import 'package:tallee/presentation/widgets/dialog/custom_alert_dialog.dart'; import 'package:tallee/presentation/widgets/dialog/custom_alert_dialog.dart';
import 'package:tallee/presentation/widgets/dialog/custom_dialog_action.dart'; import 'package:tallee/presentation/widgets/dialog/custom_dialog_action.dart';
import 'package:tallee/presentation/widgets/tiles/settings_list_tile.dart'; import 'package:tallee/presentation/widgets/tiles/settings_list_tile.dart';
@@ -197,19 +199,23 @@ class _SettingsViewState extends State<SettingsView> {
padding: const EdgeInsets.only(bottom: 12), padding: const EdgeInsets.only(bottom: 12),
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
spacing: 40, spacing: 10,
children: [ children: [
GestureDetector( HapticIconButton(
child: const Icon(Icons.language), color: CustomTheme.textColor,
onTap: () => { icon: const Icon(Icons.language),
onPressed: () async => {
await HapticFeedback.lightImpact(),
launchUrl( launchUrl(
Uri.parse('https://liquid-dev.de'), Uri.parse('https://liquid-dev.de'),
), ),
}, },
), ),
GestureDetector( HapticIconButton(
child: const FaIcon(FontAwesomeIcons.github), color: CustomTheme.textColor,
onTap: () => { icon: const FaIcon(FontAwesomeIcons.github),
onPressed: () async => {
await HapticFeedback.lightImpact(),
launchUrl( launchUrl(
Uri.parse( Uri.parse(
'https://github.com/liquiddevelopmentde', 'https://github.com/liquiddevelopmentde',
@@ -217,15 +223,19 @@ class _SettingsViewState extends State<SettingsView> {
), ),
}, },
), ),
GestureDetector( HapticIconButton(
child: Icon( color: CustomTheme.textColor,
icon: Icon(
Platform.isIOS Platform.isIOS
? CupertinoIcons.mail_solid ? CupertinoIcons.mail_solid
: Icons.email, : Icons.email,
), ),
onTap: () => launchUrl( onPressed: () async => {
Uri.parse('mailto:hi@liquid-dev.de'), await HapticFeedback.lightImpact(),
), launchUrl(
Uri.parse('mailto:hi@liquid-dev.de'),
),
},
), ),
], ],
), ),
@@ -266,21 +276,42 @@ class _SettingsViewState extends State<SettingsView> {
void showImportSnackBar({ void showImportSnackBar({
required BuildContext context, required BuildContext context,
required ImportResult result, required ImportResult result,
}) { }) async {
final loc = AppLocalizations.of(context); final loc = AppLocalizations.of(context);
switch (result) { switch (result) {
case ImportResult.success: case ImportResult.success:
showSnackbar(context: context, message: loc.data_successfully_imported); await HapticFeedback.successNotification();
if (context.mounted) {
showSnackbar(
context: context,
message: loc.data_successfully_imported,
);
}
case ImportResult.invalidSchema: case ImportResult.invalidSchema:
showSnackbar(context: context, message: loc.invalid_schema); await HapticFeedback.errorNotification();
if (context.mounted) {
showSnackbar(context: context, message: loc.invalid_schema);
}
case ImportResult.fileReadError: 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: 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: 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: case ImportResult.unknownException:
showSnackbar(context: context, message: loc.unknown_exception); await HapticFeedback.errorNotification();
if (context.mounted) {
showSnackbar(context: context, message: loc.unknown_exception);
}
} }
} }
@@ -291,15 +322,27 @@ class _SettingsViewState extends State<SettingsView> {
void showExportSnackBar({ void showExportSnackBar({
required BuildContext context, required BuildContext context,
required ExportResult result, required ExportResult result,
}) { }) async {
final loc = AppLocalizations.of(context); final loc = AppLocalizations.of(context);
switch (result) { switch (result) {
case ExportResult.success: case ExportResult.success:
showSnackbar(context: context, message: loc.data_successfully_exported); await HapticFeedback.successNotification();
if (context.mounted) {
showSnackbar(
context: context,
message: loc.data_successfully_exported,
);
}
case ExportResult.canceled: case ExportResult.canceled:
showSnackbar(context: context, message: loc.export_canceled); await HapticFeedback.errorNotification();
if (context.mounted) {
showSnackbar(context: context, message: loc.export_canceled);
}
case ExportResult.unknownException: case ExportResult.unknownException:
showSnackbar(context: context, message: loc.unknown_exception); await HapticFeedback.errorNotification();
if (context.mounted) {
showSnackbar(context: context, message: loc.unknown_exception);
}
} }
} }

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,51 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
class HapticIconButton extends StatelessWidget {
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();
},
);
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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