diff --git a/.gitea/workflows/pull_request.yaml b/.gitea/workflows/pull_request.yaml index 26f4404..825305b 100644 --- a/.gitea/workflows/pull_request.yaml +++ b/.gitea/workflows/pull_request.yaml @@ -20,11 +20,11 @@ jobs: uses: subosito/flutter-action@v2 with: channel: stable - flutter-version: 3.38.6 + flutter-version: 3.41.0 - name: Get dependencies run: | - git config --global --add safe.directory /opt/hostedtoolcache/flutter/stable-3.38.6-x64 + git config --global --add safe.directory /opt/hostedtoolcache/flutter/stable-3.41.0-x64 flutter pub get - name: Analyze Formatting @@ -46,11 +46,11 @@ jobs: uses: subosito/flutter-action@v2 with: channel: stable - flutter-version: 3.38.6 + flutter-version: 3.41.0 - name: Get dependencies run: | - git config --global --add safe.directory /opt/hostedtoolcache/flutter/stable-3.38.6-x64 + git config --global --add safe.directory /opt/hostedtoolcache/flutter/stable-3.41.0-x64 flutter pub get - name: Run tests diff --git a/.gitea/workflows/push.yaml b/.gitea/workflows/push.yaml index e24f7ad..cfe987a 100644 --- a/.gitea/workflows/push.yaml +++ b/.gitea/workflows/push.yaml @@ -32,11 +32,11 @@ jobs: uses: subosito/flutter-action@v2 with: channel: stable - flutter-version: 3.38.6 + flutter-version: 3.41.0 - name: Get dependencies run: | - git config --global --add safe.directory /opt/hostedtoolcache/flutter/stable-3.38.6-x64 + git config --global --add safe.directory /opt/hostedtoolcache/flutter/stable-3.41.0-x64 flutter pub get - name: Build APK @@ -58,11 +58,11 @@ jobs: uses: subosito/flutter-action@v2 with: channel: stable - flutter-version: 3.38.6 + flutter-version: 3.41.0 - name: Get dependencies run: | - git config --global --add safe.directory /opt/hostedtoolcache/flutter/stable-3.38.6-x64 + git config --global --add safe.directory /opt/hostedtoolcache/flutter/stable-3.41.0-x64 flutter pub get - name: Run tests @@ -118,11 +118,11 @@ jobs: uses: subosito/flutter-action@v2 with: channel: stable - flutter-version: 3.38.6 + flutter-version: 3.41.0 - name: Get dependencies run: | - git config --global --add safe.directory /opt/hostedtoolcache/flutter/stable-3.38.6-x64 + git config --global --add safe.directory /opt/hostedtoolcache/flutter/stable-3.41.0-x64 flutter pub get - name: Generate oss_licenses.dart @@ -161,11 +161,11 @@ jobs: uses: subosito/flutter-action@v2 with: channel: stable - flutter-version: 3.38.6 + flutter-version: 3.41.0 - name: Get dependencies run: | - git config --global --add safe.directory /opt/hostedtoolcache/flutter/stable-3.38.6-x64 + git config --global --add safe.directory /opt/hostedtoolcache/flutter/stable-3.41.0-x64 flutter pub get - name: Check code format 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/core/enums.dart b/lib/core/enums.dart index 605d3aa..99141e4 100644 --- a/lib/core/enums.dart +++ b/lib/core/enums.dart @@ -34,21 +34,13 @@ enum ExportResult { success, canceled, unknownException } /// - [Ruleset.multipleWinners]: Multiple players can be winners. /// - [Ruleset.placement]: The player with the highest placement wins. enum Ruleset { + singleWinner, + multipleWinners, highestScore, lowestScore, - singleWinner, - singleLoser, - multipleWinners, placement, + singleLoser, } -/// Different colors available for games -/// - [GameColor.red]: Red color -/// - [GameColor.blue]: Blue color -/// - [GameColor.green]: Green color -/// - [GameColor.yellow]: Yellow color -/// - [GameColor.purple]: Purple color -/// - [GameColor.orange]: Orange color -/// - [GameColor.pink]: Pink color -/// - [GameColor.teal]: Teal color -enum GameColor { red, blue, green, yellow, purple, orange, pink, teal } +/// Different colors for highlighting games +enum GameColor { red, orange, yellow, green, teal, blue, purple, pink } diff --git a/lib/data/dao/score_entry_dao.dart b/lib/data/dao/score_entry_dao.dart index cf6a449..830135d 100644 --- a/lib/data/dao/score_entry_dao.dart +++ b/lib/data/dao/score_entry_dao.dart @@ -228,7 +228,7 @@ class ScoreEntryDao extends DatabaseAccessor required String playerId, }) async { // Clear previous winner if exists - deleteAllScoresForMatch(matchId: matchId); + await deleteAllScoresForMatch(matchId: matchId); // Set the winner's score to 1 final rowsAffected = await into(scoreEntryTable).insert( @@ -245,7 +245,7 @@ class ScoreEntryDao extends DatabaseAccessor return rowsAffected > 0; } - // Retrieves the winner of a match by looking for a score entry where score + /// Retrieves the winner of a match by looking for a score entry where score /// is 1. Returns `null` if no player found, else the first with the score. Future getWinner({required String matchId}) async { final query = @@ -276,13 +276,42 @@ class ScoreEntryDao extends DatabaseAccessor /// Returns `true` if the winner was removed, `false` if there are multiple /// scores or if the winner cannot be removed. Future removeWinner({required String matchId}) async { - final scores = await getAllMatchScores(matchId: matchId); + return await deleteAllScoresForMatch(matchId: matchId); + } - if (scores.length > 1) { - return false; - } else { - return await deleteAllScoresForMatch(matchId: matchId); - } + /* multiple winners handling */ + + /// Sets the winners for a match. + /// + /// Returns `true` if more than 0 rows were affected + Future setWinners({ + required List winners, + required String matchId, + }) async { + // Clear previous winners if exists + await deleteAllScoresForMatch(matchId: matchId); + + if (winners.isEmpty) return false; + + await batch((batch) { + batch.insertAll( + scoreEntryTable, + winners + .map( + (player) => ScoreEntryTableCompanion.insert( + playerId: player.id, + matchId: matchId, + roundNumber: 0, + score: 1, + change: 0, + ), + ) + .toList(), + mode: InsertMode.insertOrReplace, + ); + }); + + return true; } /* Loser handling */ @@ -354,6 +383,8 @@ class ScoreEntryDao extends DatabaseAccessor } } + /* placement handling */ + /// Sets the placement for each player in a match. /// The highest score is assigned to the first player, the second highest to the second player, and so on. Future setPlacements({ diff --git a/lib/data/models/match.dart b/lib/data/models/match.dart index 4b14ff9..4db294f 100644 --- a/lib/data/models/match.dart +++ b/lib/data/models/match.dart @@ -164,7 +164,7 @@ class Match { return _getPlayersWithLowestScore().take(1).toList(); case Ruleset.multipleWinners: - return []; + return _getPlayersWithHighestScore().toList(); case Ruleset.placement: return _getPlayersWithHighestScore().take(1).toList(); diff --git a/lib/l10n/arb/app_de.arb b/lib/l10n/arb/app_de.arb index 7bc117f..b7c55fc 100644 --- a/lib/l10n/arb/app_de.arb +++ b/lib/l10n/arb/app_de.arb @@ -120,6 +120,7 @@ "search_for_groups": "Nach Gruppen suchen", "search_for_players": "Nach Spieler:innen suchen", "select_winner": "Gewinner:in wählen", + "select_winners": "Gewinner:innen wählen", "select_loser": "Verlierer:in wählen", "selected_players": "Ausgewählte Spieler:innen", "settings": "Einstellungen", @@ -141,6 +142,7 @@ "undo": "Rückgängig", "unknown_exception": "Unbekannter Fehler (siehe Konsole)", "winner": "Gewinner:in", + "winners": "Gewinner:innen", "winrate": "Siegquote", "wins": "Siege", "yesterday_at": "Gestern um" diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index 48d4eec..be5efa6 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -120,6 +120,7 @@ "search_for_groups": "Search for groups", "search_for_players": "Search for players", "select_winner": "Select Winner", + "select_winners": "Select Winners", "select_loser": "Select Loser", "selected_players": "Selected players", "settings": "Settings", @@ -150,6 +151,7 @@ "undo": "Undo", "unknown_exception": "Unknown Exception (see console)", "winner": "Winner", + "winners": "Winners", "winrate": "Winrate", "wins": "Wins", "yesterday_at": "Yesterday at" diff --git a/lib/l10n/generated/app_localizations.dart b/lib/l10n/generated/app_localizations.dart index f60a19d..3f7883d 100644 --- a/lib/l10n/generated/app_localizations.dart +++ b/lib/l10n/generated/app_localizations.dart @@ -770,6 +770,12 @@ abstract class AppLocalizations { /// **'Select Winner'** String get select_winner; + /// No description provided for @select_winners. + /// + /// In en, this message translates to: + /// **'Select Winners'** + String get select_winners; + /// No description provided for @select_loser. /// /// In en, this message translates to: @@ -896,6 +902,12 @@ abstract class AppLocalizations { /// **'Winner'** String get winner; + /// No description provided for @winners. + /// + /// In en, this message translates to: + /// **'Winners'** + String get winners; + /// No description provided for @winrate. /// /// In en, this message translates to: diff --git a/lib/l10n/generated/app_localizations_de.dart b/lib/l10n/generated/app_localizations_de.dart index 1480887..dfbe9f1 100644 --- a/lib/l10n/generated/app_localizations_de.dart +++ b/lib/l10n/generated/app_localizations_de.dart @@ -366,6 +366,9 @@ class AppLocalizationsDe extends AppLocalizations { @override String get select_winner => 'Gewinner:in wählen'; + @override + String get select_winners => 'Gewinner:innen wählen'; + @override String get select_loser => 'Verlierer:in wählen'; @@ -434,6 +437,9 @@ class AppLocalizationsDe extends AppLocalizations { @override String get winner => 'Gewinner:in'; + @override + String get winners => 'Gewinner:innen'; + @override String get winrate => 'Siegquote'; diff --git a/lib/l10n/generated/app_localizations_en.dart b/lib/l10n/generated/app_localizations_en.dart index 738bb19..93487b3 100644 --- a/lib/l10n/generated/app_localizations_en.dart +++ b/lib/l10n/generated/app_localizations_en.dart @@ -366,6 +366,9 @@ class AppLocalizationsEn extends AppLocalizations { @override String get select_winner => 'Select Winner'; + @override + String get select_winners => 'Select Winners'; + @override String get select_loser => 'Select Loser'; @@ -433,6 +436,9 @@ class AppLocalizationsEn extends AppLocalizations { @override String get winner => 'Winner'; + @override + String get winners => 'Winners'; + @override String get winrate => 'Winrate'; 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 e0f9d85..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'; @@ -47,9 +49,9 @@ class _CreateGameViewState extends State { late final AppDatabase db; late List<(Ruleset, String)> _rulesets; - Ruleset? selectedRuleset = Ruleset.singleWinner; - late List<(GameColor, String)> _colors; + + Ruleset? selectedRuleset = Ruleset.singleWinner; GameColor? selectedColor = GameColor.orange; /// Controller for the game name input field. @@ -77,38 +79,20 @@ class _CreateGameViewState extends State { @override void didChangeDependencies() { super.didChangeDependencies(); - _rulesets = [ - ( - Ruleset.singleWinner, - translateRulesetToString(Ruleset.singleWinner, context), + _rulesets = List.generate( + Ruleset.values.length, + (index) => ( + Ruleset.values[index], + translateRulesetToString(Ruleset.values[index], context), ), - ( - Ruleset.singleLoser, - translateRulesetToString(Ruleset.singleLoser, context), + ); + _colors = List.generate( + GameColor.values.length, + (index) => ( + GameColor.values[index], + translateGameColorToString(GameColor.values[index], context), ), - ( - Ruleset.highestScore, - translateRulesetToString(Ruleset.highestScore, context), - ), - ( - Ruleset.lowestScore, - translateRulesetToString(Ruleset.lowestScore, context), - ), - ( - Ruleset.multipleWinners, - translateRulesetToString(Ruleset.multipleWinners, context), - ), - ]; - _colors = [ - (GameColor.green, translateGameColorToString(GameColor.green, context)), - (GameColor.teal, translateGameColorToString(GameColor.teal, context)), - (GameColor.blue, translateGameColorToString(GameColor.blue, context)), - (GameColor.purple, translateGameColorToString(GameColor.purple, context)), - (GameColor.pink, translateGameColorToString(GameColor.pink, context)), - (GameColor.red, translateGameColorToString(GameColor.red, context)), - (GameColor.orange, translateGameColorToString(GameColor.orange, context)), - (GameColor.yellow, translateGameColorToString(GameColor.yellow, context)), - ]; + ); if (widget.gameToEdit != null) { _gameNameController.text = widget.gameToEdit!.name; @@ -138,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; @@ -214,10 +198,13 @@ class _CreateGameViewState extends State { // Choose ruleset tile if (!isEditMode()) - ChooseTile(title: loc.ruleset, trailing: getColorDropdown(loc)), + ChooseTile( + title: loc.ruleset, + trailing: getRulesetDropdown(loc), + ), // Choose color tile - ChooseTile(title: loc.color, trailing: getRulesetDropdown(loc)), + ChooseTile(title: loc.color, trailing: getColorDropdown(loc)), // Description input field Container( @@ -344,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, @@ -353,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; }); @@ -427,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, @@ -436,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 785baaf..3f4fe36 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 @@ -273,11 +273,11 @@ class _CreateMatchViewState extends State { /// Determines whether the "Create Match" button should be enabled. /// /// Returns `true` if: - /// - A ruleset is selected AND + /// - A game is selected AND /// - Either a group is selected OR at least 2 players are selected. bool isSubmitButtonEnabled() { - return (selectedGroup != null || - (selectedPlayers.length > 1) && selectedGame != null); + return ((selectedGroup != null || selectedPlayers.length > 1) && + selectedGame != null); } /// Handles navigation when the create or save button is pressed. 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 e00ebc6..e38866b 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 @@ -13,6 +13,7 @@ import 'package:tallee/data/models/team.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/cards/team_card.dart'; import 'package:tallee/presentation/widgets/colored_icon_container.dart'; @@ -69,7 +70,7 @@ class _MatchDetailViewState extends State { appBar: AppBar( title: Text(loc.match_profile), actions: [ - IconButton( + HapticIconButton( icon: const Icon(Icons.delete), onPressed: () async { showDialog( @@ -297,6 +298,7 @@ class _MatchDetailViewState extends State { Widget getResultWidget(AppLocalizations loc) { if (isSingleRowResult()) { return Row( + crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.spaceBetween, children: getSingleResultRow(loc), ); @@ -312,9 +314,12 @@ class _MatchDetailViewState extends State { if (localMatch.mvp.isNotEmpty || localMatch.mvt.isNotEmpty) { // Single Winner / Loser - final mvpName = localMatch.isTeamMatch - ? localMatch.mvt.first.name - : localMatch.mvp.first.name; + final mvps = localMatch.isTeamMatch + ? localMatch.mvt + : localMatch.mvp; + final mvpName = ruleset == Ruleset.multipleWinners + ? mvps.map((party) => party.name).join(', ') + : mvps.first.name; return [ Text( @@ -440,7 +445,8 @@ class _MatchDetailViewState extends State { // Returns if the result can be displayed in a single row bool isSingleRowResult() { return localMatch.game.ruleset == Ruleset.singleWinner || - localMatch.game.ruleset == Ruleset.singleLoser; + localMatch.game.ruleset == Ruleset.singleLoser || + localMatch.game.ruleset == Ruleset.multipleWinners; } String getPlacementText(BuildContext context, int rank) { 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 fac95bf..0fe8c1b 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 @@ -10,6 +10,8 @@ import 'package:tallee/data/models/score_entry.dart'; import 'package:tallee/data/models/team.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'; import 'package:tallee/presentation/widgets/tiles/match_result_view/score_list_tile.dart'; @@ -49,9 +51,11 @@ class _MatchResultViewState extends State { late bool isTeamMatch; - /// Currently selected winner player + /// Currently selected player(s)/team(s) (winner / looser) Player? _selectedPlayer; Team? _selectedTeam; + final Set _selectedPlayers = {}; + final Set _selectedTeams = {}; @override void initState() { @@ -84,11 +88,11 @@ class _MatchResultViewState extends State { return Scaffold( backgroundColor: CustomTheme.backgroundColor, appBar: AppBar( - leading: IconButton( + leading: HapticIconButton( icon: const Icon(Icons.close), onPressed: () { widget.onWinnerChanged?.call(); - Navigator.of(context).pop(_selectedPlayer); + Navigator.pop(context); }, ), title: Text(widget.match.name), @@ -142,17 +146,46 @@ class _MatchResultViewState extends State { const SizedBox(height: 10), // Show player selection - if (rulesetSupportsWinnerSelection()) - Expanded( - child: buildWinnerSelectionWidget(isTeamMatch), - ), + if (rulesetSupportsPlayerSelection()) + if (ruleset == Ruleset.multipleWinners) + Expanded( + child: ListView.builder( + physics: const NeverScrollableScrollPhysics(), + itemCount: allPlayers.length, + itemBuilder: (context, index) { + return CustomCheckboxListTile( + text: allPlayers[index].name, + value: _selectedPlayers.contains( + allPlayers[index], + ), + onChanged: (bool value) { + setState(() { + if (value) { + _selectedPlayers.add( + allPlayers[index], + ); + } else { + _selectedPlayers.remove( + allPlayers[index], + ); + } + }); + }, + ); + }, + ), + ) + else + Expanded( + child: buildWinnerSelectionWidget(isTeamMatch), + ), // Show score entry if (rulesetSupportsScoreEntry()) Expanded(child: buildScoreEntryWidget(isTeamMatch)), // Show draggable placement list - if (rulesetSupportsPlacement()) + if (rulesetSupportsDragBehaviour()) Expanded(child: buildPlacementWidget(isTeamMatch)), ], ), @@ -207,16 +240,24 @@ class _MatchResultViewState extends State { // Prefill fields if (widget.match.mvt.isNotEmpty) { - if (rulesetSupportsWinnerSelection()) { - _selectedTeam = allTeams.firstWhere( - (p) => p.id == widget.match.mvt.first.id, - ); + if (rulesetSupportsPlayerSelection()) { + if (ruleset == Ruleset.multipleWinners) { + for (int i = 0; i < allTeams.length; i++) { + if (allTeams[i].score == 1) { + _selectedTeams.add(allTeams[i]); + } + } + } else { + _selectedTeam = allTeams.firstWhere( + (team) => team.id == widget.match.mvt.first.id, + ); + } } else if (rulesetSupportsScoreEntry()) { for (int i = 0; i < allTeams.length; i++) { - final score = allTeams[i].score; + final score = allTeams[i].score ?? 0; controller[i].text = score.toString(); } - } else if (rulesetSupportsPlacement()) { + } else if (rulesetSupportsDragBehaviour()) { allTeams.sort((a, b) { final scoreA = a.score ?? 0; final scoreB = b.score ?? 0; @@ -237,17 +278,25 @@ class _MatchResultViewState extends State { // Prefill fields if (widget.match.mvp.isNotEmpty) { - if (rulesetSupportsWinnerSelection()) { - _selectedPlayer = allPlayers.firstWhere( - (p) => p.id == widget.match.mvp.first.id, - ); + if (rulesetSupportsPlayerSelection()) { + if (ruleset == Ruleset.multipleWinners) { + for (int i = 0; i < allPlayers.length; i++) { + if (widget.match.scores[allPlayers[i].id]?.score == 1) { + _selectedPlayers.add(allPlayers[i]); + } + } + } else { + _selectedPlayer = allPlayers.firstWhere( + (p) => p.id == widget.match.mvp.first.id, + ); + } } else if (rulesetSupportsScoreEntry()) { for (int i = 0; i < allPlayers.length; i++) { final scoreList = widget.match.scores[allPlayers[i].id]; final score = scoreList?.score ?? 0; controller[i].text = score.toString(); } - } else if (rulesetSupportsPlacement()) { + } else if (rulesetSupportsDragBehaviour()) { allPlayers.sort((a, b) { final scoreA = widget.match.scores[a.id]?.score ?? 0; final scoreB = widget.match.scores[b.id]?.score ?? 0; @@ -278,12 +327,14 @@ class _MatchResultViewState extends State { await _handleScores(); } else if (ruleset == Ruleset.placement) { await _handlePlacement(); + } else if (ruleset == Ruleset.multipleWinners) { + await _handleWinners(); } widget.onWinnerChanged?.call(); } - /// Handles saving or removing the winner in the database. + /// Handles saving or removing the (single) winner in the database. Future _handleWinner() async { if (isTeamMatch) { if (_selectedTeam == null) { @@ -309,6 +360,18 @@ class _MatchResultViewState extends State { } } + /// Handles saving the (multiple) winners to the database. + Future _handleWinners() async { + if (_selectedPlayers.isEmpty) { + return await db.scoreEntryDao.removeWinner(matchId: widget.match.id); + } else { + return await db.scoreEntryDao.setWinners( + matchId: widget.match.id, + winners: allPlayers.where((p) => _selectedPlayers.contains(p)).toList(), + ); + } + } + /// Handles saving or removing the loser in the database. Future _handleLoser() async { if (isTeamMatch) { @@ -389,20 +452,24 @@ class _MatchResultViewState extends State { return loc.select_loser; case Ruleset.placement: return loc.drag_to_set_placement; + case Ruleset.multipleWinners: + return loc.select_winners; default: return loc.enter_points; } } - bool rulesetSupportsWinnerSelection() { - return ruleset == Ruleset.singleWinner || ruleset == Ruleset.singleLoser; + bool rulesetSupportsPlayerSelection() { + return ruleset == Ruleset.singleWinner || + ruleset == Ruleset.singleLoser || + ruleset == Ruleset.multipleWinners; } bool rulesetSupportsScoreEntry() { return ruleset == Ruleset.lowestScore || ruleset == Ruleset.highestScore; } - bool rulesetSupportsPlacement() { + bool rulesetSupportsDragBehaviour() { return ruleset == Ruleset.placement; } 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 0ea0662..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 @@ -73,7 +73,6 @@ const allDependencies = [ _io, _jni, _jni_flutter, - _js, _json_annotation, _json_schema, _leak_tracker, @@ -244,13 +243,13 @@ class PackageRef { Package resolve() => allDependencies.firstWhere((d) => d.name == name); } -/// _fe_analyzer_shared 91.0.0 +/// _fe_analyzer_shared 92.0.0 const __fe_analyzer_shared = Package( name: '_fe_analyzer_shared', description: 'Logic that is shared between the front_end and analyzer packages.', repository: 'https://github.com/dart-lang/sdk/tree/main/pkg/_fe_analyzer_shared', authors: [], - version: '91.0.0', + version: '92.0.0', spdxIdentifiers: ['BSD-3-Clause'], isMarkdown: false, isSdk: false, @@ -285,13 +284,13 @@ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''', ); -/// analyzer 8.4.1 +/// analyzer 9.0.0 const _analyzer = Package( name: 'analyzer', description: 'This package provides a library that performs static analysis of Dart code.', repository: 'https://github.com/dart-lang/sdk/tree/main/pkg/analyzer', authors: [], - version: '8.4.1', + version: '9.0.0', spdxIdentifiers: ['BSD-3-Clause'], isMarkdown: false, isSdk: false, @@ -699,13 +698,13 @@ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''', ); -/// characters 1.4.0 +/// characters 1.4.1 const _characters = Package( name: 'characters', description: 'String replacement with operations that are Unicode/grapheme cluster aware.', repository: 'https://github.com/dart-lang/core/tree/main/pkgs/characters', authors: [], - version: '1.4.0', + version: '1.4.1', spdxIdentifiers: ['BSD-3-Clause'], isMarkdown: false, isSdk: false, @@ -2503,13 +2502,13 @@ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''', ); -/// flutter 3.38.6 +/// flutter 3.41.0 const _flutter = Package( name: 'flutter', description: 'A framework for writing Flutter applications', homepage: 'https://flutter.dev', authors: [], - version: '3.38.6', + version: '3.41.0', spdxIdentifiers: ['BSD-3-Clause'], isMarkdown: false, isSdk: true, @@ -3292,47 +3291,6 @@ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''', ); -/// js 0.7.2 -const _js = Package( - name: 'js', - description: 'Annotations to create static Dart interfaces for JavaScript APIs.', - repository: 'https://github.com/dart-lang/sdk/tree/main/pkg/js', - authors: [], - version: '0.7.2', - spdxIdentifiers: ['BSD-3-Clause'], - isMarkdown: false, - isSdk: false, - dependencies: [], - devDependencies: [PackageRef('lints')], - license: '''Copyright 2012, the Dart project authors. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are -met: - - * Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above - copyright notice, this list of conditions and the following - disclaimer in the documentation and/or other materials provided - with the distribution. - * Neither the name of Google LLC nor the names of its - contributors may be used to endorse or promote products derived - from this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''', - ); - /// json_annotation 4.11.0 const _json_annotation = Package( name: 'json_annotation', @@ -3671,18 +3629,18 @@ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''', ); -/// matcher 0.12.17 +/// matcher 0.12.18 const _matcher = Package( name: 'matcher', description: 'Support for specifying test expectations via an extensible Matcher class. Also includes a number of built-in Matcher implementations for common cases.', repository: 'https://github.com/dart-lang/test/tree/master/pkgs/matcher', authors: [], - version: '0.12.17', + version: '0.12.18', spdxIdentifiers: ['BSD-3-Clause'], isMarkdown: false, isSdk: false, dependencies: [PackageRef('async'), PackageRef('meta'), PackageRef('stack_trace'), PackageRef('term_glyph'), PackageRef('test_api')], - devDependencies: [PackageRef('fake_async'), PackageRef('lints'), PackageRef('test')], + devDependencies: [PackageRef('fake_async'), PackageRef('test')], license: '''Copyright 2014, the Dart project authors. Redistribution and use in source and binary forms, with or without @@ -3712,18 +3670,18 @@ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''', ); -/// material_color_utilities 0.11.1 +/// material_color_utilities 0.13.0 const _material_color_utilities = Package( name: 'material_color_utilities', description: 'Algorithms and utilities that power the Material Design 3 color system, including choosing theme colors from images and creating tones of colors; all in a new color space.', repository: 'https://github.com/material-foundation/material-color-utilities/tree/main/dart', authors: [], - version: '0.11.1', + version: '0.13.0', spdxIdentifiers: ['Apache-2.0'], isMarkdown: false, isSdk: false, dependencies: [PackageRef('collection')], - devDependencies: [PackageRef('matcher'), PackageRef('lints'), PackageRef('test')], + devDependencies: [PackageRef('matcher'), PackageRef('test')], license: '''Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ @@ -6288,6 +6246,7 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- +icu json @@ -6913,6 +6872,7 @@ skia limitations under the License. -------------------------------------------------------------------------------- angle +benchmark boringssl cpu_features flatbuffers @@ -10342,6 +10302,7 @@ prospectively choose to deem waived or otherwise exclude such Section(s) of the License, but only in their entirety and only with respect to the Combined Software. -------------------------------------------------------------------------------- +icu include json @@ -13912,34 +13873,6 @@ License & terms of use: http://www.unicode.org/copyright.html All Rights Reserved. --------------------------------------------------------------------------------- -icu - -Copyright (C) 2016 and later: Unicode, Inc. and others. -License & terms of use: http://www.unicode.org/copyright.html - -Copyright (C) 2002-2010, International Business Machines -Corporation and others. All Rights Reserved. - - --------------------------------------------------------------------------------- -icu - -Copyright (C) 2016 and later: Unicode, Inc. and others. -License & terms of use: http://www.unicode.org/copyright.html - -Copyright (c) 2001-2003 International Business Machines -Corporation and others. All Rights Reserved. - --------------------------------------------------------------------------------- -icu - -Copyright (C) 2016 and later: Unicode, Inc. and others. -License & terms of use: http://www.unicode.org/copyright.html - -Copyright (c) 2001-2010 International Business Machines -Corporation and others. All Rights Reserved. - -------------------------------------------------------------------------------- icu @@ -13954,14 +13887,6 @@ icu Copyright (C) 2016 and later: Unicode, Inc. and others. License & terms of use: http://www.unicode.org/copyright.html -Copyright (c) 2002-2010, International Business Machines Corporation and others. All Rights Reserved. - --------------------------------------------------------------------------------- -icu - -Copyright (C) 2016 and later: Unicode, Inc. and others. -License & terms of use: http://www.unicode.org/copyright.html - Copyright (c) 2002-2015, International Business Machines Corporation and others. All Rights Reserved. @@ -13976,22 +13901,6 @@ Copyright (c) 2002-2016 International Business Machines Corporation and others. All Rights Reserved. --------------------------------------------------------------------------------- -icu - -Copyright (C) 2016 and later: Unicode, Inc. and others. -License & terms of use: http://www.unicode.org/copyright.html - -Copyright (c) 2003-2005, International Business Machines Corporation and others. All Rights Reserved. - --------------------------------------------------------------------------------- -icu - -Copyright (C) 2016 and later: Unicode, Inc. and others. -License & terms of use: http://www.unicode.org/copyright.html - -Copyright (c) 2003-2010, International Business Machines Corporation and others. All Rights Reserved. - -------------------------------------------------------------------------------- icu @@ -14166,17 +14075,6 @@ License & terms of use: http://www.unicode.org/copyright.html -------------------------------------------------------------------------------- icu -Copyright (C) 2016 and later: Unicode, Inc. and others. -License & terms of use: http://www.unicode.org/copyright.html -*************************************************************************** -* -* Copyright (C) 2009 International Business Machines -* Corporation and others. All Rights Reserved. -* -*************************************************************************** --------------------------------------------------------------------------------- -icu - Copyright (C) 2016 and later: Unicode, Inc. and others. License & terms of use: http://www.unicode.org/copyright.html ***************************************************************************** @@ -14187,19 +14085,6 @@ License & terms of use: http://www.unicode.org/copyright.html ***************************************************************************** --------------------------------------------------------------------------------- -icu - -Copyright (C) 2016 and later: Unicode, Inc. and others. -License & terms of use: http://www.unicode.org/copyright.html -******************************************************************************* -* -* Copyright (C) 1995-2001, International Business Machines -* Corporation and others. All Rights Reserved. -* -******************************************************************************* - - -------------------------------------------------------------------------------- icu @@ -14278,6 +14163,15 @@ License & terms of use: http://www.unicode.org/copyright.html ******************************************************************************* +-------------------------------------------------------------------------------- +icu + +Copyright (C) 2016 and later: Unicode, Inc. and others. +License & terms of use: http://www.unicode.org/copyright.html +--------------------------------------------------------- +Copyright (C) 2013, International Business Machines +Corporation and others. All Rights Reserved. + -------------------------------------------------------------------------------- icu @@ -15561,6 +15455,22 @@ angle Copyright (c) 2008-2021 The Khronos Group Inc. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +-------------------------------------------------------------------------------- +angle + +Copyright (c) 2008-2023 The Khronos Group Inc. + Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at @@ -18045,6 +17955,28 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. -------------------------------------------------------------------------------- +volk + +Copyright (c) 2018-2024 Arseny Kapoulkine + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +-------------------------------------------------------------------------------- vulkan-validation-layers Copyright (c) 2018-2024 The Khronos Group Inc. @@ -18569,7 +18501,6 @@ Copyright (c) 2020 The ANGLE Project Authors. All rights reserved. Use of this source code is governed by a BSD-style license that can be found in the LICENSE file. -------------------------------------------------------------------------------- -angle spirv-tools Copyright (c) 2020 The Khronos Group Inc. @@ -19380,6 +19311,22 @@ spirv-tools Copyright (c) 2023 LunarG Inc. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +-------------------------------------------------------------------------------- +angle + +Copyright (c) 2023 The Khronos Group Inc. + Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at @@ -19417,6 +19364,7 @@ for details. All rights reserved. Use of this source code is governed by a BSD-style license that can be found in the LICENSE file. -------------------------------------------------------------------------------- dart +perfetto Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file for details. All rights reserved. Use of this source code is governed by a @@ -19950,6 +19898,12 @@ Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file for details. All rights reserved. Use of this source code is governed by a BSD-style license that can be found in the LICENSE file. -------------------------------------------------------------------------------- +perfetto + +Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file +for details. All rights reserved. Use of this source code is governed by a +BSD-style license that can be found in the LICENSE file. +-------------------------------------------------------------------------------- glfw Copyright (c) Camilla Löwy @@ -20809,11 +20763,6 @@ Copyright 2014-2022 The Khronos Group Inc. SPDX-License-Identifier: Apache-2.0 -------------------------------------------------------------------------------- swiftshader - -Copyright 2014-2023 The Khronos Group Inc. - -SPDX-License-Identifier: Apache-2.0 --------------------------------------------------------------------------------- vulkan vulkan-headers @@ -20851,6 +20800,7 @@ tree. An additional intellectual property rights grant can be found in the file PATENTS. All contributing project authors may be found in the AUTHORS file in the root of the source tree. -------------------------------------------------------------------------------- +benchmark flatbuffers Copyright 2015 Google Inc. All rights reserved. @@ -20936,12 +20886,6 @@ See the License for the specific language governing permissions and limitations under the License. -------------------------------------------------------------------------------- swiftshader - -Copyright 2015-2023 The Khronos Group Inc. - -SPDX-License-Identifier: Apache-2.0 --------------------------------------------------------------------------------- -swiftshader vulkan vulkan-headers @@ -20951,6 +20895,7 @@ Copyright 2015-2023 LunarG, Inc. SPDX-License-Identifier: Apache-2.0 -------------------------------------------------------------------------------- +swiftshader vulkan vulkan-headers @@ -20978,18 +20923,6 @@ skia Copyright 2016 Google Inc. -Use of this source code is governed by a BSD-style license that can be -found in the LICENSE file. --------------------------------------------------------------------------------- -skia - -Copyright 2016 Google Inc. - -Use of this source code is governed by a BSD-style license that can be -found in the LICENSE file. - -Copyright 2014 Google Inc. - Use of this source code is governed by a BSD-style license that can be found in the LICENSE file. -------------------------------------------------------------------------------- @@ -21007,6 +20940,39 @@ flatbuffers Copyright 2016 Google Inc. All rights reserved. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +-------------------------------------------------------------------------------- +benchmark + +Copyright 2016 Ismael Jimenez Martinez. All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +-------------------------------------------------------------------------------- +benchmark + +Copyright 2016 Ismael Jimenez Martinez. All rights reserved. +Copyright 2017 Roman Lebedev. All rights reserved. + Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at @@ -21437,6 +21403,7 @@ tree. An additional intellectual property rights grant can be found in the file PATENTS. All contributing project authors may be found in the AUTHORS file in the root of the source tree. -------------------------------------------------------------------------------- +benchmark flatbuffers Copyright 2018 Google Inc. All rights reserved. @@ -22213,7 +22180,6 @@ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- -angle swiftshader Copyright 2020 The SwiftShader Authors. All Rights Reserved. @@ -22263,6 +22229,7 @@ tree. An additional intellectual property rights grant can be found in the file PATENTS. All contributing project authors may be found in the AUTHORS file in the root of the source tree. -------------------------------------------------------------------------------- +benchmark flatbuffers Copyright 2021 Google Inc. All rights reserved. @@ -22695,6 +22662,22 @@ Copyright 2023 The ANGLE Project Authors. All rights reserved. Use of this source code is governed by a BSD-style license that can be found in the LICENSE file. -------------------------------------------------------------------------------- +angle + +Copyright 2023 The Android Open Source Project + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +-------------------------------------------------------------------------------- skia Copyright 2023 The Android Open Source Project @@ -22772,6 +22755,7 @@ Copyright 2023-2025 The Khronos Group Inc. SPDX-License-Identifier: Apache-2.0 -------------------------------------------------------------------------------- +swiftshader vulkan-utility-libraries Copyright 2023-2025 The Khronos Group Inc. @@ -22789,6 +22773,12 @@ skia Copyright 2024 Google Inc. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +-------------------------------------------------------------------------------- +angle + +Copyright 2024 Google Inc. All rights reserved. Use of this source code is governed by a BSD-style license that can be found in the LICENSE file. -------------------------------------------------------------------------------- @@ -22824,6 +22814,12 @@ found in the LICENSE file. -------------------------------------------------------------------------------- skia +Copyright 2024 Google LLC. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +-------------------------------------------------------------------------------- +skia + Copyright 2024 Google LLC. Use of this source code is governed by a BSD-style license that can be found in the LICENSE file. -------------------------------------------------------------------------------- @@ -22834,6 +22830,17 @@ Copyright 2024 Google, LLC Use of this source code is governed by a BSD-style license that can be found in the LICENSE file. -------------------------------------------------------------------------------- +angle + +Copyright 2024 The ANGLE Project Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +-------------------------------------------------------------------------------- +angle + +Copyright 2024 The ANGLE Project Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be found in the LICENSE file. +-------------------------------------------------------------------------------- skia Copyright 2024 The Android Open Source Project @@ -22915,6 +22922,19 @@ skia Copyright 2025 Google LLC. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +-------------------------------------------------------------------------------- +skia + +Copyright 2025 Google, LLC + +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +-------------------------------------------------------------------------------- +angle + +Copyright 2025 The ANGLE Project Authors. All rights reserved. Use of this source code is governed by a BSD-style license that can be found in the LICENSE file. -------------------------------------------------------------------------------- @@ -22979,6 +22999,12 @@ Copyright The ANGLE Project Authors. All rights reserved. Use of this source code is governed by a BSD-style license that can be found in the LICENSE file. -------------------------------------------------------------------------------- +angle + +Copyright {copyright_year} The ANGLE Project Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +-------------------------------------------------------------------------------- harfbuzz Copyright © 1998-2004 David Turner and Werner Lemberg @@ -26723,6 +26749,48 @@ FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON AN "AS IS" BASIS, AND THE COPYRIGHT HOLDER HAS NO OBLIGATION TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. -------------------------------------------------------------------------------- +flutter + +License for the Ahem font embedded below is from: +https://www.w3.org/Style/CSS/Test/Fonts/Ahem/COPYING + +The Ahem font in this directory belongs to the public domain. In +jurisdictions that do not recognize public domain ownership of these +files, the following Creative Commons Zero declaration applies: + + + +which is quoted below: + + The person who has associated a work with this document (the "Work") + affirms that he or she (the "Affirmer") is the/an author or owner of + the Work. The Work may be any work of authorship, including a + database. + + The Affirmer hereby fully, permanently and irrevocably waives and + relinquishes all of her or his copyright and related or neighboring + legal rights in the Work available under any federal or state law, + treaty or contract, including but not limited to moral rights, + publicity and privacy rights, rights protecting against unfair + competition and any rights protecting the extraction, dissemination + and reuse of data, whether such rights are present or future, vested + or contingent (the "Waiver"). The Affirmer makes the Waiver for the + benefit of the public at large and to the detriment of the Affirmer's + heirs or successors. + + The Affirmer understands and intends that the Waiver has the effect + of eliminating and entirely removing from the Affirmer's control all + the copyright and related or neighboring legal rights previously held + by the Affirmer in the Work, to that extent making the Work freely + available to the public for any and all uses and purposes without + restriction of any kind, including commercial use and uses in media + and formats or by methods that have not yet been invented or + conceived. Should the Waiver for any reason be judged legally + ineffective in any jurisdiction, the Affirmer hereby grants a free, + full, permanent, irrevocable, nonexclusive and worldwide license for + all her or his copyright and related or neighboring legal rights in + the Work. +-------------------------------------------------------------------------------- fallback_root_certificates Mozilla Public License Version 2.0 @@ -27105,8 +27173,8 @@ libpng PNG Reference Library License version 2 --------------------------------------- - * Copyright (c) 1995-2024 The PNG Reference Library Authors. - * Copyright (c) 2018-2024 Cosmin Truta. + * Copyright (c) 1995-2025 The PNG Reference Library Authors. + * Copyright (c) 2018-2025 Cosmin Truta. * Copyright (c) 2000-2002, 2004, 2006-2018 Glenn Randers-Pehrson. * Copyright (c) 1996-1997 Andreas Dilger. * Copyright (c) 1995-1996 Guy Eric Schalnat, Group 42, Inc. @@ -27240,8 +27308,8 @@ libpng PNG Reference Library License version 2 --------------------------------------- - * Copyright (c) 1995-2024 The PNG Reference Library Authors. - * Copyright (c) 2018-2024 Cosmin Truta. + * Copyright (c) 1995-2025 The PNG Reference Library Authors. + * Copyright (c) 2018-2025 Cosmin Truta. * Copyright (c) 2000-2002, 2004, 2006-2018 Glenn Randers-Pehrson. * Copyright (c) 1996-1997 Andreas Dilger. * Copyright (c) 1995-1996 Guy Eric Schalnat, Group 42, Inc. @@ -28708,7 +28776,7 @@ UNICODE LICENSE V3 COPYRIGHT AND PERMISSION NOTICE -Copyright © 2016-2023 Unicode, Inc. +Copyright © 2016-2025 Unicode, Inc. NOTICE TO USER: Carefully read the following legal agreement. BY DOWNLOADING, INSTALLING, COPYING OR OTHERWISE USING DATA FILES, AND/OR @@ -28744,6 +28812,8 @@ not be used in advertising or otherwise to promote the sale, use or other dealings in these Data Files or Software without prior written authorization of the copyright holder. +SPDX-License-Identifier: Unicode-3.0 + ---------------------------------------------------------------------- Third-Party Software Licenses @@ -29137,6 +29207,34 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ---------------------------------------------------------------------- +JSON parsing library (nlohmann/json) + +File: vendor/json/upstream/single_include/nlohmann/json.hpp (only for ICU4C) + +MIT License + +Copyright (c) 2013-2022 Niels Lohmann + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +---------------------------------------------------------------------- + File: install-sh (only for ICU4C) @@ -32422,17 +32520,6 @@ Copyright (C) 2003-2016, International Business Machines --------------------------------------------------------------------------------- -icu - -© 2016 and later: Unicode, Inc. and others. -License & terms of use: http://www.unicode.org/copyright.html - - -Copyright (C) 2008-2013, International Business Machines Corporation and -others. All Rights Reserved. - - -------------------------------------------------------------------------------- icu @@ -32472,16 +32559,6 @@ icu License & terms of use: http://www.unicode.org/copyright.html -Copyright (c) 1999-2007, International Business Machines Corporation and -others. All Rights Reserved. - --------------------------------------------------------------------------------- -icu - -© 2016 and later: Unicode, Inc. and others. -License & terms of use: http://www.unicode.org/copyright.html - - Copyright (c) 1999-2010, International Business Machines Corporation and others. All Rights Reserved. @@ -35972,15 +36049,6 @@ others. All Rights Reserved. --------------------------------------------------------------------------------- -icu - -© 2016 and later: Unicode, Inc. and others. -License & terms of use: http://www.unicode.org/copyright.html -Copyright (C) 2008-2014, International Business Machines Corporation and -others. All Rights Reserved. - - -------------------------------------------------------------------------------- icu @@ -36220,14 +36288,6 @@ others. All Rights Reserved. -------------------------------------------------------------------------------- icu -© 2016 and later: Unicode, Inc. and others. -License & terms of use: http://www.unicode.org/copyright.html -Copyright (c) 2008-2014, International Business Machines Corporation and -others. All Rights Reserved. - --------------------------------------------------------------------------------- -icu - © 2016 and later: Unicode, Inc. and others. License & terms of use: http://www.unicode.org/copyright.html Copyright (c) IBM Corporation, 2000-2010. All rights reserved. @@ -36755,17 +36815,17 @@ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''', ); -/// test 1.26.3 +/// test 1.28.0 const _test = Package( name: 'test', description: 'A full featured library for writing and running Dart tests across platforms.', repository: 'https://github.com/dart-lang/test/tree/master/pkgs/test', authors: [], - version: '1.26.3', + version: '1.28.0', spdxIdentifiers: ['BSD-3-Clause'], isMarkdown: false, isSdk: false, - dependencies: [PackageRef('analyzer'), PackageRef('async'), PackageRef('boolean_selector'), PackageRef('collection'), PackageRef('coverage'), PackageRef('http_multi_server'), PackageRef('io'), PackageRef('js'), PackageRef('matcher'), PackageRef('node_preamble'), PackageRef('package_config'), PackageRef('path'), PackageRef('pool'), PackageRef('shelf'), PackageRef('shelf_packages_handler'), PackageRef('shelf_static'), PackageRef('shelf_web_socket'), PackageRef('source_span'), PackageRef('stack_trace'), PackageRef('stream_channel'), PackageRef('test_api'), PackageRef('test_core'), PackageRef('typed_data'), PackageRef('web_socket_channel'), PackageRef('webkit_inspection_protocol'), PackageRef('yaml')], + dependencies: [PackageRef('analyzer'), PackageRef('async'), PackageRef('boolean_selector'), PackageRef('collection'), PackageRef('coverage'), PackageRef('http_multi_server'), PackageRef('io'), PackageRef('matcher'), PackageRef('node_preamble'), PackageRef('package_config'), PackageRef('path'), PackageRef('pool'), PackageRef('shelf'), PackageRef('shelf_packages_handler'), PackageRef('shelf_static'), PackageRef('shelf_web_socket'), PackageRef('source_span'), PackageRef('stack_trace'), PackageRef('stream_channel'), PackageRef('test_api'), PackageRef('test_core'), PackageRef('typed_data'), PackageRef('web_socket_channel'), PackageRef('webkit_inspection_protocol'), PackageRef('yaml')], devDependencies: [PackageRef('fake_async'), PackageRef('glob')], license: '''Copyright 2014, the Dart project authors. @@ -36796,13 +36856,13 @@ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''', ); -/// test_api 0.7.7 +/// test_api 0.7.8 const _test_api = Package( name: 'test_api', description: 'The user facing API for structuring Dart tests and checking expectations.', repository: 'https://github.com/dart-lang/test/tree/master/pkgs/test_api', authors: [], - version: '0.7.7', + version: '0.7.8', spdxIdentifiers: ['BSD-3-Clause'], isMarkdown: false, isSdk: false, @@ -36837,13 +36897,13 @@ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''', ); -/// test_core 0.6.12 +/// test_core 0.6.14 const _test_core = Package( name: 'test_core', description: 'A basic library for writing tests and running them on the VM.', repository: 'https://github.com/dart-lang/test/tree/master/pkgs/test_core', authors: [], - version: '0.6.12', + version: '0.6.14', spdxIdentifiers: ['BSD-3-Clause'], isMarkdown: false, isSdk: false, @@ -37751,12 +37811,12 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.''', ); -/// tallee 0.0.30+264 +/// tallee 0.0.33+267 const _tallee = Package( name: 'tallee', description: 'Tracking App for Card Games', authors: [], - version: '0.0.30+264', + 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 8bb6222..984326b 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. @@ -84,14 +85,21 @@ class _MainMenuButtonState extends State } else { _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(); + }, + ); + }, + ); } } }, @@ -101,6 +109,7 @@ class _MainMenuButtonState extends State } else { _cancelTimers(); if (mounted && !_isLongPressing) { + await HapticFeedback.selectionClick(); widget.onPressed?.call(); } _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 new file mode 100644 index 0000000..bb6c933 --- /dev/null +++ b/lib/presentation/widgets/tiles/match_result_view/custom_checkbox_list_tile.dart @@ -0,0 +1,57 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:tallee/core/custom_theme.dart'; + +class CustomCheckboxListTile extends StatelessWidget { + const CustomCheckboxListTile({ + super.key, + required this.text, + required this.value, + required this.onChanged, + }); + + final String text; + final bool value; + final ValueChanged onChanged; + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: () async { + await HapticFeedback.selectionClick(); + onChanged(!value); + }, + child: Container( + margin: const EdgeInsets.symmetric(horizontal: 5, vertical: 5), + padding: const EdgeInsets.symmetric(horizontal: 2), + decoration: BoxDecoration( + color: CustomTheme.boxColor, + border: Border.all(color: CustomTheme.boxBorderColor), + borderRadius: CustomTheme.standardBorderRadiusAll, + ), + child: Row( + children: [ + Checkbox( + value: value, + onChanged: (bool? v) async { + await HapticFeedback.selectionClick(); + if (v == null) return; + onChanged(v); + }, + ), + Expanded( + child: Text( + text, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + ), + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/presentation/widgets/tiles/match_tile.dart b/lib/presentation/widgets/tiles/match_tile.dart index 5dd7d37..77c3f12 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'; @@ -52,7 +53,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, @@ -332,6 +336,9 @@ class _MatchTileState extends State { return '${loc.winner}: $mvpNames (${getPointLabel(loc, mvpScore)})'; } else if (ruleset == Ruleset.placement) { return '${loc.winner}: ${widget.match.mvp.first.name}'; + } else if (ruleset == Ruleset.multipleWinners) { + final mvpNames = widget.match.mvp.map((player) => player.name).join(', '); + return '${loc.winners}: $mvpNames'; } return '${loc.winner}: n.A.'; } 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 be86af2..95a31f7 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.30+304 +version: 0.0.30+316 environment: sdk: ^3.8.1 diff --git a/test/services/data_transfer_service_test.dart b/test/services/data_transfer_service_test.dart index 70313fa..ded411d 100644 --- a/test/services/data_transfer_service_test.dart +++ b/test/services/data_transfer_service_test.dart @@ -880,14 +880,6 @@ void main() { 'createdAt': testGroup.createdAt.toIso8601String(), }, ], - 'teams': [ - { - 'id': testTeam.id, - 'name': testTeam.name, - 'memberIds': [testPlayer1.id, testPlayer2.id], - 'createdAt': testTeam.createdAt.toIso8601String(), - }, - ], 'matches': [ { 'id': testMatch.id,