diff --git a/lib/src/model/broadcast/broadcast.dart b/lib/src/model/broadcast/broadcast.dart index aeb754ebad..9188498756 100644 --- a/lib/src/model/broadcast/broadcast.dart +++ b/lib/src/model/broadcast/broadcast.dart @@ -44,6 +44,7 @@ class BroadcastTournamentData with _$BroadcastTournamentData { const factory BroadcastTournamentData({ required BroadcastTournamentId id, required String name, + required String slug, required String? imageUrl, required String? description, // PRIVATE=-1, NORMAL=3, HIGH=4, BEST=5 @@ -71,6 +72,7 @@ class BroadcastRound with _$BroadcastRound { const factory BroadcastRound({ required BroadcastRoundId id, required String name, + required String slug, required RoundStatus status, required DateTime? startsAt, required DateTime? finishedAt, @@ -126,7 +128,31 @@ class BroadcastPlayerExtended with _$BroadcastPlayerExtended { required int played, required double? score, required int? ratingDiff, + required int? performance, }) = _BroadcastPlayerExtended; } +typedef BroadcastFideData = ({({int? standard, int? rapid, int? blitz}) ratings, int? birthYear}); + +typedef BroadcastPlayerResults = + ({ + BroadcastPlayerExtended player, + BroadcastFideData fideData, + IList<BroadcastPlayerResultData> games, + }); + +enum BroadcastPoints { one, half, zero } + +@freezed +class BroadcastPlayerResultData with _$BroadcastPlayerResultData { + const factory BroadcastPlayerResultData({ + required BroadcastRoundId roundId, + required BroadcastGameId gameId, + required Side color, + required BroadcastPoints? points, + required int? ratingDiff, + required BroadcastPlayer opponent, + }) = _BroadcastPlayerResult; +} + enum RoundStatus { live, finished, upcoming } diff --git a/lib/src/model/broadcast/broadcast_federation.dart b/lib/src/model/broadcast/broadcast_federation.dart new file mode 100644 index 0000000000..267b53fc69 --- /dev/null +++ b/lib/src/model/broadcast/broadcast_federation.dart @@ -0,0 +1,205 @@ +const federationIdToName = { + 'FID': 'FIDE', + 'USA': 'United States of America', + 'IND': 'India', + 'CHN': 'China', + 'RUS': 'Russia', + 'AZE': 'Azerbaijan', + 'FRA': 'France', + 'UKR': 'Ukraine', + 'ARM': 'Armenia', + 'GER': 'Germany', + 'ESP': 'Spain', + 'NED': 'Netherlands', + 'HUN': 'Hungary', + 'POL': 'Poland', + 'ENG': 'England', + 'ROU': 'Romania', + 'NOR': 'Norway', + 'UZB': 'Uzbekistan', + 'ISR': 'Israel', + 'CZE': 'Czech Republic', + 'SRB': 'Serbia', + 'CRO': 'Croatia', + 'GRE': 'Greece', + 'IRI': 'Iran', + 'TUR': 'Turkiye', + 'SLO': 'Slovenia', + 'ARG': 'Argentina', + 'SWE': 'Sweden', + 'GEO': 'Georgia', + 'ITA': 'Italy', + 'CUB': 'Cuba', + 'AUT': 'Austria', + 'PER': 'Peru', + 'BUL': 'Bulgaria', + 'BRA': 'Brazil', + 'DEN': 'Denmark', + 'SUI': 'Switzerland', + 'CAN': 'Canada', + 'SVK': 'Slovakia', + 'LTU': 'Lithuania', + 'VIE': 'Vietnam', + 'AUS': 'Australia', + 'BEL': 'Belgium', + 'MNE': 'Montenegro', + 'MDA': 'Moldova', + 'KAZ': 'Kazakhstan', + 'ISL': 'Iceland', + 'COL': 'Colombia', + 'BIH': 'Bosnia & Herzegovina', + 'EGY': 'Egypt', + 'FIN': 'Finland', + 'MGL': 'Mongolia', + 'PHI': 'Philippines', + 'BLR': 'Belarus', + 'LAT': 'Latvia', + 'POR': 'Portugal', + 'CHI': 'Chile', + 'MEX': 'Mexico', + 'MKD': 'North Macedonia', + 'INA': 'Indonesia', + 'PAR': 'Paraguay', + 'EST': 'Estonia', + 'SGP': 'Singapore', + 'SCO': 'Scotland', + 'VEN': 'Venezuela', + 'IRL': 'Ireland', + 'URU': 'Uruguay', + 'TKM': 'Turkmenistan', + 'MAR': 'Morocco', + 'MAS': 'Malaysia', + 'BAN': 'Bangladesh', + 'ALG': 'Algeria', + 'RSA': 'South Africa', + 'AND': 'Andorra', + 'ALB': 'Albania', + 'KGZ': 'Kyrgyzstan', + 'KOS': 'Kosovo *', + 'FAI': 'Faroe Islands', + 'ZAM': 'Zambia', + 'MYA': 'Myanmar', + 'NZL': 'New Zealand', + 'ECU': 'Ecuador', + 'CRC': 'Costa Rica', + 'NGR': 'Nigeria', + 'JPN': 'Japan', + 'SYR': 'Syria', + 'DOM': 'Dominican Republic', + 'LUX': 'Luxembourg', + 'WLS': 'Wales', + 'BOL': 'Bolivia', + 'TUN': 'Tunisia', + 'UAE': 'United Arab Emirates', + 'MNC': 'Monaco', + 'TJK': 'Tajikistan', + 'PAN': 'Panama', + 'LBN': 'Lebanon', + 'NCA': 'Nicaragua', + 'ESA': 'El Salvador', + 'ANG': 'Angola', + 'TTO': 'Trinidad & Tobago', + 'SRI': 'Sri Lanka', + 'IRQ': 'Iraq', + 'JOR': 'Jordan', + 'UGA': 'Uganda', + 'MAD': 'Madagascar', + 'ZIM': 'Zimbabwe', + 'MLT': 'Malta', + 'SUD': 'Sudan', + 'KOR': 'South Korea', + 'PUR': 'Puerto Rico', + 'HON': 'Honduras', + 'GUA': 'Guatemala', + 'PAK': 'Pakistan', + 'JAM': 'Jamaica', + 'THA': 'Thailand', + 'YEM': 'Yemen', + 'LBA': 'Libya', + 'CYP': 'Cyprus', + 'NEP': 'Nepal', + 'HKG': 'Hong Kong, China', + 'SSD': 'South Sudan', + 'BOT': 'Botswana', + 'PLE': 'Palestine', + 'KEN': 'Kenya', + 'AHO': 'Netherlands Antilles', + 'MAW': 'Malawi', + 'LIE': 'Liechtenstein', + 'TPE': 'Chinese Taipei', + 'AFG': 'Afghanistan', + 'MOZ': 'Mozambique', + 'KSA': 'Saudi Arabia', + 'BAR': 'Barbados', + 'NAM': 'Namibia', + 'HAI': 'Haiti', + 'ARU': 'Aruba', + 'CIV': 'Cote d’Ivoire', + 'CPV': 'Cape Verde', + 'SUR': 'Suriname', + 'LBR': 'Liberia', + 'IOM': 'Isle of Man', + 'MTN': 'Mauritania', + 'BRN': 'Bahrain', + 'GHA': 'Ghana', + 'OMA': 'Oman', + 'BRU': 'Brunei Darussalam', + 'GCI': 'Guernsey', + 'GUM': 'Guam', + 'KUW': 'Kuwait', + 'JCI': 'Jersey', + 'MRI': 'Mauritius', + 'SEN': 'Senegal', + 'BAH': 'Bahamas', + 'MDV': 'Maldives', + 'NRU': 'Nauru', + 'TOG': 'Togo', + 'FIJ': 'Fiji', + 'PLW': 'Palau', + 'GUY': 'Guyana', + 'LES': 'Lesotho', + 'CAY': 'Cayman Islands', + 'SOM': 'Somalia', + 'SWZ': 'Eswatini', + 'TAN': 'Tanzania', + 'LCA': 'Saint Lucia', + 'ISV': 'US Virgin Islands', + 'SLE': 'Sierra Leone', + 'BER': 'Bermuda', + 'SMR': 'San Marino', + 'BDI': 'Burundi', + 'QAT': 'Qatar', + 'ETH': 'Ethiopia', + 'DJI': 'Djibouti', + 'SEY': 'Seychelles', + 'PNG': 'Papua New Guinea', + 'DMA': 'Dominica', + 'STP': 'Sao Tome and Principe', + 'MAC': 'Macau', + 'CAM': 'Cambodia', + 'VIN': 'Saint Vincent and the Grenadines', + 'BUR': 'Burkina Faso', + 'COM': 'Comoros Islands', + 'GAB': 'Gabon', + 'RWA': 'Rwanda', + 'CMR': 'Cameroon', + 'MLI': 'Mali', + 'ANT': 'Antigua and Barbuda', + 'CHA': 'Chad', + 'GAM': 'Gambia', + 'COD': 'Democratic Republic of the Congo', + 'SKN': 'Saint Kitts and Nevis', + 'BHU': 'Bhutan', + 'NIG': 'Niger', + 'GRN': 'Grenada', + 'BIZ': 'Belize', + 'CAF': 'Central African Republic', + 'ERI': 'Eritrea', + 'GEQ': 'Equatorial Guinea', + 'IVB': 'British Virgin Islands', + 'LAO': 'Laos', + 'SOL': 'Solomon Islands', + 'TGA': 'Tonga', + 'TLS': 'Timor-Leste', + 'VAN': 'Vanuatu', +}; diff --git a/lib/src/model/broadcast/broadcast_providers.dart b/lib/src/model/broadcast/broadcast_providers.dart index b23e20213d..c8696d35eb 100644 --- a/lib/src/model/broadcast/broadcast_providers.dart +++ b/lib/src/model/broadcast/broadcast_providers.dart @@ -60,6 +60,17 @@ Future<IList<BroadcastPlayerExtended>> broadcastPlayers( return ref.withClient((client) => BroadcastRepository(client).getPlayers(tournamentId)); } +@riverpod +Future<BroadcastPlayerResults> broadcastPlayerResult( + Ref ref, + BroadcastTournamentId broadcastTournamentId, + String playerId, +) { + return ref.withClient( + (client) => BroadcastRepository(client).getPlayerResults(broadcastTournamentId, playerId), + ); +} + @Riverpod(keepAlive: true) BroadcastImageWorkerFactory broadcastImageWorkerFactory(Ref ref) { return const BroadcastImageWorkerFactory(); diff --git a/lib/src/model/broadcast/broadcast_repository.dart b/lib/src/model/broadcast/broadcast_repository.dart index e3c0d4d6ef..832e0e3c8a 100644 --- a/lib/src/model/broadcast/broadcast_repository.dart +++ b/lib/src/model/broadcast/broadcast_repository.dart @@ -45,6 +45,16 @@ class BroadcastRepository { mapper: _makePlayerFromJson, ); } + + Future<BroadcastPlayerResults> getPlayerResults( + BroadcastTournamentId tournamentId, + String playerId, + ) { + return client.readJson( + Uri(path: 'broadcast/$tournamentId/players/$playerId'), + mapper: _makePlayerResultsFromJson, + ); + } } BroadcastList _makeBroadcastResponseFromJson(Map<String, dynamic> json) { @@ -62,13 +72,14 @@ Broadcast _broadcastFromPick(RequiredPick pick) { tour: _tournamentDataFromPick(pick('tour').required()), round: _roundFromPick(pick('round').required()), group: pick('group').asStringOrNull(), - roundToLinkId: pick('roundToLink', 'id').asBroadcastRoundIddOrNull() ?? roundId, + roundToLinkId: pick('roundToLink', 'id').asBroadcastRoundIdOrNull() ?? roundId, ); } BroadcastTournamentData _tournamentDataFromPick(RequiredPick pick) => BroadcastTournamentData( id: pick('id').asBroadcastTournamentIdOrThrow(), name: pick('name').asStringOrThrow(), + slug: pick('slug').asStringOrThrow(), tier: pick('tier').asIntOrNull(), imageUrl: pick('image').asStringOrNull(), description: pick('description').asStringOrNull(), @@ -116,6 +127,7 @@ BroadcastRound _roundFromPick(RequiredPick pick) { return BroadcastRound( id: pick('id').asBroadcastRoundIdOrThrow(), name: pick('name').asStringOrThrow(), + slug: pick('slug').asStringOrThrow(), status: status, startsAt: pick('startsAt').asDateTimeFromMillisecondsOrNull(), finishedAt: pick('finishedAt').asDateTimeFromMillisecondsOrNull(), @@ -209,5 +221,50 @@ BroadcastPlayerExtended _playerExtendedFromPick(RequiredPick pick) { played: pick('played').asIntOrThrow(), score: pick('score').asDoubleOrNull(), ratingDiff: pick('ratingDiff').asIntOrNull(), + performance: pick('performance').asIntOrNull(), + ); +} + +BroadcastPlayerResults _makePlayerResultsFromJson(Map<String, dynamic> json) { + return ( + player: _playerExtendedFromPick(pick(json).required()), + fideData: _fideDataFromPick(pick(json, 'fide')), + games: pick(json, 'games').asListOrThrow(_makePlayerResultFromPick).toIList(), + ); +} + +BroadcastFideData _fideDataFromPick(Pick pick) { + return ( + ratings: ( + standard: pick('ratings', 'standard').asIntOrNull(), + rapid: pick('ratings', 'rapid').asIntOrNull(), + blitz: pick('ratings', 'blitz').asIntOrNull(), + ), + birthYear: pick('year').asIntOrNull(), + ); +} + +BroadcastPlayerResultData _makePlayerResultFromPick(RequiredPick pick) { + final pointsString = pick('points').asStringOrNull(); + BroadcastPoints? points; + if (pointsString == '1') { + points = BroadcastPoints.one; + } else if (pointsString == '1/2') { + points = BroadcastPoints.half; + } else if (pointsString == '0') { + points = BroadcastPoints.zero; + } + + return BroadcastPlayerResultData( + roundId: pick('round').asBroadcastRoundIdOrThrow(), + gameId: pick('id').asBroadcastGameIdOrThrow(), + color: pick('color').asSideOrThrow(), + ratingDiff: pick('ratingDiff').asIntOrNull(), + points: points, + opponent: _playerFromPick( + pick('opponent').required(), + isPlaying: false, + thinkingTime: Duration.zero, + ), ); } diff --git a/lib/src/model/common/id.dart b/lib/src/model/common/id.dart index 2d0d75ea6e..f7cbe93049 100644 --- a/lib/src/model/common/id.dart +++ b/lib/src/model/common/id.dart @@ -8,6 +8,8 @@ extension type const StringId(String value) { bool startsWith(String prefix) => value.startsWith(prefix); } +extension type const IntId(int value) {} + extension type const GameAnyId._(String value) implements StringId { GameAnyId(this.value) : assert(value.length == 8 || value.length == 12); GameId get gameId => GameId(value.substring(0, 8)); @@ -65,7 +67,7 @@ extension type const StudyChapterId(String value) implements StringId { StudyChapterId.fromJson(dynamic json) : this(json as String); } -extension type const FideId(String value) implements StringId {} +extension type const FideId(int value) implements IntId {} extension IDPick on Pick { UserId asUserIdOrThrow() { @@ -180,7 +182,7 @@ extension IDPick on Pick { throw PickException("value $value at $debugParsingExit can't be casted to BroadcastRoundId"); } - BroadcastRoundId? asBroadcastRoundIddOrNull() { + BroadcastRoundId? asBroadcastRoundIdOrNull() { if (value == null) return null; try { return asBroadcastRoundIdOrThrow(); @@ -197,7 +199,7 @@ extension IDPick on Pick { throw PickException("value $value at $debugParsingExit can't be casted to BroadcastGameId"); } - BroadcastGameId? asBroadcastGameIddOrNull() { + BroadcastGameId? asBroadcastGameIdOrNull() { if (value == null) return null; try { return asBroadcastGameIdOrThrow(); @@ -216,7 +218,7 @@ extension IDPick on Pick { FideId asFideIdOrThrow() { final value = required().value; - if (value is String) { + if (value is int && value != 0) { return FideId(value); } throw PickException("value $value at $debugParsingExit can't be casted to FideId"); diff --git a/lib/src/view/broadcast/broadcast_boards_tab.dart b/lib/src/view/broadcast/broadcast_boards_tab.dart index a58bebff61..cb55aa1e23 100644 --- a/lib/src/view/broadcast/broadcast_boards_tab.dart +++ b/lib/src/view/broadcast/broadcast_boards_tab.dart @@ -24,10 +24,15 @@ const _kPlayerWidgetPadding = EdgeInsets.symmetric(vertical: 5.0); /// A tab that displays the live games of a broadcast round. class BroadcastBoardsTab extends ConsumerWidget { - const BroadcastBoardsTab({required this.roundId, required this.broadcastTitle}); + const BroadcastBoardsTab({ + required this.tournamentId, + required this.roundId, + required this.tournamentSlug, + }); + final BroadcastTournamentId tournamentId; final BroadcastRoundId roundId; - final String broadcastTitle; + final String tournamentSlug; @override Widget build(BuildContext context, WidgetRef ref) { @@ -58,9 +63,11 @@ class BroadcastBoardsTab extends ConsumerWidget { ) : BroadcastPreview( games: value.games.values.toIList(), + tournamentId: tournamentId, roundId: roundId, - broadcastTitle: broadcastTitle, - roundTitle: value.round.name, + title: value.round.name, + tournamentSlug: tournamentSlug, + roundSlug: value.round.slug, ), AsyncError(:final error) => SliverFillRemaining( child: Center(child: Text('Could not load broadcast: $error')), @@ -73,20 +80,28 @@ class BroadcastBoardsTab extends ConsumerWidget { class BroadcastPreview extends StatelessWidget { const BroadcastPreview({ + required this.tournamentId, required this.roundId, required this.games, - required this.broadcastTitle, - required this.roundTitle, + required this.title, + required this.tournamentSlug, + required this.roundSlug, }); - const BroadcastPreview.loading({required this.roundId, required this.broadcastTitle}) - : games = null, - roundTitle = null; + const BroadcastPreview.loading() + : tournamentId = const BroadcastTournamentId(''), + roundId = const BroadcastRoundId(''), + games = null, + title = '', + tournamentSlug = '', + roundSlug = ''; + final BroadcastTournamentId tournamentId; final BroadcastRoundId roundId; final IList<BroadcastGame>? games; - final String broadcastTitle; - final String? roundTitle; + final String title; + final String tournamentSlug; + final String roundSlug; @override Widget build(BuildContext context) { @@ -115,7 +130,7 @@ class BroadcastPreview extends StatelessWidget { delegate: SliverChildBuilderDelegate( childCount: games == null ? numberLoadingBoards : games!.length, (context, index) { - if (games == null || roundTitle == null) { + if (games == null) { return ShimmerLoading( isLoading: true, child: BoardThumbnail.loading( @@ -134,13 +149,15 @@ class BroadcastPreview extends StatelessWidget { onTap: () { pushPlatformRoute( context, - title: roundTitle, + title: title, builder: (context) => BroadcastGameScreen( + tournamentId: tournamentId, roundId: roundId, gameId: game.id, - broadcastTitle: broadcastTitle, - roundTitle: roundTitle!, + tournamentSlug: tournamentSlug, + roundSlug: roundSlug, + title: title, ), ); }, diff --git a/lib/src/view/broadcast/broadcast_game_bottom_bar.dart b/lib/src/view/broadcast/broadcast_game_bottom_bar.dart index a9e7ee843d..4283bfcc54 100644 --- a/lib/src/view/broadcast/broadcast_game_bottom_bar.dart +++ b/lib/src/view/broadcast/broadcast_game_bottom_bar.dart @@ -18,14 +18,14 @@ class BroadcastGameBottomBar extends ConsumerWidget { const BroadcastGameBottomBar({ required this.roundId, required this.gameId, - required this.broadcastTitle, - required this.roundTitle, + this.tournamentSlug, + this.roundSlug, }); final BroadcastRoundId roundId; final BroadcastGameId gameId; - final String broadcastTitle; - final String roundTitle; + final String? tournamentSlug; + final String? roundSlug; @override Widget build(BuildContext context, WidgetRef ref) { @@ -40,17 +40,16 @@ class BroadcastGameBottomBar extends ConsumerWidget { showAdaptiveActionSheet<void>( context: context, actions: [ - BottomSheetAction( - makeLabel: (context) => Text(context.l10n.mobileShareGameURL), - onPressed: (context) async { - launchShareDialog( - context, - uri: lichessUri( - '/broadcast/${broadcastTitle.toLowerCase().replaceAll(' ', '-')}/${roundTitle.toLowerCase().replaceAll(' ', '-')}/$roundId/$gameId', - ), - ); - }, - ), + if (tournamentSlug != null && roundSlug != null) + BottomSheetAction( + makeLabel: (context) => Text(context.l10n.mobileShareGameURL), + onPressed: (context) async { + launchShareDialog( + context, + uri: lichessUri('/broadcast/$tournamentSlug/$roundSlug/$roundId/$gameId'), + ); + }, + ), BottomSheetAction( makeLabel: (context) => Text(context.l10n.mobileShareGamePGN), onPressed: (context) async { diff --git a/lib/src/view/broadcast/broadcast_game_screen.dart b/lib/src/view/broadcast/broadcast_game_screen.dart index 078f6e5d73..52cdf4d0db 100644 --- a/lib/src/view/broadcast/broadcast_game_screen.dart +++ b/lib/src/view/broadcast/broadcast_game_screen.dart @@ -7,7 +7,6 @@ import 'package:lichess_mobile/src/constants.dart'; import 'package:lichess_mobile/src/model/analysis/analysis_preferences.dart'; import 'package:lichess_mobile/src/model/broadcast/broadcast.dart'; import 'package:lichess_mobile/src/model/broadcast/broadcast_game_controller.dart'; -import 'package:lichess_mobile/src/model/broadcast/broadcast_round_controller.dart'; import 'package:lichess_mobile/src/model/common/chess.dart'; import 'package:lichess_mobile/src/model/common/eval.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; @@ -19,8 +18,10 @@ import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; import 'package:lichess_mobile/src/view/analysis/analysis_layout.dart'; import 'package:lichess_mobile/src/view/broadcast/broadcast_game_bottom_bar.dart'; +import 'package:lichess_mobile/src/view/broadcast/broadcast_game_screen_providers.dart'; import 'package:lichess_mobile/src/view/broadcast/broadcast_game_settings.dart'; import 'package:lichess_mobile/src/view/broadcast/broadcast_game_tree_view.dart'; +import 'package:lichess_mobile/src/view/broadcast/broadcast_player_results_screen.dart'; import 'package:lichess_mobile/src/view/broadcast/broadcast_player_widget.dart'; import 'package:lichess_mobile/src/view/engine/engine_gauge.dart'; import 'package:lichess_mobile/src/view/engine/engine_lines.dart'; @@ -31,16 +32,20 @@ import 'package:lichess_mobile/src/widgets/pgn.dart'; import 'package:lichess_mobile/src/widgets/platform_scaffold.dart'; class BroadcastGameScreen extends ConsumerStatefulWidget { + final BroadcastTournamentId tournamentId; final BroadcastRoundId roundId; final BroadcastGameId gameId; - final String broadcastTitle; - final String roundTitle; + final String? tournamentSlug; + final String? roundSlug; + final String? title; const BroadcastGameScreen({ + required this.tournamentId, required this.roundId, required this.gameId, - required this.broadcastTitle, - required this.roundTitle, + this.tournamentSlug, + this.roundSlug, + this.title, }); @override @@ -69,13 +74,27 @@ class _BroadcastGameScreenState extends ConsumerState<BroadcastGameScreen> @override Widget build(BuildContext context) { + final broadcastRoundGameState = ref.watch( + broadcastRoundGameProvider(widget.roundId, widget.gameId), + ); final broadcastGameState = ref.watch( broadcastGameControllerProvider(widget.roundId, widget.gameId), ); + final title = + (widget.title != null) + ? Text(widget.title!, overflow: TextOverflow.ellipsis, maxLines: 1) + : switch (ref.watch(broadcastGameScreenTitleProvider(widget.roundId))) { + AsyncData(value: final title) => Text( + title, + overflow: TextOverflow.ellipsis, + maxLines: 1, + ), + _ => const SizedBox.shrink(), + }; return PlatformScaffold( appBar: PlatformAppBar( - title: Text(widget.roundTitle, overflow: TextOverflow.ellipsis, maxLines: 1), + title: title, actions: [ AppBarAnalysisTabIndicator(tabs: tabs, controller: _tabController), AppBarIconButton( @@ -93,15 +112,17 @@ class _BroadcastGameScreenState extends ConsumerState<BroadcastGameScreen> ), ], ), - body: switch (broadcastGameState) { - AsyncData() => _Body( + body: switch ((broadcastRoundGameState, broadcastGameState)) { + (AsyncData(), AsyncData()) => _Body( + widget.tournamentId, widget.roundId, widget.gameId, - widget.broadcastTitle, - widget.roundTitle, + widget.tournamentSlug, + widget.roundSlug, tabController: _tabController, ), - AsyncError(:final error) => Center(child: Text('Cannot load broadcast game: $error')), + (AsyncError(:final error), _) => Center(child: Text('Cannot load broadcast game: $error')), + (_, AsyncError(:final error)) => Center(child: Text('Cannot load broadcast game: $error')), _ => const Center(child: CircularProgressIndicator.adaptive()), }, ); @@ -110,17 +131,19 @@ class _BroadcastGameScreenState extends ConsumerState<BroadcastGameScreen> class _Body extends ConsumerWidget { const _Body( + this.tournamentId, this.roundId, this.gameId, - this.broadcastTitle, - this.roundTitle, { + this.tournamentSlug, + this.roundSlug, { required this.tabController, }); + final BroadcastTournamentId tournamentId; final BroadcastRoundId roundId; final BroadcastGameId gameId; - final String broadcastTitle; - final String roundTitle; + final String? tournamentSlug; + final String? roundSlug; final TabController tabController; @override @@ -140,11 +163,13 @@ class _Body extends ConsumerWidget { (context, boardSize, borderRadius) => _BroadcastBoard(roundId, gameId, boardSize, borderRadius), boardHeader: _PlayerWidget( + tournamentId: tournamentId, roundId: roundId, gameId: gameId, widgetPosition: _PlayerWidgetPosition.top, ), boardFooter: _PlayerWidget( + tournamentId: tournamentId, roundId: roundId, gameId: gameId, widgetPosition: _PlayerWidgetPosition.bottom, @@ -179,8 +204,8 @@ class _Body extends ConsumerWidget { bottomBar: BroadcastGameBottomBar( roundId: roundId, gameId: gameId, - broadcastTitle: broadcastTitle, - roundTitle: roundTitle, + tournamentSlug: tournamentSlug, + roundSlug: roundSlug, ), children: [_OpeningExplorerTab(roundId, gameId), BroadcastGameTreeView(roundId, gameId)], ); @@ -309,8 +334,14 @@ class _BroadcastBoardState extends ConsumerState<_BroadcastBoard> { enum _PlayerWidgetPosition { bottom, top } class _PlayerWidget extends ConsumerWidget { - const _PlayerWidget({required this.roundId, required this.gameId, required this.widgetPosition}); + const _PlayerWidget({ + required this.tournamentId, + required this.roundId, + required this.gameId, + required this.widgetPosition, + }); + final BroadcastTournamentId tournamentId; final BroadcastRoundId roundId; final BroadcastGameId gameId; final _PlayerWidgetPosition widgetPosition; @@ -319,14 +350,8 @@ class _PlayerWidget extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final broadcastGameState = ref.watch(broadcastGameControllerProvider(roundId, gameId)).requireValue; - // TODO - // we'll probably want to remove this and get the game state from a single controller - // this won't work with deep links for instance - final game = ref.watch( - broadcastRoundControllerProvider( - roundId, - ).select((round) => round.requireValue.games[gameId]!), - ); + final game = ref.watch(broadcastRoundGameProvider(roundId, gameId)).requireValue; + final isCursorOnLiveMove = broadcastGameState.currentPath == broadcastGameState.broadcastLivePath; final sideToMove = broadcastGameState.position.turn; @@ -334,6 +359,7 @@ class _PlayerWidget extends ConsumerWidget { _PlayerWidgetPosition.bottom => broadcastGameState.pov, _PlayerWidgetPosition.top => broadcastGameState.pov.opposite, }; + final player = game.players[side]!; final liveClock = isCursorOnLiveMove ? player.clock : null; final gameStatus = game.status; @@ -341,73 +367,87 @@ class _PlayerWidget extends ConsumerWidget { final pastClocks = broadcastGameState.clocks; final pastClock = (sideToMove == side) ? pastClocks?.parentClock : pastClocks?.clock; - return Container( - color: - Theme.of(context).platform == TargetPlatform.iOS - ? Styles.cupertinoCardColor.resolveFrom(context) - : Theme.of(context).colorScheme.surfaceContainer, - padding: const EdgeInsets.only(left: 8.0), - child: Row( - children: [ - if (game.isOver) ...[ - Text( - (gameStatus == BroadcastResult.draw) - ? '½' - : (gameStatus == BroadcastResult.whiteWins) - ? side == Side.white - ? '1' - : '0' - : side == Side.black - ? '1' - : '0', - style: const TextStyle().copyWith(fontWeight: FontWeight.bold), - ), - const SizedBox(width: 16.0), - ], - Expanded( - child: BroadcastPlayerWidget( - federation: player.federation, - title: player.title, - name: player.name, - rating: player.rating, - textStyle: const TextStyle().copyWith(fontWeight: FontWeight.bold), + return GestureDetector( + onTap: () { + pushPlatformRoute( + context, + builder: + (context) => BroadcastPlayerResultsScreen( + tournamentId, + (player.fideId != null) ? player.fideId!.toString() : player.name, + player.title, + player.name, + ), + ); + }, + child: Container( + color: + Theme.of(context).platform == TargetPlatform.iOS + ? Styles.cupertinoCardColor.resolveFrom(context) + : Theme.of(context).colorScheme.surfaceContainer, + padding: const EdgeInsets.only(left: 8.0), + child: Row( + children: [ + if (game.isOver) ...[ + Text( + (gameStatus == BroadcastResult.draw) + ? '½' + : (gameStatus == BroadcastResult.whiteWins) + ? side == Side.white + ? '1' + : '0' + : side == Side.black + ? '1' + : '0', + style: const TextStyle().copyWith(fontWeight: FontWeight.bold), + ), + const SizedBox(width: 16.0), + ], + Expanded( + child: BroadcastPlayerWidget( + federation: player.federation, + title: player.title, + name: player.name, + rating: player.rating, + textStyle: const TextStyle().copyWith(fontWeight: FontWeight.bold), + ), ), - ), - if (liveClock != null || pastClock != null) - Container( - height: kAnalysisBoardHeaderOrFooterHeight, - color: - (side == sideToMove) - ? isCursorOnLiveMove - ? Theme.of(context).colorScheme.tertiaryContainer - : Theme.of(context).colorScheme.secondaryContainer - : Colors.transparent, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 6.0), - child: Center( - child: - liveClock != null - ? CountdownClockBuilder( - timeLeft: liveClock, - active: side == sideToMove, - builder: - (context, timeLeft) => _Clock( - timeLeft: timeLeft, - isSideToMove: side == sideToMove, - isLive: true, - ), - tickInterval: const Duration(seconds: 1), - clockUpdatedAt: game.updatedClockAt, - ) - : _Clock( - timeLeft: pastClock!, - isSideToMove: side == sideToMove, - isLive: false, - ), + if (liveClock != null || pastClock != null) + Container( + height: kAnalysisBoardHeaderOrFooterHeight, + color: + (side == sideToMove) + ? isCursorOnLiveMove + ? Theme.of(context).colorScheme.tertiaryContainer + : Theme.of(context).colorScheme.secondaryContainer + : Colors.transparent, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 6.0), + child: Center( + child: + liveClock != null + ? CountdownClockBuilder( + timeLeft: liveClock, + active: side == sideToMove, + builder: + (context, timeLeft) => _Clock( + timeLeft: timeLeft, + isSideToMove: side == sideToMove, + isLive: true, + ), + tickInterval: const Duration(seconds: 1), + clockUpdatedAt: game.updatedClockAt, + ) + : _Clock( + timeLeft: pastClock!, + isSideToMove: side == sideToMove, + isLive: false, + ), + ), ), ), - ), - ], + ], + ), ), ); } diff --git a/lib/src/view/broadcast/broadcast_game_screen_providers.dart b/lib/src/view/broadcast/broadcast_game_screen_providers.dart new file mode 100644 index 0000000000..288733f47f --- /dev/null +++ b/lib/src/view/broadcast/broadcast_game_screen_providers.dart @@ -0,0 +1,25 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:lichess_mobile/src/model/broadcast/broadcast.dart'; +import 'package:lichess_mobile/src/model/broadcast/broadcast_round_controller.dart'; +import 'package:lichess_mobile/src/model/common/id.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'broadcast_game_screen_providers.g.dart'; + +@riverpod +Future<BroadcastGame> broadcastRoundGame( + Ref ref, + BroadcastRoundId roundId, + BroadcastGameId gameId, +) { + return ref.watch( + broadcastRoundControllerProvider(roundId).selectAsync((round) => round.games[gameId]!), + ); +} + +@riverpod +Future<String> broadcastGameScreenTitle(Ref ref, BroadcastRoundId roundId) { + return ref.watch( + broadcastRoundControllerProvider(roundId).selectAsync((round) => round.round.name), + ); +} diff --git a/lib/src/view/broadcast/broadcast_list_screen.dart b/lib/src/view/broadcast/broadcast_list_screen.dart index 81d09d934b..98aee4235c 100644 --- a/lib/src/view/broadcast/broadcast_list_screen.dart +++ b/lib/src/view/broadcast/broadcast_list_screen.dart @@ -266,6 +266,7 @@ class BroadcastCard extends StatefulWidget { tour: BroadcastTournamentData( id: BroadcastTournamentId(''), name: '', + slug: '', imageUrl: null, description: '', information: ( @@ -280,6 +281,7 @@ class BroadcastCard extends StatefulWidget { round: BroadcastRound( id: BroadcastRoundId(''), name: '', + slug: '', status: RoundStatus.finished, startsAt: null, finishedAt: null, diff --git a/lib/src/view/broadcast/broadcast_player_results_screen.dart b/lib/src/view/broadcast/broadcast_player_results_screen.dart new file mode 100644 index 0000000000..2b1d14527b --- /dev/null +++ b/lib/src/view/broadcast/broadcast_player_results_screen.dart @@ -0,0 +1,308 @@ +import 'dart:math'; + +import 'package:dartchess/dartchess.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:lichess_mobile/src/model/broadcast/broadcast.dart'; +import 'package:lichess_mobile/src/model/broadcast/broadcast_federation.dart'; +import 'package:lichess_mobile/src/model/broadcast/broadcast_providers.dart'; +import 'package:lichess_mobile/src/model/common/id.dart'; +import 'package:lichess_mobile/src/styles/styles.dart'; +import 'package:lichess_mobile/src/utils/l10n_context.dart'; +import 'package:lichess_mobile/src/utils/navigation.dart'; +import 'package:lichess_mobile/src/view/broadcast/broadcast_game_screen.dart'; +import 'package:lichess_mobile/src/view/broadcast/broadcast_player_widget.dart'; +import 'package:lichess_mobile/src/widgets/platform_scaffold.dart'; +import 'package:lichess_mobile/src/widgets/progression_widget.dart'; +import 'package:lichess_mobile/src/widgets/stat_card.dart'; + +class BroadcastPlayerResultsScreen extends StatelessWidget { + final BroadcastTournamentId tournamentId; + final String playerId; + final String? playerTitle; + final String playerName; + + const BroadcastPlayerResultsScreen( + this.tournamentId, + this.playerId, + this.playerTitle, + this.playerName, + ); + + @override + Widget build(BuildContext context) { + return PlatformScaffold( + appBar: PlatformAppBar(title: BroadcastPlayerWidget(title: playerTitle, name: playerName)), + body: _Body(tournamentId, playerId), + ); + } +} + +const _kTableRowPadding = EdgeInsets.symmetric(vertical: 12.0); + +class _Body extends ConsumerWidget { + final BroadcastTournamentId tournamentId; + final String playerId; + + const _Body(this.tournamentId, this.playerId); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final playersResults = ref.watch(broadcastPlayerResultProvider(tournamentId, playerId)); + + switch (playersResults) { + case AsyncData(value: final playerResults): + final player = playerResults.player; + final fideData = playerResults.fideData; + final showRatingDiff = playerResults.games.any((result) => result.ratingDiff != null); + final statWidth = + (MediaQuery.sizeOf(context).width - Styles.bodyPadding.horizontal - 10 * 2) / 3; + const cardSpacing = 10.0; + final indexWidth = max(8.0 + playerResults.games.length.toString().length * 10.0, 28.0); + + return ListView.builder( + itemCount: playerResults.games.length + 1, + itemBuilder: (context, index) { + if (index == 0) { + return Padding( + padding: Styles.bodyPadding, + child: Column( + spacing: cardSpacing, + children: [ + if (fideData.ratings.standard != null && + fideData.ratings.rapid != null && + fideData.ratings.blitz != null) + Row( + mainAxisAlignment: MainAxisAlignment.center, + spacing: cardSpacing, + children: [ + if (fideData.ratings.standard != null) + SizedBox( + width: statWidth, + child: StatCard( + context.l10n.classical, + value: fideData.ratings.standard.toString(), + ), + ), + if (fideData.ratings.rapid != null) + SizedBox( + width: statWidth, + child: StatCard( + context.l10n.rapid, + value: fideData.ratings.rapid.toString(), + ), + ), + if (fideData.ratings.blitz != null) + SizedBox( + width: statWidth, + child: StatCard( + context.l10n.blitz, + value: fideData.ratings.blitz.toString(), + ), + ), + ], + ), + if (fideData.birthYear != null && + player.federation != null && + player.fideId != null) + Row( + mainAxisAlignment: MainAxisAlignment.center, + spacing: cardSpacing, + children: [ + if (fideData.birthYear != null) + SizedBox( + width: statWidth, + child: StatCard( + context.l10n.broadcastAgeThisYear, + value: (DateTime.now().year - fideData.birthYear!).toString(), + ), + ), + if (player.federation != null) + SizedBox( + width: statWidth, + child: StatCard( + context.l10n.broadcastFederation, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Image.asset( + 'assets/images/fide-fed/${player.federation}.png', + height: 12, + ), + const SizedBox(width: 5), + Flexible( + child: Text( + federationIdToName[player.federation!]!, + style: const TextStyle(fontSize: 18.0), + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + ), + ), + if (player.fideId != null) + SizedBox( + width: statWidth, + child: StatCard('FIDE ID', value: player.fideId!.toString()), + ), + ], + ), + Row( + mainAxisAlignment: MainAxisAlignment.center, + spacing: cardSpacing, + children: [ + if (player.score != null) + SizedBox( + width: statWidth, + child: StatCard( + context.l10n.broadcastScore, + value: + '${player.score!.toStringAsFixed((player.score! == player.score!.roundToDouble()) ? 0 : 1)} / ${player.played}', + ), + ), + if (player.performance != null) + SizedBox( + width: statWidth, + child: StatCard( + context.l10n.performance, + value: player.performance.toString(), + ), + ), + if (player.ratingDiff != null) + SizedBox( + width: statWidth, + child: StatCard( + context.l10n.broadcastRatingDiff, + child: ProgressionWidget(player.ratingDiff!, fontSize: 18.0), + ), + ), + ], + ), + ], + ), + ); + } + + final playerResult = playerResults.games[index - 1]; + + return GestureDetector( + onTap: () { + pushPlatformRoute( + context, + builder: + (context) => BroadcastGameScreen( + tournamentId: tournamentId, + roundId: playerResult.roundId, + gameId: playerResult.gameId, + ), + ); + }, + child: ColoredBox( + color: + Theme.of(context).platform == TargetPlatform.iOS + ? index.isEven + ? CupertinoColors.secondarySystemBackground.resolveFrom(context) + : CupertinoColors.tertiarySystemBackground.resolveFrom(context) + : index.isEven + ? Theme.of(context).colorScheme.surfaceContainerLow + : Theme.of(context).colorScheme.surfaceContainerHigh, + child: Padding( + padding: _kTableRowPadding, + child: Row( + children: [ + SizedBox( + width: indexWidth, + child: Center( + child: Text( + index.toString(), + style: const TextStyle(fontWeight: FontWeight.bold), + ), + ), + ), + Expanded( + flex: 5, + child: BroadcastPlayerWidget( + federation: playerResult.opponent.federation, + title: playerResult.opponent.title, + name: playerResult.opponent.name, + ), + ), + Expanded( + flex: 3, + child: + (playerResult.opponent.rating != null) + ? Center(child: Text(playerResult.opponent.rating.toString())) + : const SizedBox.shrink(), + ), + SizedBox( + width: 30, + child: Center( + child: Container( + width: 15, + height: 15, + decoration: BoxDecoration( + border: + (Theme.of(context).brightness == Brightness.light && + playerResult.color == Side.white || + Theme.of(context).brightness == Brightness.dark && + playerResult.color == Side.black) + ? Border.all( + width: 2.0, + color: Theme.of(context).colorScheme.outline, + ) + : null, + shape: BoxShape.circle, + color: switch (playerResult.color) { + Side.white => Colors.white.withValues(alpha: 0.9), + Side.black => Colors.black.withValues(alpha: 0.9), + }, + ), + ), + ), + ), + SizedBox( + width: 30, + child: Center( + child: Text( + switch (playerResult.points) { + BroadcastPoints.one => '1', + BroadcastPoints.half => '½', + BroadcastPoints.zero => '0', + _ => '*', + }, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: switch (playerResult.points) { + BroadcastPoints.one => context.lichessColors.good, + BroadcastPoints.zero => context.lichessColors.error, + _ => null, + }, + ), + ), + ), + ), + if (showRatingDiff) + SizedBox( + width: 38, + child: + (playerResult.ratingDiff != null) + ? ProgressionWidget(playerResult.ratingDiff!, fontSize: 14) + : null, + ), + ], + ), + ), + ), + ); + }, + ); + case AsyncError(:final error): + return Center(child: Text('Cannot load player data: $error')); + case _: + return const Center(child: CircularProgressIndicator.adaptive()); + } + } +} diff --git a/lib/src/view/broadcast/broadcast_player_widget.dart b/lib/src/view/broadcast/broadcast_player_widget.dart index 8e73f17394..e46774cbab 100644 --- a/lib/src/view/broadcast/broadcast_player_widget.dart +++ b/lib/src/view/broadcast/broadcast_player_widget.dart @@ -4,7 +4,7 @@ import 'package:lichess_mobile/src/styles/styles.dart'; class BroadcastPlayerWidget extends ConsumerWidget { const BroadcastPlayerWidget({ - required this.federation, + this.federation, required this.title, required this.name, this.rating, @@ -28,8 +28,8 @@ class BroadcastPlayerWidget extends ConsumerWidget { if (title != null) ...[ Text( title!, - style: const TextStyle().copyWith( - color: context.lichessColors.brag, + style: TextStyle( + color: (title == 'BOT') ? context.lichessColors.fancy : context.lichessColors.brag, fontWeight: FontWeight.bold, ), ), @@ -38,7 +38,7 @@ class BroadcastPlayerWidget extends ConsumerWidget { Flexible(child: Text(name, style: textStyle, overflow: TextOverflow.ellipsis)), if (rating != null) ...[ const SizedBox(width: 5), - Text(rating.toString(), style: const TextStyle(), overflow: TextOverflow.ellipsis), + Text(rating.toString(), overflow: TextOverflow.ellipsis), ], ], ); diff --git a/lib/src/view/broadcast/broadcast_players_tab.dart b/lib/src/view/broadcast/broadcast_players_tab.dart index 985e510b01..713766979e 100644 --- a/lib/src/view/broadcast/broadcast_players_tab.dart +++ b/lib/src/view/broadcast/broadcast_players_tab.dart @@ -9,6 +9,8 @@ import 'package:lichess_mobile/src/model/broadcast/broadcast_providers.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; +import 'package:lichess_mobile/src/utils/navigation.dart'; +import 'package:lichess_mobile/src/view/broadcast/broadcast_player_results_screen.dart'; import 'package:lichess_mobile/src/view/broadcast/broadcast_player_widget.dart'; import 'package:lichess_mobile/src/widgets/progression_widget.dart'; @@ -29,7 +31,7 @@ class BroadcastPlayersTab extends ConsumerWidget { final players = ref.watch(broadcastPlayersProvider(tournamentId)); return switch (players) { - AsyncData(value: final players) => PlayersList(players), + AsyncData(value: final players) => PlayersList(players, tournamentId), AsyncError(:final error) => SliverPadding( padding: edgeInsets, sliver: SliverFillRemaining(child: Center(child: Text('Cannot load players data: $error'))), @@ -50,9 +52,10 @@ const _kTableRowPadding = EdgeInsets.symmetric( const _kHeaderTextStyle = TextStyle(fontWeight: FontWeight.bold, overflow: TextOverflow.ellipsis); class PlayersList extends ConsumerStatefulWidget { - const PlayersList(this.players); + const PlayersList(this.players, this.tournamentId); final IList<BroadcastPlayerExtended> players; + final BroadcastTournamentId tournamentId; @override ConsumerState<PlayersList> createState() => _PlayersListState(); @@ -75,7 +78,13 @@ class _PlayersListState extends ConsumerState<PlayersList> { _SortingTypes.score => (BroadcastPlayerExtended a, BroadcastPlayerExtended b) { if (a.score == null) return 1; if (b.score == null) return -1; - return b.score!.compareTo(a.score!); + + final value = b.score!.compareTo(a.score!); + if (value == 0) { + return a.played.compareTo(b.played); + } else { + return value; + } }, }; @@ -109,7 +118,7 @@ class _PlayersListState extends ConsumerState<PlayersList> { @override Widget build(BuildContext context) { final double eloWidth = max(MediaQuery.sizeOf(context).width * 0.2, 100); - final double scoreWidth = max(MediaQuery.sizeOf(context).width * 0.15, 70); + final double scoreWidth = max(MediaQuery.sizeOf(context).width * 0.15, 90); return SliverList.builder( itemCount: players.length + 1, @@ -164,8 +173,21 @@ class _PlayersListState extends ConsumerState<PlayersList> { ); } else { final player = players[index - 1]; - return Container( - decoration: BoxDecoration( + + return GestureDetector( + onTap: () { + pushPlatformRoute( + context, + builder: + (context) => BroadcastPlayerResultsScreen( + widget.tournamentId, + player.fideId != null ? player.fideId.toString() : player.name, + player.title, + player.name, + ), + ); + }, + child: ColoredBox( color: Theme.of(context).platform == TargetPlatform.iOS ? index.isEven @@ -174,49 +196,52 @@ class _PlayersListState extends ConsumerState<PlayersList> { : index.isEven ? Theme.of(context).colorScheme.surfaceContainerLow : Theme.of(context).colorScheme.surfaceContainerHigh, - ), - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Expanded( - child: Padding( - padding: _kTableRowPadding, - child: BroadcastPlayerWidget( - federation: player.federation, - title: player.title, - name: player.name, + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Expanded( + child: Padding( + padding: _kTableRowPadding, + child: BroadcastPlayerWidget( + federation: player.federation, + title: player.title, + name: player.name, + ), ), ), - ), - SizedBox( - width: eloWidth, - child: Padding( - padding: _kTableRowPadding, - child: Row( - children: [ - if (player.rating != null) ...[ - Text(player.rating.toString()), - const SizedBox(width: 5), - if (player.ratingDiff != null) - ProgressionWidget(player.ratingDiff!, fontSize: 14), + SizedBox( + width: eloWidth, + child: Padding( + padding: _kTableRowPadding, + child: Row( + children: [ + if (player.rating != null) ...[ + Text(player.rating.toString()), + const SizedBox(width: 5), + if (player.ratingDiff != null) + ProgressionWidget(player.ratingDiff!, fontSize: 14), + ], ], - ], + ), ), ), - ), - SizedBox( - width: scoreWidth, - child: Padding( - padding: _kTableRowPadding, - child: - (player.score != null) - ? Text( - '${player.score!.toStringAsFixed((player.score! == player.score!.roundToDouble()) ? 0 : 1)}/${player.played}', - ) - : const SizedBox.shrink(), + SizedBox( + width: scoreWidth, + child: Padding( + padding: _kTableRowPadding, + child: + (player.score != null) + ? Align( + alignment: Alignment.centerRight, + child: Text( + '${player.score!.toStringAsFixed((player.score! == player.score!.roundToDouble()) ? 0 : 1)} / ${player.played}', + ), + ) + : null, + ), ), - ), - ], + ], + ), ), ); } diff --git a/lib/src/view/broadcast/broadcast_round_screen.dart b/lib/src/view/broadcast/broadcast_round_screen.dart index bd042a96f6..cafba27587 100644 --- a/lib/src/view/broadcast/broadcast_round_screen.dart +++ b/lib/src/view/broadcast/broadcast_round_screen.dart @@ -117,8 +117,9 @@ class _BroadcastRoundScreenState extends ConsumerState<BroadcastRoundScreen> cupertinoTabSwitcher: tabSwitcher, sliver: switch (asyncTournament) { AsyncData(:final value) => BroadcastBoardsTab( + tournamentId: _selectedTournamentId, roundId: _selectedRoundId ?? value.defaultRoundId, - broadcastTitle: widget.broadcast.title, + tournamentSlug: widget.broadcast.tour.slug, ), _ => const SliverFillRemaining(child: SizedBox.shrink()), }, @@ -180,8 +181,9 @@ class _BroadcastRoundScreenState extends ConsumerState<BroadcastRoundScreen> _TabView( sliver: switch (asyncTournament) { AsyncData(:final value) => BroadcastBoardsTab( + tournamentId: _selectedTournamentId, roundId: _selectedRoundId ?? value.defaultRoundId, - broadcastTitle: widget.broadcast.title, + tournamentSlug: widget.broadcast.tour.slug, ), _ => const SliverFillRemaining(child: SizedBox.shrink()), },