Haptisches Feedback hinzufügen #216

Merged
flixcoo merged 14 commits from feature/215-haptisches-feedback-hinzufügen into development 2026-05-14 13:09:01 +00:00
13 changed files with 131 additions and 17 deletions
Showing only changes of commit 1d20127af4 - Show all commits

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(
flixcoo marked this conversation as resolved
Review

Bitte überprüfen: So wie ich getestet haben kann man an diversen Stellen die Buttons weglassen, wenn sie nur navigation übernehmen. Bitte alle Buttons in den App Bars entfernen, wenn das tatsächlich möglich ist.

Bitte überprüfen: So wie ich getestet haben kann man an diversen Stellen die Buttons weglassen, wenn sie nur navigation übernehmen. Bitte alle Buttons in den App Bars entfernen, wenn das tatsächlich möglich ist.
Review

hab einmal per search alles mit appbar angeguckt, die buttons sind nur da explizit drin, wo sie auch gebraucht werden, weil sie z.B. mit einer variable den screen poppen

hab einmal per search alles mit appbar angeguckt, die buttons sind nur da explizit drin, wo sie auch gebraucht werden, weil sie z.B. mit einer variable den screen poppen
Review

Alles klar, dann passt

Alles klar, dann passt
Review

aber grundsätzlich hast du recht, standardmäßig ist der zurück button immer da

aber grundsätzlich hast du recht, standardmäßig ist der zurück button immer da
backButtonIconBuilder: (context) => const HapticBackButton(),
closeButtonIconBuilder: (context) => const HapticCloseButton(),
);
static const SearchBarThemeData searchBarTheme = SearchBarThemeData( 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';
@@ -55,8 +56,9 @@ class _CustomNavigationBarState extends State<CustomNavigationBar>
actions: [ actions: [
IconButton( IconButton(
onPressed: () async { onPressed: () async {
await Navigator.push( final navigator = Navigator.of(context);
context, await HapticFeedback.selectionClick();
await navigator.push(
sneeex marked this conversation as resolved
Review

Warum rufst du hier extra nochmal HapticFeedback.selectionClick() auf?

Warum rufst du hier extra nochmal `HapticFeedback.selectionClick()` auf?
Review

war wohl ausversehen, vmtl noch aus dem testing, habs entfernt

war wohl ausversehen, vmtl noch aus dem testing, habs entfernt
adaptivePageRoute(builder: (_) => const SettingsView()), 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,10 @@ class _CreateGroupViewState extends State<CreateGroupView> {
if (!mounted) return; if (!mounted) return;
if (success) { if (success) {
await HapticFeedback.successNotification();
Navigator.pop(context, updatedGroup); Navigator.pop(context, updatedGroup);
} else { } else {
await HapticFeedback.errorNotification();
showSnackbar( showSnackbar(
message: widget.groupToEdit == null message: widget.groupToEdit == null
? loc.error_creating_group ? loc.error_creating_group

View File

@@ -1,4 +1,5 @@
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:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:tallee/core/adaptive_page_route.dart'; import 'package:tallee/core/adaptive_page_route.dart';
@@ -68,6 +69,7 @@ class _GroupDetailViewState extends State<GroupDetailView> {
IconButton( IconButton(
icon: const Icon(Icons.delete), icon: const Icon(Icons.delete),
onPressed: () async { onPressed: () async {
await HapticFeedback.selectionClick();
showDialog<bool>( showDialog<bool>(
context: context, context: context,
builder: (context) => CustomAlertDialog( builder: (context) => CustomAlertDialog(
@@ -75,11 +77,17 @@ class _GroupDetailViewState extends State<GroupDetailView> {
content: Text(loc.this_cannot_be_undone), content: Text(loc.this_cannot_be_undone),
actions: [ actions: [
CustomDialogAction( CustomDialogAction(
onPressed: () => Navigator.of(context).pop(true), onPressed: () async {
await HapticFeedback.warningNotification();
Navigator.of(context).pop(true);
},
text: loc.delete, text: loc.delete,
), ),
CustomDialogAction( CustomDialogAction(
onPressed: () => Navigator.of(context).pop(false), onPressed: () async {
await HapticFeedback.selectionClick();
Navigator.of(context).pop(false);
},
buttonType: ButtonType.secondary, buttonType: ButtonType.secondary,
text: loc.cancel, text: loc.cancel,
), ),

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/adaptive_page_route.dart'; import 'package:tallee/core/adaptive_page_route.dart';
import 'package:tallee/core/constants.dart'; import 'package:tallee/core/constants.dart';
@@ -102,6 +103,7 @@ class _GroupViewState extends State<GroupView> {
text: loc.create_group, text: loc.create_group,
icon: Icons.group_add, icon: Icons.group_add,
onPressed: () async { onPressed: () async {
await HapticFeedback.selectionClick();
await Navigator.push( await Navigator.push(
context, context,
adaptivePageRoute( adaptivePageRoute(

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';
@@ -201,7 +202,8 @@ class _SettingsViewState extends State<SettingsView> {
children: [ children: [
GestureDetector( GestureDetector(
child: const Icon(Icons.language), child: const Icon(Icons.language),
onTap: () => { onTap: () async => {
await HapticFeedback.lightImpact(),
launchUrl( launchUrl(
Uri.parse('https://liquid-dev.de'), Uri.parse('https://liquid-dev.de'),
), ),
@@ -209,7 +211,8 @@ class _SettingsViewState extends State<SettingsView> {
), ),
GestureDetector( GestureDetector(
child: const FaIcon(FontAwesomeIcons.github), child: const FaIcon(FontAwesomeIcons.github),
onTap: () => { onTap: () async => {
await HapticFeedback.lightImpact(),
launchUrl( launchUrl(
Uri.parse( Uri.parse(
'https://github.com/liquiddevelopmentde', 'https://github.com/liquiddevelopmentde',
@@ -223,9 +226,12 @@ class _SettingsViewState extends State<SettingsView> {
? CupertinoIcons.mail_solid ? CupertinoIcons.mail_solid
: Icons.email, : Icons.email,
), ),
onTap: () => launchUrl( onTap: () async => {
Uri.parse('mailto:hi@liquid-dev.de'), await HapticFeedback.lightImpact(),
), launchUrl(
Uri.parse('mailto:hi@liquid-dev.de'),
),
},
), ),
], ],
), ),
@@ -266,20 +272,38 @@ 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:
if (context.mounted) {
await HapticFeedback.successNotification();
}
showSnackbar(context: context, message: loc.data_successfully_imported); showSnackbar(context: context, message: loc.data_successfully_imported);
case ImportResult.invalidSchema: case ImportResult.invalidSchema:
if (context.mounted) {
await HapticFeedback.errorNotification();
}
showSnackbar(context: context, message: loc.invalid_schema); showSnackbar(context: context, message: loc.invalid_schema);
case ImportResult.fileReadError: case ImportResult.fileReadError:
if (context.mounted) {
await HapticFeedback.errorNotification();
}
showSnackbar(context: context, message: loc.error_reading_file); showSnackbar(context: context, message: loc.error_reading_file);
case ImportResult.canceled: case ImportResult.canceled:
if (context.mounted) {
await HapticFeedback.errorNotification();
}
showSnackbar(context: context, message: loc.import_canceled); showSnackbar(context: context, message: loc.import_canceled);
case ImportResult.formatException: case ImportResult.formatException:
if (context.mounted) {
await HapticFeedback.errorNotification();
}
showSnackbar(context: context, message: loc.format_exception); showSnackbar(context: context, message: loc.format_exception);
case ImportResult.unknownException: case ImportResult.unknownException:
if (context.mounted) {
await HapticFeedback.errorNotification();
}
showSnackbar(context: context, message: loc.unknown_exception); showSnackbar(context: context, message: loc.unknown_exception);
} }
} }
@@ -291,13 +315,16 @@ 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:
await HapticFeedback.successNotification();
showSnackbar(context: context, message: loc.data_successfully_exported); showSnackbar(context: context, message: loc.data_successfully_exported);
case ExportResult.canceled: case ExportResult.canceled:
await HapticFeedback.errorNotification();
showSnackbar(context: context, message: loc.export_canceled); showSnackbar(context: context, message: loc.export_canceled);
await HapticFeedback.errorNotification();
case ExportResult.unknownException: case ExportResult.unknownException:
showSnackbar(context: context, message: loc.unknown_exception); showSnackbar(context: context, message: loc.unknown_exception);
} }

View File

@@ -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();
},
);
}
}

View File

@@ -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();
},
);
}
}

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';
@@ -197,7 +198,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 +296,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/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,10 @@ 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();
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';
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),