feat: Deleting games associated with matches deletes them
This commit is contained in:
@@ -299,6 +299,25 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
|
|||||||
return count ?? 0;
|
return count ?? 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Retrieves the number of matches associated with a specific game.
|
||||||
|
Future<int> getMatchCountByGame({required String gameId}) async {
|
||||||
|
final count =
|
||||||
|
await (selectOnly(matchTable)
|
||||||
|
..where(matchTable.gameId.equals(gameId))
|
||||||
|
..addColumns([matchTable.id.count()]))
|
||||||
|
.map((row) => row.read(matchTable.id.count()))
|
||||||
|
.getSingle();
|
||||||
|
return count ?? 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Deletes all matches associated with a specific game.
|
||||||
|
/// Returns the number of matches deleted.
|
||||||
|
Future<int> deleteMatchesByGame({required String gameId}) async {
|
||||||
|
final query = delete(matchTable)..where((m) => m.gameId.equals(gameId));
|
||||||
|
final rowsAffected = await query.go();
|
||||||
|
return rowsAffected;
|
||||||
|
}
|
||||||
|
|
||||||
/// Retrieves all matches associated with the given [groupId].
|
/// Retrieves all matches associated with the given [groupId].
|
||||||
/// Queries the database directly, filtering by [groupId].
|
/// Queries the database directly, filtering by [groupId].
|
||||||
Future<List<Match>> getGroupMatches({required String groupId}) async {
|
Future<List<Match>> getGroupMatches({required String groupId}) async {
|
||||||
|
|||||||
@@ -34,6 +34,14 @@
|
|||||||
"delete": "Löschen",
|
"delete": "Löschen",
|
||||||
"delete_all_data": "Alle Daten löschen",
|
"delete_all_data": "Alle Daten löschen",
|
||||||
"delete_game": "Spielvorlage löschen",
|
"delete_game": "Spielvorlage löschen",
|
||||||
|
"delete_game_with_matches_warning": "Wenn du diese Spielvorlage löschst, werden {count, plural, =1{1 Spiel} other{{count} Spiele}} mit dieser Spielvorlage ebenfalls gelöscht.",
|
||||||
|
"@delete_game_with_matches_warning": {
|
||||||
|
"placeholders": {
|
||||||
|
"count": {
|
||||||
|
"type": "int"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"delete_group": "Gruppe löschen",
|
"delete_group": "Gruppe löschen",
|
||||||
"delete_match": "Spiel löschen",
|
"delete_match": "Spiel löschen",
|
||||||
"description": "Beschreibung",
|
"description": "Beschreibung",
|
||||||
|
|||||||
@@ -389,6 +389,14 @@
|
|||||||
"delete": "Delete",
|
"delete": "Delete",
|
||||||
"delete_all_data": "Delete all data",
|
"delete_all_data": "Delete all data",
|
||||||
"delete_game": "Delete Game",
|
"delete_game": "Delete Game",
|
||||||
|
"delete_game_with_matches_warning": "If you delete this game template, {count, plural, =1{1 match} other{{count} matches}} using this game template will also be deleted.",
|
||||||
|
"@delete_game_with_matches_warning": {
|
||||||
|
"placeholders": {
|
||||||
|
"count": {
|
||||||
|
"type": "int"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"delete_group": "Delete Group",
|
"delete_group": "Delete Group",
|
||||||
"delete_match": "Delete Match",
|
"delete_match": "Delete Match",
|
||||||
"description": "Description",
|
"description": "Description",
|
||||||
|
|||||||
@@ -302,6 +302,12 @@ abstract class AppLocalizations {
|
|||||||
/// **'Delete Game'**
|
/// **'Delete Game'**
|
||||||
String get delete_game;
|
String get delete_game;
|
||||||
|
|
||||||
|
/// No description provided for @delete_game_with_matches_warning.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'If you delete this game template, {count, plural, =1{1 match} other{{count} matches}} using this game template will also be deleted.'**
|
||||||
|
String delete_game_with_matches_warning(int count);
|
||||||
|
|
||||||
/// Confirmation dialog for deleting a group
|
/// Confirmation dialog for deleting a group
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
|
|||||||
@@ -114,6 +114,17 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get delete_game => 'Spielvorlage löschen';
|
String get delete_game => 'Spielvorlage löschen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String delete_game_with_matches_warning(int count) {
|
||||||
|
String _temp0 = intl.Intl.pluralLogic(
|
||||||
|
count,
|
||||||
|
locale: localeName,
|
||||||
|
other: '$count Spiele',
|
||||||
|
one: '1 Spiel',
|
||||||
|
);
|
||||||
|
return 'Wenn du diese Spielvorlage löschst, werden $_temp0 mit dieser Spielvorlage ebenfalls gelöscht.';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get delete_group => 'Gruppe löschen';
|
String get delete_group => 'Gruppe löschen';
|
||||||
|
|
||||||
|
|||||||
@@ -114,6 +114,17 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get delete_game => 'Delete Game';
|
String get delete_game => 'Delete Game';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String delete_game_with_matches_warning(int count) {
|
||||||
|
String _temp0 = intl.Intl.pluralLogic(
|
||||||
|
count,
|
||||||
|
locale: localeName,
|
||||||
|
other: '$count matches',
|
||||||
|
one: '1 match',
|
||||||
|
);
|
||||||
|
return 'If you delete this game template, $_temp0 using this game template will also be deleted.';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get delete_group => 'Delete Group';
|
String get delete_group => 'Delete Group';
|
||||||
|
|
||||||
|
|||||||
@@ -159,7 +159,7 @@ class _ChooseGameViewState extends State<ChooseGameView> {
|
|||||||
adaptivePageRoute(
|
adaptivePageRoute(
|
||||||
builder: (context) => CreateGameView(
|
builder: (context) => CreateGameView(
|
||||||
gameToEdit: game,
|
gameToEdit: game,
|
||||||
canDelete: canDeleteGame(game),
|
matchCount: getMatchCount(game),
|
||||||
onGameChanged: () {
|
onGameChanged: () {
|
||||||
widget.onGamesUpdated?.call();
|
widget.onGamesUpdated?.call();
|
||||||
},
|
},
|
||||||
@@ -224,11 +224,10 @@ class _ChooseGameViewState extends State<ChooseGameView> {
|
|||||||
gameCounts = await db.gameDao.getGameUsage();
|
gameCounts = await db.gameDao.getGameUsage();
|
||||||
}
|
}
|
||||||
|
|
||||||
// A game can only be deleted if there are no matches using it
|
// Returns the number of matches that use the given [game].
|
||||||
bool canDeleteGame(Game game) {
|
int getMatchCount(Game game) {
|
||||||
final count = gameCounts
|
return gameCounts
|
||||||
.firstWhere((gc) => gc.$1.id == game.id, orElse: () => (game, 0))
|
.firstWhere((gc) => gc.$1.id == game.id, orElse: () => (game, 0))
|
||||||
.$2;
|
.$2;
|
||||||
return count == 0;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ class CreateGameView extends StatefulWidget {
|
|||||||
super.key,
|
super.key,
|
||||||
required this.onGameChanged,
|
required this.onGameChanged,
|
||||||
this.gameToEdit,
|
this.gameToEdit,
|
||||||
this.canDelete = false,
|
this.matchCount = 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
/// Callback to invoke when the game is created or edited
|
/// Callback to invoke when the game is created or edited
|
||||||
@@ -34,7 +34,7 @@ class CreateGameView extends StatefulWidget {
|
|||||||
/// An optional game to prefill the fields
|
/// An optional game to prefill the fields
|
||||||
final Game? gameToEdit;
|
final Game? gameToEdit;
|
||||||
|
|
||||||
final bool canDelete;
|
final int matchCount;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<CreateGameView> createState() => _CreateGameViewState();
|
State<CreateGameView> createState() => _CreateGameViewState();
|
||||||
@@ -139,45 +139,59 @@ class _CreateGameViewState extends State<CreateGameView> {
|
|||||||
if (isEditMode())
|
if (isEditMode())
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.delete),
|
icon: const Icon(Icons.delete),
|
||||||
onPressed: widget.canDelete
|
onPressed: () async {
|
||||||
? () async {
|
if (!context.mounted) return;
|
||||||
showDialog<bool>(
|
|
||||||
context: context,
|
// Build the dialog content based on match count
|
||||||
builder: (context) => CustomAlertDialog(
|
final String dialogContent = widget.matchCount > 0
|
||||||
title: loc.delete_game,
|
? loc.delete_game_with_matches_warning(widget.matchCount)
|
||||||
content: Text(loc.this_cannot_be_undone),
|
: loc.this_cannot_be_undone;
|
||||||
actions: [
|
|
||||||
CustomDialogAction(
|
showDialog<bool>(
|
||||||
onPressed: () =>
|
context: context,
|
||||||
Navigator.of(context).pop(false),
|
builder: (context) => CustomAlertDialog(
|
||||||
text: loc.cancel,
|
title: loc.delete_game,
|
||||||
),
|
content: Text(dialogContent),
|
||||||
CustomDialogAction(
|
actions: [
|
||||||
onPressed: () =>
|
CustomDialogAction(
|
||||||
Navigator.of(context).pop(true),
|
isDestructive: true,
|
||||||
text: loc.delete,
|
onPressed: () => Navigator.of(context).pop(true),
|
||||||
),
|
text: loc.delete,
|
||||||
],
|
),
|
||||||
),
|
CustomDialogAction(
|
||||||
).then((confirmed) async {
|
onPressed: () => Navigator.of(context).pop(false),
|
||||||
if (confirmed == true && context.mounted) {
|
buttonType: ButtonType.secondary,
|
||||||
bool success = await db.gameDao.deleteGame(
|
text: loc.cancel,
|
||||||
gameId: widget.gameToEdit!.id,
|
),
|
||||||
);
|
],
|
||||||
if (!context.mounted) return;
|
),
|
||||||
if (success) {
|
).then((confirmed) async {
|
||||||
widget.onGameChanged.call();
|
if (confirmed == true && context.mounted) {
|
||||||
Navigator.of(
|
// Delete assocaited matches
|
||||||
context,
|
if (widget.matchCount > 0) {
|
||||||
).pop((game: widget.gameToEdit, delete: true));
|
await db.matchDao.deleteMatchesByGame(
|
||||||
} else {
|
gameId: widget.gameToEdit!.id,
|
||||||
if (!mounted) return;
|
);
|
||||||
showSnackbar(message: loc.error_deleting_game);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
: null,
|
|
||||||
|
// Delete the targetted game
|
||||||
|
bool success = await db.gameDao.deleteGame(
|
||||||
|
gameId: widget.gameToEdit!.id,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!context.mounted) return;
|
||||||
|
if (success) {
|
||||||
|
widget.onGameChanged.call();
|
||||||
|
Navigator.of(
|
||||||
|
context,
|
||||||
|
).pop((game: widget.gameToEdit, delete: true));
|
||||||
|
} else {
|
||||||
|
if (!mounted) return;
|
||||||
|
showSnackbar(message: loc.error_deleting_game);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ class AnimatedDialogButton extends StatefulWidget {
|
|||||||
required this.onPressed,
|
required this.onPressed,
|
||||||
this.buttonConstraints,
|
this.buttonConstraints,
|
||||||
this.buttonType = ButtonType.primary,
|
this.buttonType = ButtonType.primary,
|
||||||
|
this.isDescructive = false,
|
||||||
});
|
});
|
||||||
|
|
||||||
final String buttonText;
|
final String buttonText;
|
||||||
@@ -24,6 +25,8 @@ class AnimatedDialogButton extends StatefulWidget {
|
|||||||
|
|
||||||
final ButtonType buttonType;
|
final ButtonType buttonType;
|
||||||
|
|
||||||
|
final bool isDescructive;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<AnimatedDialogButton> createState() => _AnimatedDialogButtonState();
|
State<AnimatedDialogButton> createState() => _AnimatedDialogButtonState();
|
||||||
}
|
}
|
||||||
@@ -33,28 +36,8 @@ class _AnimatedDialogButtonState extends State<AnimatedDialogButton> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final textStyling = TextStyle(
|
final textStyling = _getTextStyling();
|
||||||
color: widget.buttonType == ButtonType.primary
|
final buttonDecoration = _getButtonDecoration();
|
||||||
? Colors.black
|
|
||||||
: Colors.white,
|
|
||||||
fontSize: 16,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
);
|
|
||||||
|
|
||||||
final buttonDecoration = widget.buttonType == ButtonType.primary
|
|
||||||
// Primary
|
|
||||||
? BoxDecoration(
|
|
||||||
color: Colors.white,
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
)
|
|
||||||
: widget.buttonType == ButtonType.secondary
|
|
||||||
// Secondary
|
|
||||||
? BoxDecoration(
|
|
||||||
border: BoxBorder.all(color: Colors.white, width: 2),
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
)
|
|
||||||
// Tertiary
|
|
||||||
: const BoxDecoration();
|
|
||||||
|
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onTapDown: (_) => setState(() => _isPressed = true),
|
onTapDown: (_) => setState(() => _isPressed = true),
|
||||||
@@ -84,4 +67,42 @@ class _AnimatedDialogButtonState extends State<AnimatedDialogButton> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TextStyle _getTextStyling() {
|
||||||
|
late Color textColor;
|
||||||
|
if (widget.buttonType == ButtonType.primary) {
|
||||||
|
textColor = widget.isDescructive ? Colors.white : Colors.black;
|
||||||
|
} else if (widget.buttonType == ButtonType.secondary) {
|
||||||
|
textColor = widget.isDescructive ? Colors.red : Colors.white;
|
||||||
|
} else {
|
||||||
|
textColor = widget.isDescructive ? Colors.red : Colors.white;
|
||||||
|
}
|
||||||
|
|
||||||
|
return TextStyle(
|
||||||
|
color: textColor,
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
BoxDecoration _getButtonDecoration() {
|
||||||
|
if (widget.buttonType == ButtonType.primary) {
|
||||||
|
// Primary
|
||||||
|
return BoxDecoration(
|
||||||
|
color: widget.isDescructive ? Colors.red : Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
);
|
||||||
|
} else if (widget.buttonType == ButtonType.secondary) {
|
||||||
|
// Secondary
|
||||||
|
return BoxDecoration(
|
||||||
|
border: BoxBorder.all(
|
||||||
|
color: widget.isDescructive ? Colors.red : Colors.white,
|
||||||
|
width: 2,
|
||||||
|
),
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// Tertiary
|
||||||
|
return const BoxDecoration();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ class CustomDialogAction extends StatelessWidget {
|
|||||||
required this.onPressed,
|
required this.onPressed,
|
||||||
required this.text,
|
required this.text,
|
||||||
this.buttonType = ButtonType.primary,
|
this.buttonType = ButtonType.primary,
|
||||||
|
this.isDestructive = false,
|
||||||
});
|
});
|
||||||
|
|
||||||
final String text;
|
final String text;
|
||||||
@@ -20,12 +21,15 @@ class CustomDialogAction extends StatelessWidget {
|
|||||||
|
|
||||||
final VoidCallback onPressed;
|
final VoidCallback onPressed;
|
||||||
|
|
||||||
|
final bool isDestructive;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return AnimatedDialogButton(
|
return AnimatedDialogButton(
|
||||||
onPressed: onPressed,
|
onPressed: onPressed,
|
||||||
buttonText: text,
|
buttonText: text,
|
||||||
buttonType: buttonType,
|
buttonType: buttonType,
|
||||||
|
isDescructive: isDestructive,
|
||||||
buttonConstraints: const BoxConstraints(minWidth: 300),
|
buttonConstraints: const BoxConstraints(minWidth: 300),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -366,5 +366,55 @@ void main() {
|
|||||||
expect(match.group, isNotNull);
|
expect(match.group, isNotNull);
|
||||||
expect(match.group!.id, testGroup1.id);
|
expect(match.group!.id, testGroup1.id);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('getMatchCountByGame() works correctly', () async {
|
||||||
|
var count = await database.matchDao.getMatchCountByGame(
|
||||||
|
gameId: testGame.id,
|
||||||
|
);
|
||||||
|
expect(count, 0);
|
||||||
|
|
||||||
|
await database.matchDao.addMatch(match: testMatch1);
|
||||||
|
count = await database.matchDao.getMatchCountByGame(gameId: testGame.id);
|
||||||
|
expect(count, 1);
|
||||||
|
|
||||||
|
await database.matchDao.addMatch(match: testMatch2);
|
||||||
|
count = await database.matchDao.getMatchCountByGame(gameId: testGame.id);
|
||||||
|
expect(count, 2);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('getMatchCountByGame() returns 0 for non-existent game', () async {
|
||||||
|
final count = await database.matchDao.getMatchCountByGame(
|
||||||
|
gameId: 'non-existent-game-id',
|
||||||
|
);
|
||||||
|
expect(count, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('deleteMatchesByGame() deletes all matches for a game', () async {
|
||||||
|
await database.matchDao.addMatch(match: testMatch1);
|
||||||
|
await database.matchDao.addMatch(match: testMatch2);
|
||||||
|
|
||||||
|
var count = await database.matchDao.getMatchCountByGame(
|
||||||
|
gameId: testGame.id,
|
||||||
|
);
|
||||||
|
expect(count, 2);
|
||||||
|
|
||||||
|
final deletedCount = await database.matchDao.deleteMatchesByGame(
|
||||||
|
gameId: testGame.id,
|
||||||
|
);
|
||||||
|
expect(deletedCount, 2);
|
||||||
|
|
||||||
|
count = await database.matchDao.getMatchCountByGame(gameId: testGame.id);
|
||||||
|
expect(count, 0);
|
||||||
|
|
||||||
|
final allMatches = await database.matchDao.getAllMatches();
|
||||||
|
expect(allMatches, isEmpty);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('deleteMatchesByGame() returns 0 for non-existent game', () async {
|
||||||
|
final deletedCount = await database.matchDao.deleteMatchesByGame(
|
||||||
|
gameId: 'non-existent-game-id',
|
||||||
|
);
|
||||||
|
expect(deletedCount, 0);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user