From 53eb2589ab3eb3d2e2d37d2d42d3a5289de8d786 Mon Sep 17 00:00:00 2001 From: Gustl22 Date: Mon, 2 Sep 2024 14:16:08 +0200 Subject: [PATCH] feat: Transcript for team matches (#50) --- .../lib/l10n/app_de.arb | 19 +- .../lib/l10n/app_en.arb | 17 + .../lib/services/print/pdf/components.dart | 67 +++- .../lib/services/print/pdf/pdf_sheet.dart | 197 ++++++++++ .../lib/services/print/pdf/score_sheet.dart | 289 +++++--------- .../print/pdf/team_match_transcript.dart | 363 ++++++++++++++++++ .../lib/view/app_navigation.dart | 2 +- .../screens/display/bout/bout_display.dart | 20 +- .../screens/display/match/match_display.dart | 40 +- .../lib/view/screens/home/home.dart | 2 +- .../team_match/team_match_bout_overview.dart | 55 ++- .../team_match/team_match_overview.dart | 59 ++- wrestling_scoreboard_client/pubspec.lock | 34 +- wrestling_scoreboard_client/pubspec.yaml | 9 +- 14 files changed, 928 insertions(+), 245 deletions(-) create mode 100644 wrestling_scoreboard_client/lib/services/print/pdf/pdf_sheet.dart create mode 100644 wrestling_scoreboard_client/lib/services/print/pdf/team_match_transcript.dart diff --git a/wrestling_scoreboard_client/lib/l10n/app_de.arb b/wrestling_scoreboard_client/lib/l10n/app_de.arb index d2d567d0..11f973e2 100644 --- a/wrestling_scoreboard_client/lib/l10n/app_de.arb +++ b/wrestling_scoreboard_client/lib/l10n/app_de.arb @@ -1,6 +1,8 @@ { "appName": "Ringerkampf-Anzeige", - "home": "Start", + "start": "Start", + "home": "Heim", + "guest": "Gast", "explore": "Erkunden", "more": "Mehr", "settings": "Einstellungen", @@ -100,7 +102,18 @@ "endDate": "Ende", "wrestlingRulesPdf": "https://www.ringen.de/wp-content/uploads/2019/01/Internationales-Regelwerk_Januar-2019_.pdf", + "transcriptTeamMatches": "Protokoll für Mannschaftskämpfe", + "scoreSheetSingleCompetitions": "Punktzettel für Einzelmeisterschaften", + "signature": "Unterschrift", + "pool": "Pool", + "round": "Runde", + "mat": "Matte", + "status": "Status", + "visitors": "Besucher", + "numberAbbreviation": "Nr.", + "total": "Gesamt", "participantVacant": "unbesetzt", + "weight": "Gewicht", "weightClass": "Gewichtsklasse", "weightClasses": "Gewichtsklassen", @@ -166,6 +179,10 @@ "boutResultDsqAbbr": "DQ", "boutResultDsq2Abbr": "DQ2", "actions": "Aktionen", + "point": "Punkt", + "points": "Punkte", + "technicalPoints": "Technische Punkte", + "classificationPoints": "Gruppen-Punkte", "participations": "Teilnahmen", "boutConfig": "Kampf-Konfiguration", diff --git a/wrestling_scoreboard_client/lib/l10n/app_en.arb b/wrestling_scoreboard_client/lib/l10n/app_en.arb index cc677ff0..1906d186 100644 --- a/wrestling_scoreboard_client/lib/l10n/app_en.arb +++ b/wrestling_scoreboard_client/lib/l10n/app_en.arb @@ -3,7 +3,9 @@ "@appName": { "description": "The internationalized app name" }, + "start": "Start", "home": "Home", + "guest": "Guest", "explore": "Explore", "more": "More", "settings": "Settings", @@ -103,7 +105,18 @@ "endDate": "End", "wrestlingRulesPdf": "https://uww.org/sites/default/files/2019-12/wrestling_rules.pdf", + "transcriptTeamMatches": "Transcript for Team Matches", + "scoreSheetSingleCompetitions": "Score sheet for Single Competitions", + "signature": "Signature", + "pool": "Pool", + "round": "Round", + "mat": "Mat", + "status": "Status", + "visitors": "Visitors", + "numberAbbreviation": "No.", + "total": "Total", "participantVacant": "vacant", + "weight": "Weight", "weightClass": "Weight Class", "weightClasses": "Weight Classes", @@ -169,6 +182,10 @@ "boutResultDsqAbbr": "DSQ", "boutResultDsq2Abbr": "DSQ2", "actions": "Actions", + "point": "point", + "points": "Points", + "technicalPoints": "Technical Points", + "classificationPoints": "Classification Points", "participations": "Participations", "boutConfig": "Bout configuration", diff --git a/wrestling_scoreboard_client/lib/services/print/pdf/components.dart b/wrestling_scoreboard_client/lib/services/print/pdf/components.dart index 511c9562..f1fedc79 100644 --- a/wrestling_scoreboard_client/lib/services/print/pdf/components.dart +++ b/wrestling_scoreboard_client/lib/services/print/pdf/components.dart @@ -1,13 +1,36 @@ import 'package:pdf/pdf.dart'; import 'package:pdf/widgets.dart'; +import 'package:wrestling_scoreboard_client/services/print/pdf/pdf_sheet.dart'; +import 'package:wrestling_scoreboard_common/common.dart'; + +buildCheckBox({ + bool isChecked = false, + PdfColor pencilColor = PdfColors.blue800, + PdfColor? checkBoxColor, +}) => + Container( + color: checkBoxColor, + margin: const EdgeInsets.all(4), + height: 20, + width: 20, + foregroundDecoration: BoxDecoration( + border: Border.all( + color: PdfColors.grey, + width: .5, + ), + ), + alignment: Alignment.center, + child: isChecked ? Text('×', style: TextStyle(fontSize: 20, color: pencilColor)) : null, + ); Widget buildTextCell( String title, { double? height = 60, double? width, double? fontSize, - PdfColor borderColor = PdfColors.grey, - PdfColor textColor = PdfColors.black, + PdfColor? borderColor, + double? borderWidth, + PdfColor? textColor, PdfColor? color, EdgeInsets? margin, Alignment alignment = Alignment.centerLeft, @@ -19,19 +42,39 @@ Widget buildTextCell( alignment: alignment, foregroundDecoration: BoxDecoration( border: Border.all( - color: borderColor, - width: .5, + color: borderColor ?? PdfColors.grey, + width: borderWidth ?? .5, ), ), height: height, width: width, - child: Text(title, style: TextStyle(fontSize: fontSize, color: textColor))); + child: Text(title, style: TextStyle(fontSize: fontSize, color: textColor ?? PdfColors.black))); } Widget buildFormCell({ String? title, String? content, - double height = 60, + double height = 40, + double? width, + PdfColor borderColor = PdfColors.grey, + PdfColor? color, + PdfColor pencilColor = PdfColors.blue800, +}) { + return buildFormCellWidget( + title: title, + content: content == null ? null : Text(content, style: TextStyle(fontSize: 11, color: pencilColor)), + height: height, + width: width, + borderColor: borderColor, + color: color, + pencilColor: pencilColor, + ); +} + +Widget buildFormCellWidget({ + String? title, + Widget? content, + double height = 40, double? width, PdfColor borderColor = PdfColors.grey, PdfColor? color, @@ -59,9 +102,19 @@ Widget buildFormCell({ Expanded( child: Container( alignment: Alignment.center, - child: Text(content, style: TextStyle(fontSize: 12, color: pencilColor)), + child: content, )), ], ), ); } + +extension BoutRolePdfColor on BoutRole { + PdfColor? get pdfColor { + return this == BoutRole.red ? PdfSheet.homeColor : PdfSheet.guestColor; + } + + PdfColor? get textPdfColor { + return PdfColors.white; + } +} diff --git a/wrestling_scoreboard_client/lib/services/print/pdf/pdf_sheet.dart b/wrestling_scoreboard_client/lib/services/print/pdf/pdf_sheet.dart new file mode 100644 index 00000000..d018a8a1 --- /dev/null +++ b/wrestling_scoreboard_client/lib/services/print/pdf/pdf_sheet.dart @@ -0,0 +1,197 @@ +import 'dart:typed_data'; + +import 'package:flutter/material.dart' show BuildContext; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:pdf/pdf.dart'; +import 'package:pdf/widgets.dart'; +import 'package:printing/printing.dart'; +import 'package:wrestling_scoreboard_client/localization/date_time.dart'; +import 'package:wrestling_scoreboard_client/services/print/pdf/components.dart'; +import 'package:wrestling_scoreboard_common/common.dart'; + +abstract class PdfSheet { + static const PdfPageFormat a4 = + PdfPageFormat(21.0 * PdfPageFormat.cm, 29.7 * PdfPageFormat.cm, marginAll: 1.0 * PdfPageFormat.cm); + static const PdfPageFormat a4Cross = + PdfPageFormat(29.7 * PdfPageFormat.cm, 21.0 * PdfPageFormat.cm, marginAll: 1.0 * PdfPageFormat.cm); + + static const horizontalGap = 8.0; + static const verticalGap = 8.0; + + static const pencilColor = PdfColors.blue900; + static const homeColor = PdfColors.red; + static const guestColor = PdfColors.blue; + + final PdfColor baseColor; + final PdfColor accentColor; + late final AppLocalizations localizations; + final BuildContext buildContext; + + PdfSheet({ + this.baseColor = PdfColors.blueGrey500, + this.accentColor = PdfColors.blueGrey900, + required this.buildContext, + }) { + localizations = AppLocalizations.of(buildContext)!; + } + + Future buildPdf({PdfPageFormat? pageFormat}); + + Widget buildFooter(Context context) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text( + '© ${DateTime.now().year} - August Oberhauser', + style: const TextStyle(fontSize: 6, color: PdfColors.grey500), + ), + Text( + 'Page ${context.pageNumber}/${context.pagesCount}', + style: const TextStyle(fontSize: 8, color: PdfColors.grey800), + ), + ], + ); + } + + Future buildTheme({ + PdfPageFormat pageFormat = PdfSheet.a4, + Font? base, + Font? bold, + Font? italic, + }) async { + return PageTheme( + pageFormat: pageFormat, + theme: ThemeData.withFont( + base: base ?? await PdfGoogleFonts.robotoRegular(), + bold: bold ?? await PdfGoogleFonts.robotoBold(), + italic: italic ?? await PdfGoogleFonts.robotoItalic(), + ), + ); + } + + Widget buildInfo(Context context, WrestlingEvent wrestlingEvent) { + return Table(columnWidths: { + 0: const FlexColumnWidth(2), + 1: const FlexColumnWidth(1), + 2: const FlexColumnWidth(1), + 3: const FlexColumnWidth(1), + 4: const FixedColumnWidth(120), + }, children: [ + TableRow( + children: [ + buildFormCell( + title: 'Event-${localizations.name}', + // ?? localizations.location, + content: wrestlingEvent is TeamMatch + ? ('${wrestlingEvent.home.team.name} – ${wrestlingEvent.guest.team.name}') + : (wrestlingEvent as Competition).name, + color: PdfColors.grey100, + pencilColor: PdfSheet.pencilColor, + height: 40), + if (wrestlingEvent is TeamMatch) + buildFormCell( + title: localizations.league, + // ?? localizations.location, + content: wrestlingEvent.league?.fullname, + color: PdfColors.grey100, + pencilColor: PdfSheet.pencilColor, + height: 40), + if (wrestlingEvent is! TeamMatch) + buildFormCell( + title: 'Competition', + // ?? localizations.location, + content: 'Competition', + color: PdfColors.grey100, + pencilColor: PdfSheet.pencilColor, + height: 40), + buildFormCell( + title: localizations.date, + content: wrestlingEvent.date.toDateTimeStringFromLocaleName(localizations.localeName), + color: PdfColors.grey100, + pencilColor: PdfSheet.pencilColor, + height: 40), + buildFormCell( + title: 'Ort', + // ?? localizations.location, + content: wrestlingEvent.location, + color: PdfColors.grey100, + pencilColor: PdfSheet.pencilColor, + height: 40), + ], + ), + ]); + } + + Widget buildPerson({required String title, String? no, double? width}) { + const cellHeight = 30.0; + return buildFormCell(title: '$title (Name/Nr.)', content: no, height: cellHeight, width: width); + } + + List buildStaff(Context context, WrestlingEvent wrestlingEvent, {double? width}) { + Person? timeKeeper; + Person? transcriptWriter; + if (wrestlingEvent is TeamMatch) { + timeKeeper = wrestlingEvent.timeKeeper; + transcriptWriter = wrestlingEvent.transcriptWriter; + } else if (wrestlingEvent is Competition) {} + + return [ + buildPerson( + title: localizations.timeKeeper.toUpperCase(), + no: timeKeeper == null ? '' : '${timeKeeper.id} / ${timeKeeper.fullName}', + width: width), + buildPerson( + title: localizations.transcriptionWriter.toUpperCase(), + no: transcriptWriter == null ? '' : '${transcriptWriter.id} / ${transcriptWriter.fullName}', + width: width), + ]; + } + + List buildStewards(Context context, WrestlingEvent wrestlingEvent, {double? width}) { + List stewards = []; + // TODO: stewards from list + if (stewards.length < 3) { + stewards.addAll(Iterable.generate(3 - stewards.length, (i) => null)); + } + return stewards + .map( + (steward) => buildPerson( + title: localizations.steward.toUpperCase(), + no: steward == null ? '' : '${steward.id} / ${steward.fullName}', + width: width), + ) + .toList(); + } + + List buildReferees(Context context, WrestlingEvent wrestlingEvent, {double? width}) { + Person? matChairman; + Person? referee; + Person? judge; + if (wrestlingEvent is TeamMatch) { + matChairman = wrestlingEvent.matChairman; + referee = wrestlingEvent.referee; + judge = wrestlingEvent.judge; + } else if (wrestlingEvent is Competition) { + // TODO: get referees from bout + // matChairman = bout.matChairman; + // referee = bout.referee; + // judge = bout.judge; + } + + return [ + buildPerson( + title: localizations.matChairman.toUpperCase(), + no: matChairman == null ? '' : '${matChairman.id} / ${matChairman.fullName}', + width: width), + buildPerson( + title: localizations.referee.toUpperCase(), + no: referee == null ? '' : '${referee.id} / ${referee.fullName}', + width: width), + buildPerson( + title: localizations.judge.toUpperCase(), + no: judge == null ? '' : '${judge.id} / ${judge.fullName}', + width: width), + ]; + } +} diff --git a/wrestling_scoreboard_client/lib/services/print/pdf/score_sheet.dart b/wrestling_scoreboard_client/lib/services/print/pdf/score_sheet.dart index b6839ed6..0d27f78f 100644 --- a/wrestling_scoreboard_client/lib/services/print/pdf/score_sheet.dart +++ b/wrestling_scoreboard_client/lib/services/print/pdf/score_sheet.dart @@ -2,88 +2,67 @@ import 'dart:math'; import 'dart:typed_data'; import 'package:flutter/services.dart' show rootBundle; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:pdf/pdf.dart'; import 'package:pdf/widgets.dart'; -import 'package:printing/printing.dart'; -import 'package:wrestling_scoreboard_client/localization/date_time.dart'; import 'package:wrestling_scoreboard_client/localization/duration.dart'; import 'package:wrestling_scoreboard_client/services/print/pdf/components.dart'; -import 'package:wrestling_scoreboard_client/view/screens/display/bout/bout_display.dart'; +import 'package:wrestling_scoreboard_client/services/print/pdf/pdf_sheet.dart'; import 'package:wrestling_scoreboard_common/common.dart'; -// TODO: Replace boutState with individual dataTypes or introduce model. -Future generateScoreSheet(BoutState boutState, - {PdfPageFormat? pageFormat, required, required AppLocalizations localizations}) async { - final scoreSheet = ScoreSheet( - boutState: boutState, - localizations: localizations, - baseColor: PdfColors.blueGrey500, - accentColor: PdfColors.blueGrey900, - ); - - return await scoreSheet.buildPdf(pageFormat: pageFormat); -} - -class ScoreSheet { +class ScoreSheet extends PdfSheet { ScoreSheet({ - required this.boutState, - required this.baseColor, - required this.accentColor, - required this.localizations, + required this.bout, + required this.boutActions, + required this.wrestlingEvent, + required this.boutConfig, + super.baseColor, + super.accentColor, + required super.buildContext, }); - static const PdfPageFormat a4 = - PdfPageFormat(21.0 * PdfPageFormat.cm, 29.7 * PdfPageFormat.cm, marginAll: 1.0 * PdfPageFormat.cm); - - static const horizontalGap = 8.0; - static const verticalGap = 8.0; - - final BoutState boutState; - final AppLocalizations localizations; - - Bout get bout => boutState.bout; + final Bout bout; + final List boutActions; + final BoutConfig boutConfig; + final WrestlingEvent wrestlingEvent; - WrestlingEvent get event => boutState.widget.wrestlingEvent; - final PdfColor baseColor; - final PdfColor accentColor; - - static const _pencilColor = PdfColors.blue900; - static const _homeColor = PdfColors.red; - static const _guestColor = PdfColors.blue; + WrestlingEvent get event => wrestlingEvent; String? _logo; + @override Future buildPdf({PdfPageFormat? pageFormat}) async { final doc = Document(); _logo = await rootBundle.loadString('assets/images/icons/launcher.svg'); - final actions = await boutState.getActions(); + final actions = boutActions; -// Add page to the PDF + // Add page to the PDF doc.addPage( MultiPage( - pageTheme: _buildTheme( - pageFormat ?? a4, - await PdfGoogleFonts.robotoRegular(), - await PdfGoogleFonts.robotoBold(), - await PdfGoogleFonts.robotoItalic(), - ), + pageTheme: await buildTheme(), header: _buildHeader, - footer: _buildFooter, + footer: buildFooter, build: (context) => [ - _buildInfoHeader1(context), - Container(height: verticalGap), + Container(height: PdfSheet.verticalGap), + buildInfo(context, event), + Container(height: PdfSheet.verticalGap), _buildInfoHeader2(context), - Container(height: verticalGap), + Container(height: PdfSheet.verticalGap), _buildParticipantsHeader(context), - Container(height: verticalGap), + Container(height: PdfSheet.verticalGap), _buildPointsBody(context, actions), + Container(height: PdfSheet.verticalGap), + Column( + children: [ + ...buildReferees(context, event, width: 120.0), + ...buildStaff(context, event, width: 120.0), + ], + ), ], ), ); -// Return the PDF file content + // Return the PDF file content return doc.save(); } @@ -103,7 +82,7 @@ class ScoreSheet { height: 25, alignment: Alignment.centerLeft, child: Text( - 'PUNKTZETTEL FÜR EINZELMEISTERSCHAFTEN', + localizations.scoreSheetSingleCompetitions.toUpperCase(), style: TextStyle( color: baseColor, fontWeight: FontWeight.bold, @@ -115,63 +94,8 @@ class ScoreSheet { ]); } - Widget _buildInfoHeader1(Context context) { - buildCheckBox({bool isChecked = false}) => Container( - margin: const EdgeInsets.all(4), - height: 20, - width: 20, - foregroundDecoration: BoxDecoration( - border: Border.all( - color: PdfColors.grey, - width: .5, - ), - ), - alignment: Alignment.center, - child: isChecked ? Text('×', style: const TextStyle(fontSize: 20, color: _pencilColor)) : null); - - buildJudges({required String title, String? no}) { - const cellHeight = 20.0; - return TableRow(children: [ - buildTextCell(title, fontSize: 7, height: cellHeight), - buildFormCell(title: 'Nr.', content: no, height: cellHeight, width: 60), - ]); - } - - final isFreeStyle = bout.weightClass?.style == WrestlingStyle.free; - - final wrestlingEvent = event; - Person? matChairman; - Person? referee; - Person? judge; - if (wrestlingEvent is TeamMatch) { - matChairman = wrestlingEvent.matChairman; - referee = wrestlingEvent.referee; - judge = wrestlingEvent.judge; - } else if (wrestlingEvent is Competition) { -// TODO: get referees from bout -// matChairman = bout.matChairman; -// referee = bout.referee; -// judge = bout.judge; - } - return Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - Row(children: [ - Text(localizations.freeStyle, style: const TextStyle(fontSize: 7)), - buildCheckBox(isChecked: isFreeStyle), - buildCheckBox(isChecked: !isFreeStyle), - Text(localizations.grecoRoman, style: const TextStyle(fontSize: 7)), - ]), - Table(children: [ - buildJudges(title: localizations.matChairman.toUpperCase(), no: matChairman?.id.toString() ?? ''), - buildJudges(title: localizations.referee.toUpperCase(), no: referee?.id.toString() ?? ''), - buildJudges(title: localizations.judge.toUpperCase(), no: judge?.id.toString() ?? ''), - ]), - ]); - } - Widget _buildInfoHeader2(Context context) { + final isFreeStyle = bout.weightClass?.style == WrestlingStyle.free; return Table( columnWidths: { 0: const FlexColumnWidth(2), @@ -185,29 +109,48 @@ class ScoreSheet { children: [ TableRow( children: [ - buildFormCell( - title: localizations.date, - content: event.date.toDateStringFromLocaleName(localizations.localeName), + buildFormCellWidget( + title: localizations.wrestlingStyle, + content: Row(mainAxisSize: MainAxisSize.max, mainAxisAlignment: MainAxisAlignment.center, children: [ + Text(localizations.freeStyle, style: const TextStyle(fontSize: 7)), + buildCheckBox( + isChecked: isFreeStyle, pencilColor: PdfSheet.pencilColor, checkBoxColor: PdfColors.white), + buildCheckBox( + isChecked: !isFreeStyle, pencilColor: PdfSheet.pencilColor, checkBoxColor: PdfColors.white), + Text(localizations.grecoRoman, style: const TextStyle(fontSize: 7)), + ]), color: PdfColors.grey300, - pencilColor: _pencilColor), + pencilColor: PdfSheet.pencilColor), buildFormCell( title: '${localizations.weightClass} (${bout.weightClass?.unit.toAbbr()})', content: bout.weightClass?.weight.toString(), color: PdfColors.grey300, - pencilColor: _pencilColor), + pencilColor: PdfSheet.pencilColor), buildFormCell( title: localizations.boutNo, // TODO: change to boutNo content: bout.id?.toString() ?? '', color: PdfColors.grey300, - pencilColor: _pencilColor), + pencilColor: PdfSheet.pencilColor), buildFormCell( - title: 'POOL' /*localizations.pool*/, + title: localizations.pool.toUpperCase(), content: bout.pool?.toString() ?? '', color: PdfColors.grey300, - pencilColor: _pencilColor), - buildFormCell(title: 'RUNDE', content: '', color: PdfColors.grey300, pencilColor: _pencilColor), - buildFormCell(title: 'PLATZ', content: '', color: PdfColors.grey300, pencilColor: _pencilColor), - buildFormCell(title: 'MATTE', content: '', color: PdfColors.grey300, pencilColor: _pencilColor), + pencilColor: PdfSheet.pencilColor), + buildFormCell( + title: localizations.round.toUpperCase(), + content: '', + color: PdfColors.grey300, + pencilColor: PdfSheet.pencilColor), + buildFormCell( + title: localizations.place.toUpperCase(), + content: '', + color: PdfColors.grey300, + pencilColor: PdfSheet.pencilColor), + buildFormCell( + title: localizations.mat.toUpperCase(), + content: '', + color: PdfColors.grey300, + pencilColor: PdfSheet.pencilColor), ], ), ], @@ -218,11 +161,15 @@ class ScoreSheet { Widget buildParticipantNameColumn({String? name, String? club, required PdfColor borderColor}) { return Column(children: [ buildFormCell( - title: localizations.name, content: name, pencilColor: _pencilColor, height: 40, borderColor: borderColor), + title: localizations.name, + content: name, + pencilColor: PdfSheet.pencilColor, + height: 40, + borderColor: borderColor), buildFormCell( title: 'NATION / VERBAND / ${localizations.club}', content: club, - pencilColor: _pencilColor, + pencilColor: PdfSheet.pencilColor, height: 40, borderColor: borderColor), ]); @@ -230,9 +177,9 @@ class ScoreSheet { Widget buildParticipantColumn({required bool isLeft, Membership? membership, required PdfColor borderColor}) { final numberCell = buildFormCell( - title: 'NR.', + title: localizations.numberAbbreviation.toUpperCase(), content: membership?.person.id?.toString() ?? '', - pencilColor: _pencilColor, + pencilColor: PdfSheet.pencilColor, height: 80, borderColor: borderColor); final content = [ @@ -271,9 +218,11 @@ class ScoreSheet { } return Row(children: [ - buildParticipantColumn(isLeft: true, membership: bout.r?.participation.membership, borderColor: _homeColor), - Container(width: horizontalGap), - buildParticipantColumn(isLeft: false, membership: bout.b?.participation.membership, borderColor: _guestColor), + buildParticipantColumn( + isLeft: true, membership: bout.r?.participation.membership, borderColor: PdfSheet.homeColor), + Container(width: PdfSheet.horizontalGap), + buildParticipantColumn( + isLeft: false, membership: bout.b?.participation.membership, borderColor: PdfSheet.guestColor), ]); } @@ -282,7 +231,7 @@ class ScoreSheet { const roundCellHeight = 35.0; const breakCellHeight = 15.0; - final rounds = boutState.boutConfig.periodCount; + final rounds = boutConfig.periodCount; Widget buildColorCell({required String colorStr, required PdfColor borderColor}) => Transform.rotateBox( angle: pi * 0.5, @@ -290,19 +239,21 @@ class ScoreSheet { child: buildTextCell( colorStr, height: 40, - width: headerCellHeight + verticalGap + (rounds * roundCellHeight) + ((rounds - 1) * breakCellHeight), + width: + headerCellHeight + PdfSheet.verticalGap + (rounds * roundCellHeight) + ((rounds - 1) * breakCellHeight), borderColor: borderColor, textColor: borderColor, alignment: Alignment.center, )); - Widget buildTotalCell(PdfColor borderColor) => - buildTextCell('TOTAL', height: headerCellHeight, borderColor: borderColor, alignment: Alignment.center); - Widget buildTechnicalPointsHeaderCell(PdfColor borderColor) => buildTextCell('TECHNISCHE PUNKTE', + Widget buildTotalCell(PdfColor borderColor) => buildTextCell(localizations.total.toUpperCase(), height: headerCellHeight, borderColor: borderColor, alignment: Alignment.center); + Widget buildTechnicalPointsHeaderCell(PdfColor borderColor) => + buildTextCell(localizations.technicalPoints.toUpperCase(), + height: headerCellHeight, borderColor: borderColor, alignment: Alignment.center); TableRow buildRound({required int round}) { - final periodDurMin = boutState.boutConfig.periodDuration * round; - final periodDurMax = periodDurMin + boutState.boutConfig.periodDuration; + final periodDurMin = boutConfig.periodDuration * round; + final periodDurMax = periodDurMin + boutConfig.periodDuration; final periodActions = actions.where( (element) => element.duration.compareTo(periodDurMax) <= 0 && element.duration.compareTo(periodDurMin) > 0); final periodActionsRed = periodActions.where((element) => element.role == BoutRole.red); @@ -314,20 +265,20 @@ class ScoreSheet { .map((e) => e.pointCount ?? 0) .fold(0, (cur, next) => (cur + next)) .toString(), - borderColor: _homeColor, + borderColor: PdfSheet.homeColor, height: roundCellHeight), buildFormCell( content: periodActionsRed.map((e) => e.toString()).join(', '), - borderColor: _homeColor, + borderColor: PdfSheet.homeColor, height: roundCellHeight), - buildTextCell('ROUND\n${round + 1}', + buildTextCell('${localizations.round.toUpperCase()}\n${round + 1}', height: roundCellHeight, - margin: const EdgeInsets.symmetric(horizontal: horizontalGap), + margin: const EdgeInsets.symmetric(horizontal: PdfSheet.horizontalGap), fontSize: 8, alignment: Alignment.center), buildFormCell( content: periodActionsBlue.map((e) => e.toString()).join(', '), - borderColor: _guestColor, + borderColor: PdfSheet.guestColor, height: roundCellHeight), buildFormCell( content: periodActionsBlue @@ -335,7 +286,7 @@ class ScoreSheet { .map((e) => e.pointCount ?? 0) .fold(0, (cur, next) => (cur + next)) .toString(), - borderColor: _guestColor, + borderColor: PdfSheet.guestColor, height: roundCellHeight), ]); } @@ -346,15 +297,15 @@ class ScoreSheet { roundRows.add(buildRound(round: round)); if (round < (rounds - 1)) { final breakDurationStr = - '${localizations.breakDuration}: ${boutState.boutConfig.breakDuration.formatMinutesAndSeconds()}'; + '${localizations.breakDuration}: ${boutConfig.breakDuration.formatMinutesAndSeconds()}'; roundRows.add(TableRow(children: [ - Container(color: _homeColor, height: breakCellHeight), + Container(color: PdfSheet.homeColor, height: breakCellHeight), buildTextCell( breakDurationStr, fontSize: 8, alignment: Alignment.center, - borderColor: _homeColor, - color: _homeColor, + borderColor: PdfSheet.homeColor, + color: PdfSheet.homeColor, textColor: PdfColors.white, height: breakCellHeight, ), @@ -363,12 +314,12 @@ class ScoreSheet { breakDurationStr, fontSize: 8, alignment: Alignment.center, - borderColor: _guestColor, - color: _guestColor, + borderColor: PdfSheet.guestColor, + color: PdfSheet.guestColor, textColor: PdfColors.white, height: breakCellHeight, ), - Container(color: _guestColor, height: breakCellHeight), + Container(color: PdfSheet.guestColor, height: breakCellHeight), ])); } } @@ -384,50 +335,22 @@ class ScoreSheet { }, children: [ TableRow(children: [ - buildTotalCell(_homeColor), - buildTechnicalPointsHeaderCell(_homeColor), + buildTotalCell(PdfSheet.homeColor), + buildTechnicalPointsHeaderCell(PdfSheet.homeColor), Container(), - buildTechnicalPointsHeaderCell(_guestColor), - buildTotalCell(_guestColor), + buildTechnicalPointsHeaderCell(PdfSheet.guestColor), + buildTotalCell(PdfSheet.guestColor), ]), - TableRow(children: [Container(height: verticalGap)]), + TableRow(children: [Container(height: PdfSheet.verticalGap)]), ...roundRows, ], )); } return Row(children: [ - buildColorCell(colorStr: localizations.red.toUpperCase(), borderColor: _homeColor), + buildColorCell(colorStr: localizations.red.toUpperCase(), borderColor: PdfSheet.homeColor), buildTechnicalPoints(), - buildColorCell(colorStr: localizations.blue.toUpperCase(), borderColor: _guestColor), + buildColorCell(colorStr: localizations.blue.toUpperCase(), borderColor: PdfSheet.guestColor), ]); } - - Widget _buildFooter(Context context) { - return Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - Text( - '© ${DateTime.now().year} - August Oberhauser', - style: const TextStyle(fontSize: 6, color: PdfColors.grey500), - ), - Text( - 'Page ${context.pageNumber}/${context.pagesCount}', - style: const TextStyle(fontSize: 8, color: PdfColors.grey800), - ), - ], - ); - } - - PageTheme _buildTheme(PdfPageFormat pageFormat, Font base, Font bold, Font italic) { - return PageTheme( - pageFormat: pageFormat, - theme: ThemeData.withFont( - base: base, - bold: bold, - italic: italic, - ), - ); - } } diff --git a/wrestling_scoreboard_client/lib/services/print/pdf/team_match_transcript.dart b/wrestling_scoreboard_client/lib/services/print/pdf/team_match_transcript.dart new file mode 100644 index 00000000..6e085682 --- /dev/null +++ b/wrestling_scoreboard_client/lib/services/print/pdf/team_match_transcript.dart @@ -0,0 +1,363 @@ +import 'dart:typed_data'; + +import 'package:flutter/services.dart' show rootBundle; +import 'package:pdf/pdf.dart'; +import 'package:pdf/widgets.dart'; +import 'package:wrestling_scoreboard_client/localization/bout_result.dart'; +import 'package:wrestling_scoreboard_client/localization/wrestling_style.dart'; +import 'package:wrestling_scoreboard_client/services/print/pdf/components.dart'; +import 'package:wrestling_scoreboard_client/services/print/pdf/pdf_sheet.dart'; +import 'package:wrestling_scoreboard_common/common.dart'; + +class TeamMatchTranscript extends PdfSheet { + TeamMatchTranscript({ + required this.teamMatchBoutActions, + required this.teamMatch, + required this.boutConfig, + super.baseColor, + super.accentColor, + required super.buildContext, + }); + + final Map> teamMatchBoutActions; + final BoutConfig boutConfig; + final TeamMatch teamMatch; + + Iterable get bouts => teamMatchBoutActions.keys.map((tmb) => tmb.bout); + + TeamMatch get event => teamMatch; + String? _logo; + + @override + Future buildPdf({PdfPageFormat? pageFormat}) async { + final doc = Document(); + + _logo = await rootBundle.loadString('assets/images/icons/launcher.svg'); + final homePoints = TeamMatch.getHomePoints(bouts); + final guestPoints = TeamMatch.getGuestPoints(bouts); + final winner = homePoints > guestPoints + ? teamMatch.home.team.name + : homePoints < guestPoints + ? teamMatch.guest.team.name + : ''; + + // Add page to the PDF + doc.addPage( + MultiPage( + pageTheme: await buildTheme(pageFormat: pageFormat ?? PdfSheet.a4Cross), + header: _buildHeader, + footer: buildFooter, + build: (context) => [ + buildInfo(context, event), + Container(height: PdfSheet.verticalGap), + _buildBoutTable(context), + Container(height: PdfSheet.verticalGap), + Table( + columnWidths: [ + const FlexColumnWidth(1), // Winner + const FlexColumnWidth(1), // Visitors count + const FlexColumnWidth(6), // Comment + ].asMap(), + children: [ + TableRow(children: [ + buildFormCell(title: localizations.winner, content: winner, height: 30.0, color: PdfColors.grey100), + buildFormCell( + title: localizations.visitors, + content: teamMatch.visitorsCount?.toString() ?? '', + height: 30.0, + color: PdfColors.grey100), + buildFormCell( + title: localizations.comment, + content: teamMatch.comment ?? '', + height: 30.0, + color: PdfColors.grey100) + ]), + ]), + Container(height: PdfSheet.verticalGap), + _buildPersons(context), + ], + ), + ); + + // Return the PDF file content + return doc.save(); + } + + Widget _buildHeader(Context context) { + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Container( + height: 25, + alignment: Alignment.centerLeft, + child: Text( + localizations.transcriptTeamMatches.toUpperCase(), + style: TextStyle( + color: baseColor, + fontWeight: FontWeight.bold, + fontSize: 12, + ), + ), + ), + Container( + padding: const EdgeInsets.all(8), + height: 48, + child: _logo != null ? SvgImage(svg: _logo!) : null, + ) + ], + ); + } + + Widget _buildPersons(Context context) { + final signaturePersons = [...buildReferees(context, event), ...buildTeamLeader(context, event)]; + final staff = buildStaff(context, event); + final stewards = buildStewards(context, event); + return Table( + defaultColumnWidth: const FlexColumnWidth(1), + children: [ + TableRow( + children: [ + ...signaturePersons.map( + (p) => Column( + children: [p, buildFormCell(title: localizations.signature, height: 30.0, color: PdfColors.grey100)]), + ), + Column(children: staff), + Column(children: stewards), + ].map((child) => Container(padding: const EdgeInsets.symmetric(horizontal: 2), child: child)).toList()), + ], + ); + } + + List buildTeamLeader(Context context, TeamMatch teamMatch, {double? width}) { + return [ + buildPerson( + title: '${localizations.home} ${localizations.leader.toUpperCase()}', + no: teamMatch.home.leader?.person.fullName ?? '', + width: width), + buildPerson( + title: '${localizations.guest} ${localizations.leader.toUpperCase()}', + no: teamMatch.guest.leader?.person.fullName ?? '', + width: width), + ]; + } + + Widget _buildBoutTable(Context context) { + const titleCellHeight = 16.0; + const headerCellHeight = 30.0; + const cellHeight = 16.0; + const headerFontSize = 10.0; + const cellFontSize = 10.0; + const marginBottom = EdgeInsets.only(bottom: PdfSheet.verticalGap); + + List participantStateColumnWidths() => [ + const FlexColumnWidth(0.8), // Weight + const FlexColumnWidth(2.2), // Name + const FlexColumnWidth(1), // No + const FlexColumnWidth(0.5), // Status + ]; + + List buildTeamHeader(Team team, BoutRole role) { + final textColor = role.textPdfColor; + return [ + buildTextCell( + '${role == BoutRole.red ? localizations.home : localizations.guest}: ${team.name}', + color: role.pdfColor, + height: titleCellHeight, + textColor: textColor, + fontSize: headerFontSize, + borderColor: role.pdfColor, + alignment: Alignment.center, + ), + ]; + } + + List buildTeamFooter(BoutRole role) { + return [ + Container(color: role.pdfColor, height: titleCellHeight), + Container(color: role.pdfColor, height: titleCellHeight), + Container(color: role.pdfColor, height: titleCellHeight), + Container(color: role.pdfColor, height: titleCellHeight), + ]; + } + + List buildParticipantStateColumnHeaders(BoutRole role) { + final color = role.pdfColor; + final textColor = role.textPdfColor; + return [ + buildTextCell(localizations.weight, + height: headerCellHeight, + color: color, + fontSize: headerFontSize, + textColor: textColor, + borderColor: textColor, + margin: marginBottom), + buildTextCell(localizations.name, + height: headerCellHeight, + color: color, + fontSize: headerFontSize, + textColor: textColor, + borderColor: textColor, + margin: marginBottom), + buildTextCell(localizations.membershipNumber, + height: headerCellHeight, + color: color, + fontSize: headerFontSize, + textColor: textColor, + borderColor: textColor, + margin: marginBottom), + buildTextCell(localizations.status, + height: headerCellHeight, + color: color, + fontSize: headerFontSize, + textColor: textColor, + borderColor: textColor, + margin: marginBottom), + ]; + } + + List buildParticipantState(ParticipantState? state, BoutRole role) { + final borderColor = role.pdfColor; + return [ + buildTextCell(state?.participation.weight?.toString() ?? '', + height: cellHeight, borderColor: borderColor, fontSize: cellFontSize), + buildTextCell(state?.participation.membership.person.fullName ?? '-', + height: cellHeight, borderColor: borderColor, fontSize: cellFontSize), + buildTextCell(state?.participation.membership.no ?? '', + height: cellHeight, borderColor: borderColor, fontSize: cellFontSize), + buildTextCell(state?.participation.membership.person.toStatus() ?? '', + height: cellHeight, borderColor: borderColor, fontSize: cellFontSize), + ]; + } + + return Table( + columnWidths: [ + const FlexColumnWidth(0.5), // No + const FlexColumnWidth(0.8), // Weightclass + const FlexColumnWidth(0.3), // Style + ...participantStateColumnWidths(), + const FlexColumnWidth(0.5), // Technical points red + const FlexColumnWidth(0.5), // Classification points red + const FlexColumnWidth(0.7), // Result + const FlexColumnWidth(0.7), // Duration + const FlexColumnWidth(0.5), // Classification points blue + const FlexColumnWidth(0.5), // Technical points blue + ...participantStateColumnWidths(), + const FlexColumnWidth(1.5), // Comment + ].asMap(), + children: [ + TableRow(columnSpans: const { + 3: 4, + 13: 4 + }, children: [ + Container(height: titleCellHeight), + Container(height: titleCellHeight), + Container(height: titleCellHeight), + ...buildTeamHeader(teamMatch.home.team, BoutRole.red), + Container(color: BoutRole.red.pdfColor, height: titleCellHeight), + Container(color: BoutRole.red.pdfColor, height: titleCellHeight), + Container(height: titleCellHeight), + Container(height: titleCellHeight), + Container(color: BoutRole.blue.pdfColor, height: titleCellHeight), + Container(color: BoutRole.blue.pdfColor, height: titleCellHeight), + ...buildTeamHeader(teamMatch.guest.team, BoutRole.blue), + Container(height: titleCellHeight), + ]), + TableRow( + children: [ + buildTextCell(localizations.boutNo, + height: headerCellHeight, fontSize: headerFontSize, margin: marginBottom), + buildTextCell(localizations.weightClass, + height: headerCellHeight, fontSize: headerFontSize, margin: marginBottom), + buildTextCell(localizations.wrestlingStyle, + height: headerCellHeight, fontSize: headerFontSize, margin: marginBottom), + ...buildParticipantStateColumnHeaders(BoutRole.red), + buildTextCell(localizations.technicalPoints, + height: headerCellHeight, + fontSize: headerFontSize, + color: PdfSheet.homeColor, + textColor: PdfColors.white, + margin: marginBottom, + borderColor: BoutRole.red.textPdfColor), + buildTextCell(localizations.classificationPoints, + height: headerCellHeight, + fontSize: headerFontSize, + color: PdfSheet.homeColor, + textColor: PdfColors.white, + margin: marginBottom, + borderColor: BoutRole.red.textPdfColor), + buildTextCell(localizations.result, + height: headerCellHeight, fontSize: headerFontSize, margin: marginBottom), + buildTextCell(localizations.duration, + height: headerCellHeight, fontSize: headerFontSize, margin: marginBottom), + buildTextCell(localizations.classificationPoints, + height: headerCellHeight, + fontSize: headerFontSize, + color: PdfSheet.guestColor, + textColor: PdfColors.white, + margin: marginBottom, + borderColor: BoutRole.blue.textPdfColor), + buildTextCell(localizations.technicalPoints, + height: headerCellHeight, + fontSize: headerFontSize, + color: PdfSheet.guestColor, + textColor: PdfColors.white, + margin: marginBottom, + borderColor: BoutRole.blue.textPdfColor), + ...buildParticipantStateColumnHeaders(BoutRole.blue), + buildTextCell(localizations.comment, + height: headerCellHeight, fontSize: headerFontSize, margin: marginBottom), + ], + ), + ...teamMatchBoutActions.entries.map((boutEntry) { + final bout = boutEntry.key; + final actions = boutEntry.value; + PdfColor? winnerColor = bout.bout.winnerRole?.pdfColor; + PdfColor? winnerTextColor = bout.bout.winnerRole?.textPdfColor; + return TableRow(children: [ + buildTextCell(bout.pos.toString(), height: cellHeight, fontSize: cellFontSize), + buildTextCell(bout.bout.weightClass?.name ?? '-', height: cellHeight, fontSize: cellFontSize), + buildTextCell(bout.bout.weightClass?.style.abbreviation(buildContext) ?? '-', + height: cellHeight, fontSize: cellFontSize), + ...buildParticipantState(bout.bout.r, BoutRole.red), + buildTextCell(ParticipantState.getTechnicalPoints(actions, BoutRole.red).toString(), + height: cellHeight, borderColor: BoutRole.red.pdfColor, fontSize: cellFontSize), + buildTextCell(bout.bout.r?.classificationPoints?.toString() ?? '', + height: cellHeight, borderColor: BoutRole.red.pdfColor, fontSize: cellFontSize), + buildTextCell(bout.bout.result?.abbreviation(buildContext) ?? '', + height: cellHeight, + alignment: Alignment.center, + color: winnerColor, + textColor: winnerTextColor, + fontSize: cellFontSize), + buildTextCell(durationToString(bout.bout.duration), + height: cellHeight, alignment: Alignment.center, fontSize: cellFontSize), + buildTextCell(bout.bout.b?.classificationPoints?.toString() ?? '', + height: cellHeight, borderColor: BoutRole.blue.pdfColor, fontSize: cellFontSize), + buildTextCell(ParticipantState.getTechnicalPoints(actions, BoutRole.blue).toString(), + height: cellHeight, borderColor: BoutRole.blue.pdfColor, fontSize: cellFontSize), + ...buildParticipantState(bout.bout.b, BoutRole.blue), + // TODO: bout comment + buildTextCell('', height: cellHeight, fontSize: cellFontSize), + ]); + }), + TableRow(columnSpans: const { + 0: 3 + }, children: [ + buildTextCell(localizations.total, height: titleCellHeight, fontSize: headerFontSize), + ...buildTeamFooter(BoutRole.red), + Container(color: BoutRole.red.pdfColor, height: titleCellHeight), + buildTextCell(TeamMatch.getHomePoints(bouts).toString(), + borderColor: BoutRole.red.pdfColor, height: titleCellHeight, fontSize: headerFontSize, borderWidth: 2.0), + Container(height: titleCellHeight), + Container(height: titleCellHeight), + buildTextCell(TeamMatch.getGuestPoints(bouts).toString(), + borderColor: BoutRole.blue.pdfColor, height: titleCellHeight, fontSize: headerFontSize, borderWidth: 2.0), + Container(color: BoutRole.blue.pdfColor, height: titleCellHeight), + ...buildTeamFooter(BoutRole.blue), + Container(height: titleCellHeight), + ]), + ], + ); + } +} diff --git a/wrestling_scoreboard_client/lib/view/app_navigation.dart b/wrestling_scoreboard_client/lib/view/app_navigation.dart index 9d117a68..85f3d6d7 100644 --- a/wrestling_scoreboard_client/lib/view/app_navigation.dart +++ b/wrestling_scoreboard_client/lib/view/app_navigation.dart @@ -46,7 +46,7 @@ class _AppNavigationState extends State { items: [ BottomNavigationBarItem( icon: const Icon(Icons.home), - label: localizations.home, + label: localizations.start, ), BottomNavigationBarItem( icon: const Icon(Icons.explore), diff --git a/wrestling_scoreboard_client/lib/view/screens/display/bout/bout_display.dart b/wrestling_scoreboard_client/lib/view/screens/display/bout/bout_display.dart index 1f5d82c6..f46a449f 100644 --- a/wrestling_scoreboard_client/lib/view/screens/display/bout/bout_display.dart +++ b/wrestling_scoreboard_client/lib/view/screens/display/bout/bout_display.dart @@ -412,18 +412,26 @@ class BoutState extends ConsumerState { @override Widget build(BuildContext context) { - final localizations = AppLocalizations.of(context)!; double width = MediaQuery.of(context).size.width; double padding = width / 100; final bottomPadding = EdgeInsets.only(bottom: padding); Color stopwatchColor = stopwatch == _breakStopwatch ? Colors.orange : Theme.of(context).colorScheme.onSurface; - final shareAction = IconButton( - icon: const Icon(Icons.share), + final pdfAction = IconButton( + icon: const Icon(Icons.print), onPressed: () async { - final bytes = await generateScoreSheet(this, localizations: localizations); - Printing.sharePdf(bytes: bytes); + final actions = await getActions(); + if (context.mounted) { + final bytes = await ScoreSheet( + bout: bout, + buildContext: context, + boutActions: actions, + wrestlingEvent: widget.wrestlingEvent, + boutConfig: boutConfig, + ).buildPdf(); + Printing.sharePdf(bytes: bytes, filename: '${bout.getFileBaseName(widget.wrestlingEvent)}.pdf'); + } }, ); final infoAction = IconButton( @@ -447,7 +455,7 @@ class BoutState extends ConsumerState { navigateToBoutByIndex: saveAndNavigateToBoutByIndex, child: WindowStateScaffold( hideAppBarOnFullscreen: true, - actions: [infoAction, shareAction], + actions: [infoAction, pdfAction], body: SingleChildScrollView( child: Column( children: [ diff --git a/wrestling_scoreboard_client/lib/view/screens/display/match/match_display.dart b/wrestling_scoreboard_client/lib/view/screens/display/match/match_display.dart index 280936bb..409e6f80 100644 --- a/wrestling_scoreboard_client/lib/view/screens/display/match/match_display.dart +++ b/wrestling_scoreboard_client/lib/view/screens/display/match/match_display.dart @@ -1,9 +1,13 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; +import 'package:printing/printing.dart'; import 'package:wrestling_scoreboard_client/localization/bout_result.dart'; import 'package:wrestling_scoreboard_client/localization/bout_utils.dart'; import 'package:wrestling_scoreboard_client/localization/wrestling_style.dart'; +import 'package:wrestling_scoreboard_client/provider/data_provider.dart'; +import 'package:wrestling_scoreboard_client/services/print/pdf/team_match_transcript.dart'; import 'package:wrestling_scoreboard_client/utils/units.dart'; import 'package:wrestling_scoreboard_client/view/screens/display/bout/bout_display.dart'; import 'package:wrestling_scoreboard_client/view/screens/display/common.dart'; @@ -15,7 +19,7 @@ import 'package:wrestling_scoreboard_client/view/widgets/scaled_text.dart'; import 'package:wrestling_scoreboard_client/view/widgets/themed.dart'; import 'package:wrestling_scoreboard_common/common.dart'; -class MatchDisplay extends StatelessWidget { +class MatchDisplay extends ConsumerWidget { static const route = 'display'; static const flexWidths = [17, 50, 30, 50]; @@ -25,7 +29,8 @@ class MatchDisplay extends StatelessWidget { const MatchDisplay({required this.id, this.teamMatch, super.key}); @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { + final localizations = AppLocalizations.of(context)!; double width = MediaQuery.of(context).size.width; double padding = width / 140; return SingleConsumer( @@ -36,16 +41,41 @@ class MatchDisplay extends StatelessWidget { icon: const Icon(Icons.info), onPressed: () => handleSelectedTeamMatch(match, context), ); + final pdfAction = IconButton( + icon: const Icon(Icons.print), + onPressed: () async { + final teamMatchBouts = await ref.read(manyDataStreamProvider( + ManyProviderData(filterObject: match), + ).future); + + final teamMatchBoutActions = Map.fromEntries(await Future.wait(teamMatchBouts.map((teamMatchBout) async { + final boutActions = await ref.read(manyDataStreamProvider( + ManyProviderData(filterObject: teamMatchBout.bout), + ).future); + // final boutActions = await (await ref.read(dataManagerNotifierProvider)).readMany(filterObject: teamMatchBout.bout); + return MapEntry(teamMatchBout, boutActions); + }))); + if (context.mounted) { + final bytes = await TeamMatchTranscript( + teamMatchBoutActions: teamMatchBoutActions, + buildContext: context, + teamMatch: match, + boutConfig: match.league?.division.boutConfig ?? const BoutConfig(), + ).buildPdf(); + Printing.sharePdf(bytes: bytes, filename: '${match.fileBaseName}.pdf'); + } + }, + ); return WindowStateScaffold( hideAppBarOnFullscreen: true, - actions: [infoAction], + actions: [infoAction, pdfAction], body: ManyConsumer( filterObject: match, builder: (context, teamMatchBouts) { final matchInfos = [ match.league?.fullname, - '${AppLocalizations.of(context)!.boutNo}: ${match.id ?? ''}', - if (match.referee != null) '${AppLocalizations.of(context)!.refereeAbbr}: ${match.referee?.fullName}', + '${localizations.boutNo}: ${match.id ?? ''}', + if (match.referee != null) '${localizations.refereeAbbr}: ${match.referee?.fullName}', // Not enough space to display all three referees // if (match.matChairman != null) // '${AppLocalizations.of(context)!.refereeAbbr}: ${match.matChairman?.fullName}', diff --git a/wrestling_scoreboard_client/lib/view/screens/home/home.dart b/wrestling_scoreboard_client/lib/view/screens/home/home.dart index 802c09a9..d49e1fe6 100644 --- a/wrestling_scoreboard_client/lib/view/screens/home/home.dart +++ b/wrestling_scoreboard_client/lib/view/screens/home/home.dart @@ -140,7 +140,7 @@ class HomeState extends ConsumerState { } return WindowStateScaffold( - appBarTitle: Text(localizations.home), + appBarTitle: Text(localizations.start), body: ResponsiveContainer( child: Column( children: [ diff --git a/wrestling_scoreboard_client/lib/view/screens/overview/team_match/team_match_bout_overview.dart b/wrestling_scoreboard_client/lib/view/screens/overview/team_match/team_match_bout_overview.dart index f0b1c816..b217ae77 100644 --- a/wrestling_scoreboard_client/lib/view/screens/overview/team_match/team_match_bout_overview.dart +++ b/wrestling_scoreboard_client/lib/view/screens/overview/team_match/team_match_bout_overview.dart @@ -2,7 +2,10 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; +import 'package:printing/printing.dart'; +import 'package:wrestling_scoreboard_client/provider/data_provider.dart'; import 'package:wrestling_scoreboard_client/provider/network_provider.dart'; +import 'package:wrestling_scoreboard_client/services/print/pdf/score_sheet.dart'; import 'package:wrestling_scoreboard_client/view/screens/display/bout/bout_display.dart'; import 'package:wrestling_scoreboard_client/view/screens/edit/team_match/team_match_bout_edit.dart'; import 'package:wrestling_scoreboard_client/view/screens/overview/bout_overview.dart'; @@ -21,32 +24,56 @@ class TeamMatchBoutOverview extends BoutOverview { @override Widget build(BuildContext context, WidgetRef ref) { final localizations = AppLocalizations.of(context)!; + return SingleConsumer( id: id, initialData: teamMatchBout, - builder: (context, data) { + builder: (context, teamMatchBout) { + final bout = teamMatchBout.bout; + Future> getActions() => ref.read( + manyDataStreamProvider(ManyProviderData(filterObject: bout)).future); + + final pdfAction = IconButton( + icon: const Icon(Icons.print), + onPressed: () async { + final actions = await getActions(); + if (context.mounted) { + final bytes = await ScoreSheet( + bout: bout, + boutActions: actions, + buildContext: context, + wrestlingEvent: teamMatchBout.teamMatch, + boutConfig: teamMatchBout.teamMatch.league?.division.boutConfig ?? const BoutConfig(), + ).buildPdf(); + Printing.sharePdf(bytes: bytes, filename: '${bout.getFileBaseName(teamMatchBout.teamMatch)}.pdf'); + } + }, + ); + return buildOverview( context, ref, classLocale: localizations.bout, editPage: TeamMatchBoutEdit( - teamMatchBout: data, - initialTeamMatch: data.teamMatch, + teamMatchBout: teamMatchBout, + initialTeamMatch: teamMatchBout.teamMatch, ), - onDelete: () async => (await ref.read(dataManagerNotifierProvider)).deleteSingle(data), + onDelete: () async => + (await ref.read(dataManagerNotifierProvider)).deleteSingle(teamMatchBout), tiles: [], actions: [ + pdfAction, Padding( padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 16), child: ElevatedButton.icon( icon: const Icon(Icons.tv), - onPressed: () => handleSelectedBoutDisplay(data, context), + onPressed: () => handleSelectedBoutDisplay(teamMatchBout, context), label: Text(localizations.display), ), ) ], - dataId: data.bout.id!, - initialData: data.bout, + dataId: teamMatchBout.bout.id!, + initialData: teamMatchBout.bout, ); }, ); @@ -57,3 +84,17 @@ class TeamMatchBoutOverview extends BoutOverview { '/${TeamMatchOverview.route}/${bout.teamMatch.id}/${TeamMatchBoutOverview.route}/${bout.id}/${TeamMatchBoutDisplay.route}'); } } + +extension BoutFileExt on Bout { + String getFileBaseName(WrestlingEvent event) { + final fileNameBuilder = [ + event.date.toIso8601String().substring(0, 10), + id?.toString(), + r?.participation.membership.person.surname, + '–', + b?.participation.membership.person.surname, + ]; + fileNameBuilder.removeWhere((e) => e == null || e.isEmpty); + return fileNameBuilder.map((e) => e!.replaceAll(' ', '-')).join('_'); + } +} diff --git a/wrestling_scoreboard_client/lib/view/screens/overview/team_match/team_match_overview.dart b/wrestling_scoreboard_client/lib/view/screens/overview/team_match/team_match_overview.dart index 140c60fc..bc4c5383 100644 --- a/wrestling_scoreboard_client/lib/view/screens/overview/team_match/team_match_overview.dart +++ b/wrestling_scoreboard_client/lib/view/screens/overview/team_match/team_match_overview.dart @@ -2,12 +2,14 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; +import 'package:printing/printing.dart'; import 'package:wrestling_scoreboard_client/localization/bout_utils.dart'; import 'package:wrestling_scoreboard_client/localization/date_time.dart'; import 'package:wrestling_scoreboard_client/localization/season.dart'; import 'package:wrestling_scoreboard_client/provider/data_provider.dart'; import 'package:wrestling_scoreboard_client/provider/network_provider.dart'; import 'package:wrestling_scoreboard_client/services/network/data_manager.dart'; +import 'package:wrestling_scoreboard_client/services/print/pdf/team_match_transcript.dart'; import 'package:wrestling_scoreboard_client/utils/export.dart'; import 'package:wrestling_scoreboard_client/view/screens/display/match/match_display.dart'; import 'package:wrestling_scoreboard_client/view/screens/edit/lineup_edit.dart'; @@ -41,6 +43,32 @@ class TeamMatchOverview extends ConsumerWidget { id: id, initialData: match, builder: (context, match) { + final pdfAction = IconButton( + icon: const Icon(Icons.print), + onPressed: () async { + final teamMatchBouts = await ref.read(manyDataStreamProvider( + ManyProviderData(filterObject: match), + ).future); + + final teamMatchBoutActions = Map.fromEntries(await Future.wait(teamMatchBouts.map((teamMatchBout) async { + final boutActions = await ref.read(manyDataStreamProvider( + ManyProviderData(filterObject: teamMatchBout.bout), + ).future); + // final boutActions = await (await ref.read(dataManagerNotifierProvider)).readMany(filterObject: teamMatchBout.bout); + return MapEntry(teamMatchBout, boutActions); + }))); + if (context.mounted) { + final bytes = await TeamMatchTranscript( + teamMatchBoutActions: teamMatchBoutActions, + buildContext: context, + teamMatch: match, + boutConfig: match.league?.division.boutConfig ?? const BoutConfig(), + ).buildPdf(); + Printing.sharePdf(bytes: bytes, filename: '${match.fileBaseName}.pdf'); + } + }, + ); + return OverviewScaffold( dataObject: match, label: localizations.match, @@ -51,23 +79,12 @@ class TeamMatchOverview extends ConsumerWidget { onPressed: () async { final reporter = match.organization?.getReporter(); if (reporter != null) { - final fileNameBuilder = [ - match.date.toIso8601String().substring(0, 10), - match.league?.fullname, - match.no, - match.home.team.name, - '–', - match.guest.team.name, - ]; - fileNameBuilder.removeWhere((e) => e == null || e.isEmpty); - final fileBaseName = fileNameBuilder.map((e) => e!.replaceAll(' ', '-')).join('_'); - final bouts = await _getBouts(ref, match: match); final boutMap = Map.fromEntries(await Future.wait( bouts.map((bout) async => MapEntry(bout, await _getActions(ref, bout: bout))))); final reportStr = reporter.exportTeamMatchReport(match, boutMap); - await exportRDB(fileBaseName: fileBaseName, rdbString: reportStr); + await exportRDB(fileBaseName: match.fileBaseName, rdbString: reportStr); } else { if (context.mounted) { showExceptionDialog( @@ -81,6 +98,7 @@ class TeamMatchOverview extends ConsumerWidget { } }, icon: const Icon(Icons.description)), + pdfAction, Padding( padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 16), child: ElevatedButton.icon( @@ -185,7 +203,7 @@ class TeamMatchOverview extends ConsumerWidget { icon: Icons.history_edu, ), ContentItem( - title: '-', // Multiple stewards + title: '-', // TODO: Multiple stewards subtitle: localizations.steward, icon: Icons.security, ), @@ -292,3 +310,18 @@ class TeamMatchOverview extends ConsumerWidget { ); } } + +extension TeamMatchFileExt on TeamMatch { + String get fileBaseName { + final fileNameBuilder = [ + date.toIso8601String().substring(0, 10), + league?.fullname, + no, + home.team.name, + '–', + guest.team.name, + ]; + fileNameBuilder.removeWhere((e) => e == null || e.isEmpty); + return fileNameBuilder.map((e) => e!.replaceAll(' ', '-')).join('_'); + } +} diff --git a/wrestling_scoreboard_client/pubspec.lock b/wrestling_scoreboard_client/pubspec.lock index fd787902..e56962f3 100644 --- a/wrestling_scoreboard_client/pubspec.lock +++ b/wrestling_scoreboard_client/pubspec.lock @@ -95,13 +95,12 @@ packages: source: hosted version: "7.0.0" audioplayers_web: - dependency: "direct overridden" + dependency: transitive description: - path: "packages/audioplayers_web" - ref: HEAD - resolved-ref: f284cc99038881abaf40b9f1961321b695dbc3e2 - url: "https://github.com/bluefireteam/audioplayers.git" - source: git + name: audioplayers_web + sha256: "3609bdf0e05e66a3d9750ee40b1e37f2a622c4edb796cc600b53a90a30a2ace4" + url: "https://pub.dev" + source: hosted version: "5.0.1" audioplayers_windows: dependency: transitive @@ -267,10 +266,10 @@ packages: dependency: transitive description: name: coverage - sha256: "7b594a150942e0d3be99cd45a1d0b5caff27ba5a27f292ed8e8d904ba3f167b5" + sha256: c1fb2dce3c0085f39dc72668e85f8e0210ec7de05345821ff58530567df345a5 url: "https://pub.dev" source: hosted - version: "1.9.1" + version: "1.9.2" cross_file: dependency: transitive description: @@ -689,7 +688,7 @@ packages: description: path: "packages/package_info_plus/package_info_plus" ref: HEAD - resolved-ref: "91f48a6bc7d11c4238c9539ca06e6fa768995580" + resolved-ref: "4e6def451016ca07d1f1ce80b881225e1f52b577" url: "https://github.com/fluttercommunity/plus_plugins.git" source: git version: "8.0.2" @@ -768,10 +767,11 @@ packages: pdf: dependency: "direct main" description: - name: pdf - sha256: "05df53f8791587402493ac97b9869d3824eccbc77d97855f4545cf72df3cae07" - url: "https://pub.dev" - source: hosted + path: pdf + ref: "313-column-span" + resolved-ref: a893e72cee6920ed3f6e0e340d408524b70874fd + url: "https://github.com/Gustl22/dart_pdf.git" + source: git version: "3.11.1" pdf_widget_wrapper: dependency: transitive @@ -965,10 +965,10 @@ packages: dependency: transitive description: name: shelf_static - sha256: a41d3f53c4adf0f57480578c1d61d90342cd617de7fc8077b1304643c2d85c1e + sha256: c87c3875f91262785dade62d135760c2c69cb217ac759485334c5857ad89f6e3 url: "https://pub.dev" source: hosted - version: "1.1.2" + version: "1.1.3" shelf_web_socket: dependency: transitive description: @@ -1186,10 +1186,10 @@ packages: dependency: transitive description: name: uuid - sha256: "83d37c7ad7aaf9aa8e275490669535c8080377cfa7a7004c24dfac53afffaa90" + sha256: f33d6bb662f0e4f79dcd7ada2e6170f3b3a2530c28fc41f49a411ddedd576a77 url: "https://pub.dev" source: hosted - version: "4.4.2" + version: "4.5.0" vector_math: dependency: transitive description: diff --git a/wrestling_scoreboard_client/pubspec.yaml b/wrestling_scoreboard_client/pubspec.yaml index 01d44d2e..955cca8f 100644 --- a/wrestling_scoreboard_client/pubspec.yaml +++ b/wrestling_scoreboard_client/pubspec.yaml @@ -51,11 +51,12 @@ dev_dependencies: build_runner: ^2.4.7 dependency_overrides: - # TODO(gustl22): Remove when published: https://github.com/bluefireteam/audioplayers/pull/1828 - audioplayers_web: + # TODO(gustl22): Remove when published: https://github.com/DavBfr/dart_pdf/pull/1736 + pdf: git: - url: https://github.com/bluefireteam/audioplayers.git - path: packages/audioplayers_web + url: https://github.com/Gustl22/dart_pdf.git + path: pdf + ref: 313-column-span # TODO(gustl22): Remove when published: https://github.com/fluttercommunity/plus_plugins/commit/05f8afb8fc43bc702ab5e3e14e3cba9d79983446 package_info_plus: git: