From e64cbac84ee5d1d96692d7a0e764329ed6d0947a Mon Sep 17 00:00:00 2001 From: paul019 <39464035+paul019@users.noreply.github.com> Date: Tue, 10 Sep 2024 10:36:45 +0200 Subject: [PATCH] Add graphical editor (#10) --- lib/l10n/app_de.arb | 5 +- lib/l10n/app_en.arb | 5 +- .../tournament_tree_model.dart | 26 +++ .../tournament_tree_view.dart | 2 +- .../settings_page/tournament_tree_card.dart | 157 ++++++++++++------ .../tournament_tree_entry_editor.dart | 105 ++++++++++++ 6 files changed, 244 insertions(+), 56 deletions(-) create mode 100644 lib/screens/settings_page/tournament_tree_entry_editor.dart diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index 7a8dd74..5b0df48 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -117,5 +117,8 @@ "notSpecifiedYet": "Noch nicht festgelegt", "text": "Text", "video": "Video", - "yes": "Ja" + "yes": "Ja", + "clickBoxToEdit": "(Box anklicken zum Bearbeiten)", + "editTournamentTreeEntry": "Eintrag bearbeiten", + "save": "Speichern" } \ No newline at end of file diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index f0846b9..5e373be 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -117,5 +117,8 @@ "notSpecifiedYet": "Not specified yet", "text": "Text", "video": "Video", - "yes": "Yes" + "yes": "Yes", + "clickBoxToEdit": "(Click box to edit)", + "editTournamentTreeEntry": "Edit entry", + "save": "Save" } \ No newline at end of file diff --git a/lib/models/tournament_tree_model/tournament_tree_model.dart b/lib/models/tournament_tree_model/tournament_tree_model.dart index 3ea5d7c..bb586c1 100644 --- a/lib/models/tournament_tree_model/tournament_tree_model.dart +++ b/lib/models/tournament_tree_model/tournament_tree_model.dart @@ -44,4 +44,30 @@ class TournamentTreeModel { bool get isEmpty => stages.isEmpty; int get numOfStages => stages.length; + + TournamentTreeEntry? getEntryAtCoordinate(int i, int j) { + if (numOfStages > i) { + final stage = stages[i]; + if (stage.entries.length > j) { + return stage.entries[j]; + } + } + + return null; + } + + TournamentTreeModel replaceEntryAtCoordinate(int i, int j, TournamentTreeEntry entry) { + final List newStages = List.of(stages); + + if (i < numOfStages) { + final stage = stages[i]; + if (j < stage.entries.length) { + final List newEntries = List.of(stage.entries); + newEntries[j] = entry; + newStages[i] = TournamentTreeStage(entries: newEntries); + } + } + + return TournamentTreeModel(stages: newStages); + } } diff --git a/lib/screens/presentation_screen_two/tournament_tree_view.dart b/lib/screens/presentation_screen_two/tournament_tree_view.dart index 004f83d..b8e83c6 100644 --- a/lib/screens/presentation_screen_two/tournament_tree_view.dart +++ b/lib/screens/presentation_screen_two/tournament_tree_view.dart @@ -32,7 +32,7 @@ class TournamentTreeView extends StatelessWidget { child: Column( mainAxisSize: MainAxisSize.max, children: [ - for (var stage in tournamentTreeModel.stages.reversed) + for (var stage in tournamentTreeModel.stages) Flexible( flex: 1, child: Row( diff --git a/lib/screens/settings_page/tournament_tree_card.dart b/lib/screens/settings_page/tournament_tree_card.dart index ca64e16..b6be1a9 100644 --- a/lib/screens/settings_page/tournament_tree_card.dart +++ b/lib/screens/settings_page/tournament_tree_card.dart @@ -1,9 +1,13 @@ +import 'dart:math'; + import 'package:flutter/material.dart'; import 'package:integration_bee_helper/models/settings_model/settings_model.dart'; +import 'package:integration_bee_helper/models/tournament_tree_model/tournament_tree_entry.dart'; import 'package:integration_bee_helper/models/tournament_tree_model/tournament_tree_model.dart'; +import 'package:integration_bee_helper/models/tournament_tree_model/tournament_tree_stage.dart'; +import 'package:integration_bee_helper/screens/settings_page/tournament_tree_entry_editor.dart'; import 'package:integration_bee_helper/services/basic_services/intl_service.dart'; import 'package:integration_bee_helper/services/settings_service/settings_service.dart'; -import 'package:integration_bee_helper/widgets/cancel_save_buttons.dart'; class TournamentTreeCard extends StatefulWidget { final SettingsModel settings; @@ -15,29 +19,7 @@ class TournamentTreeCard extends StatefulWidget { } class _TournamentTreeCardState extends State { - bool hasChanged = false; - - late String tournamentTreeString; - late TextEditingController tournamentTreeStringController; - - @override - void initState() { - tournamentTreeString = widget.settings.tournamentTree.encode(); - tournamentTreeStringController = - TextEditingController(text: tournamentTreeString); - - super.initState(); - } - - @override - void didUpdateWidget(covariant TournamentTreeCard oldWidget) { - tournamentTreeString = widget.settings.tournamentTree.encode(); - tournamentTreeStringController.text = tournamentTreeString; - - hasChanged = false; - - super.didUpdateWidget(oldWidget); - } + static const maxNumberOfStages = 4; @override Widget build(BuildContext context) { @@ -50,47 +32,116 @@ class _TournamentTreeCardState extends State { children: [ Center( child: Padding( - padding: const EdgeInsets.symmetric(vertical: 8.0), + padding: const EdgeInsets.only(top: 8.0), child: Text( MyIntl.of(context).tournamentTree, style: const TextStyle(fontWeight: FontWeight.bold), ), ), ), - const Divider(), - TextField( - decoration: InputDecoration( - border: InputBorder.none, - hintText: MyIntl.of(context).tournamentTree, + Center( + child: Padding( + padding: const EdgeInsets.only(bottom: 8.0), + child: Text( + MyIntl.of(context).clickBoxToEdit, + ), ), - controller: tournamentTreeStringController, - onChanged: (v) => setState(() { - tournamentTreeString = v; - hasChanged = true; - }), - maxLines: 8, ), - if (hasChanged) - CancelSaveButtons( - onCancel: () { - setState(() { - hasChanged = false; - tournamentTreeString = - widget.settings.tournamentTree.encode(); - }); - }, - onSave: () async { - await SettingsService().edit( - tournamentTree: - TournamentTreeModel.decode(tournamentTreeString), - ); - setState(() => hasChanged = false); - }, - ), + const Divider(), + Column( + children: [ + for (var i = 0; i < maxNumberOfStages; i++) + Row( + children: [ + for (var j = 0; j < pow(2, i); j++) + _buildEntry(context, i, j), + ], + ), + ], + ), ], ), ), ), ); } + + TournamentTreeModel get tournamentTree { + final initialTournamentTree = widget.settings.tournamentTree; + final List stages = []; + + for (int i = 0; i < maxNumberOfStages; i++) { + final List entries = []; + + for (int j = 0; j < pow(2, i); j++) { + TournamentTreeEntry? initialEntry = + initialTournamentTree.getEntryAtCoordinate(i, j); + + entries.add(TournamentTreeEntry( + title: initialEntry?.title ?? '', + subtitle: initialEntry?.subtitle ?? '', + flex: 1, + )); + } + + stages.add(TournamentTreeStage(entries: entries)); + } + + return TournamentTreeModel(stages: stages); + } + + Widget _buildEntry(BuildContext context, int i, int j) { + TournamentTreeEntry? entry = tournamentTree.getEntryAtCoordinate(i, j); + bool isEmpty = entry == null || entry.isEmpty; + + return Flexible( + child: Card( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8.0), + ), + color: isEmpty ? null : Colors.white, + child: InkWell( + borderRadius: BorderRadius.circular(8.0), + onTap: () { + TournamentTreeEntryEditor.show( + context: context, + entry: entry, + onSave: (title, subtitle) { + final entry = TournamentTreeEntry( + title: title, + subtitle: subtitle, + flex: 1, + ); + + final newTournamentTree = + tournamentTree.replaceEntryAtCoordinate(i, j, entry); + + SettingsService().edit(tournamentTree: newTournamentTree); + }, + ); + }, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + entry?.title ?? '', + textAlign: TextAlign.center, + style: const TextStyle( + fontWeight: FontWeight.bold, + ), + ), + Text( + entry?.subtitle ?? '', + textAlign: TextAlign.center, + ), + ], + ), + ), + ), + ), + ); + } } diff --git a/lib/screens/settings_page/tournament_tree_entry_editor.dart b/lib/screens/settings_page/tournament_tree_entry_editor.dart new file mode 100644 index 0000000..dd90412 --- /dev/null +++ b/lib/screens/settings_page/tournament_tree_entry_editor.dart @@ -0,0 +1,105 @@ +import 'package:flutter/material.dart'; +import 'package:integration_bee_helper/models/tournament_tree_model/tournament_tree_entry.dart'; +import 'package:integration_bee_helper/services/basic_services/intl_service.dart'; +import 'package:pointer_interceptor/pointer_interceptor.dart'; + +class TournamentTreeEntryEditor extends StatefulWidget { + final TournamentTreeEntry? entry; + final void Function(String, String) onSave; + + const TournamentTreeEntryEditor({ + super.key, + required this.entry, + required this.onSave, + }); + + static void show({ + required BuildContext context, + required TournamentTreeEntry? entry, + required void Function(String, String) onSave, + }) { + showDialog( + context: context, + builder: (BuildContext dialogContext) { + return TournamentTreeEntryEditor( + entry: entry, + onSave: onSave, + ); + }, + ); + } + + @override + State createState() => + _TournamentTreeEntryEditorState(); +} + +class _TournamentTreeEntryEditorState extends State { + late String title; + late String subtitle; + + late TextEditingController titleController; + late TextEditingController subtitleController; + + @override + void initState() { + title = widget.entry?.title ?? ''; + subtitle = widget.entry?.subtitle ?? ''; + + titleController = TextEditingController(text: title); + subtitleController = TextEditingController(text: subtitle); + + super.initState(); + } + + @override + Widget build(BuildContext context) { + return PointerInterceptor( + child: AlertDialog( + title: Text(MyIntl.of(context).editTournamentTreeEntry), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + TextField( + decoration: InputDecoration( + border: InputBorder.none, + hintText: MyIntl.of(context).title, + ), + controller: titleController, + onChanged: (v) => setState(() { + title = v; + }), + maxLines: 1, + ), + TextField( + decoration: InputDecoration( + border: InputBorder.none, + hintText: MyIntl.of(context).subtitle, + ), + controller: subtitleController, + onChanged: (v) => setState(() { + subtitle = v; + }), + maxLines: 1, + ), + ], + ), + actions: [ + TextButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: Text(MyIntl.of(context).cancel), + ), + TextButton( + onPressed: () { + widget.onSave(title, subtitle); + Navigator.of(context).pop(); + }, + child: Text(MyIntl.of(context).save), + ), + ], + ), + ); + } +}