From 1e8253b90fda428f96efe43e1ddfe00f07b00c45 Mon Sep 17 00:00:00 2001 From: Julien <120588494+julien4215@users.noreply.github.com> Date: Mon, 9 Dec 2024 10:55:14 +0100 Subject: [PATCH 01/16] Add player results screen --- lib/src/model/broadcast/broadcast.dart | 31 ++ .../model/broadcast/broadcast_federation.dart | 205 ++++++++++ .../model/broadcast/broadcast_providers.dart | 12 + .../model/broadcast/broadcast_repository.dart | 57 ++- lib/src/model/common/id.dart | 10 +- .../view/broadcast/broadcast_boards_tab.dart | 38 +- .../broadcast/broadcast_game_bottom_bar.dart | 33 +- .../view/broadcast/broadcast_game_screen.dart | 26 +- .../view/broadcast/broadcast_list_screen.dart | 1 + .../broadcast_player_results_screen.dart | 351 ++++++++++++++++++ .../broadcast/broadcast_player_widget.dart | 9 +- .../view/broadcast/broadcast_players_tab.dart | 106 +++--- .../broadcast/broadcast_round_screen.dart | 2 - 13 files changed, 778 insertions(+), 103 deletions(-) create mode 100644 lib/src/model/broadcast/broadcast_federation.dart create mode 100644 lib/src/view/broadcast/broadcast_player_results_screen.dart diff --git a/lib/src/model/broadcast/broadcast.dart b/lib/src/model/broadcast/broadcast.dart index fff18f2d78..a1ad6be868 100644 --- a/lib/src/model/broadcast/broadcast.dart +++ b/lib/src/model/broadcast/broadcast.dart @@ -82,6 +82,7 @@ class BroadcastRound with _$BroadcastRound { required DateTime? startsAt, required DateTime? finishedAt, required bool startsAfterPrevious, + required String? url, }) = _BroadcastRound; } @@ -136,9 +137,39 @@ 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 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, diff --git a/lib/src/model/broadcast/broadcast_federation.dart b/lib/src/model/broadcast/broadcast_federation.dart new file mode 100644 index 0000000000..1deb73bb2e --- /dev/null +++ b/lib/src/model/broadcast/broadcast_federation.dart @@ -0,0 +1,205 @@ +const fedIdToName = { + '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 a7e02edc8d..f06c422cc8 100644 --- a/lib/src/model/broadcast/broadcast_providers.dart +++ b/lib/src/model/broadcast/broadcast_providers.dart @@ -65,6 +65,18 @@ Future> broadcastPlayers( ); } +@riverpod +Future 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 67dec06f47..c5f943d719 100644 --- a/lib/src/model/broadcast/broadcast_repository.dart +++ b/lib/src/model/broadcast/broadcast_repository.dart @@ -57,6 +57,16 @@ class BroadcastRepository { mapper: _makePlayerFromJson, ); } + + Future getPlayerResults( + BroadcastTournamentId tournamentId, + String playerId, + ) { + return client.readJson( + Uri(path: 'broadcast/$tournamentId/players/$playerId'), + mapper: _makePlayerResultsFromJson, + ); + } } BroadcastList _makeBroadcastResponseFromJson( @@ -79,7 +89,7 @@ Broadcast _broadcastFromPick(RequiredPick pick) { round: _roundFromPick(pick('round').required()), group: pick('group').asStringOrNull(), roundToLinkId: - pick('roundToLink', 'id').asBroadcastRoundIddOrNull() ?? roundId, + pick('roundToLink', 'id').asBroadcastRoundIdOrNull() ?? roundId, ); } @@ -143,6 +153,7 @@ BroadcastRound _roundFromPick(RequiredPick pick) { startsAt: pick('startsAt').asDateTimeFromMillisecondsOrNull(), finishedAt: pick('finishedAt').asDateTimeFromMillisecondsOrNull(), startsAfterPrevious: pick('startsAfterPrevious').asBoolOrFalse(), + url: pick('url').asStringOrNull(), ); } @@ -219,5 +230,49 @@ BroadcastPlayerExtended _playerExtendedFromPick(RequiredPick pick) { played: pick('played').asIntOrThrow(), score: pick('score').asDoubleOrNull(), ratingDiff: pick('ratingDiff').asIntOrNull(), + performance: pick('performance').asIntOrNull(), + ); +} + +BroadcastPlayerResults _makePlayerResultsFromJson( + Map 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()), ); } diff --git a/lib/src/model/common/id.dart b/lib/src/model/common/id.dart index b41a4e82a6..6f0a3729d6 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() { @@ -192,7 +194,7 @@ extension IDPick on Pick { ); } - BroadcastRoundId? asBroadcastRoundIddOrNull() { + BroadcastRoundId? asBroadcastRoundIdOrNull() { if (value == null) return null; try { return asBroadcastRoundIdOrThrow(); @@ -211,7 +213,7 @@ extension IDPick on Pick { ); } - BroadcastGameId? asBroadcastGameIddOrNull() { + BroadcastGameId? asBroadcastGameIdOrNull() { if (value == null) return null; try { return asBroadcastGameIdOrThrow(); @@ -232,7 +234,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( diff --git a/lib/src/view/broadcast/broadcast_boards_tab.dart b/lib/src/view/broadcast/broadcast_boards_tab.dart index 269f03d455..fa1b3dde91 100644 --- a/lib/src/view/broadcast/broadcast_boards_tab.dart +++ b/lib/src/view/broadcast/broadcast_boards_tab.dart @@ -26,11 +26,9 @@ const _kPlayerWidgetPadding = EdgeInsets.symmetric(vertical: 5.0); class BroadcastBoardsTab extends ConsumerWidget { const BroadcastBoardsTab({ required this.roundId, - required this.broadcastTitle, }); final BroadcastRoundId roundId; - final String broadcastTitle; @override Widget build(BuildContext context, WidgetRef ref) { @@ -60,19 +58,15 @@ class BroadcastBoardsTab extends ConsumerWidget { : BroadcastPreview( games: value.games.values.toIList(), roundId: roundId, - broadcastTitle: broadcastTitle, - roundTitle: value.round.name, + title: value.round.name, + roundUrl: value.round.url, ), AsyncError(:final error) => SliverFillRemaining( child: Center( child: Text('Could not load broadcast: $error'), ), ), - _ => const SliverFillRemaining( - child: Center( - child: CircularProgressIndicator.adaptive(), - ), - ), + _ => const BroadcastPreview.loading() }, ); } @@ -82,20 +76,20 @@ class BroadcastPreview extends StatelessWidget { const BroadcastPreview({ required this.roundId, required this.games, - required this.broadcastTitle, - required this.roundTitle, + required this.title, + required this.roundUrl, }); - const BroadcastPreview.loading({ - required this.roundId, - required this.broadcastTitle, - }) : games = null, - roundTitle = null; + const BroadcastPreview.loading() + : roundId = const BroadcastRoundId(''), + games = null, + title = '', + roundUrl = null; final BroadcastRoundId roundId; final IList? games; - final String broadcastTitle; - final String? roundTitle; + final String title; + final String? roundUrl; @override Widget build(BuildContext context) { @@ -123,7 +117,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( @@ -142,12 +136,12 @@ class BroadcastPreview extends StatelessWidget { onTap: () { pushPlatformRoute( context, - title: roundTitle, + title: title, builder: (context) => BroadcastGameScreen( roundId: roundId, gameId: game.id, - broadcastTitle: broadcastTitle, - roundTitle: roundTitle!, + roundUrl: roundUrl, + 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 a2c7a5b804..d2973ec3d9 100644 --- a/lib/src/view/broadcast/broadcast_game_bottom_bar.dart +++ b/lib/src/view/broadcast/broadcast_game_bottom_bar.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/model/broadcast/broadcast_game_controller.dart'; import 'package:lichess_mobile/src/model/broadcast/broadcast_repository.dart'; +import 'package:lichess_mobile/src/model/broadcast/broadcast_round_controller.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/game/game_share_service.dart'; import 'package:lichess_mobile/src/network/http.dart'; @@ -18,19 +19,19 @@ class BroadcastGameBottomBar extends ConsumerWidget { const BroadcastGameBottomBar({ required this.roundId, required this.gameId, - required this.broadcastTitle, - required this.roundTitle, + this.roundUrl, }); final BroadcastRoundId roundId; final BroadcastGameId gameId; - final String broadcastTitle; - final String roundTitle; + final String? roundUrl; @override Widget build(BuildContext context, WidgetRef ref) { final ctrlProvider = broadcastGameControllerProvider(roundId, gameId); final broadcastGameState = ref.watch(ctrlProvider).requireValue; + final broadcastRoundState = + ref.watch(broadcastRoundControllerProvider(roundId)); return BottomBar( children: [ @@ -40,17 +41,19 @@ class BroadcastGameBottomBar extends ConsumerWidget { showAdaptiveActionSheet( 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 (roundUrl != null || broadcastRoundState.hasValue) + BottomSheetAction( + makeLabel: (context) => + Text(context.l10n.mobileShareGameURL), + onPressed: (context) async { + launchShareDialog( + context, + uri: Uri.parse( + '${roundUrl ?? broadcastRoundState.requireValue.round.url}/$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 55a01f0352..68f860f08e 100644 --- a/lib/src/view/broadcast/broadcast_game_screen.dart +++ b/lib/src/view/broadcast/broadcast_game_screen.dart @@ -33,14 +33,14 @@ import 'package:lichess_mobile/src/widgets/platform_scaffold.dart'; class BroadcastGameScreen extends ConsumerStatefulWidget { final BroadcastRoundId roundId; final BroadcastGameId gameId; - final String broadcastTitle; - final String roundTitle; + final String? roundUrl; + final String? title; const BroadcastGameScreen({ required this.roundId, required this.gameId, - required this.broadcastTitle, - required this.roundTitle, + this.roundUrl, + this.title, }); @override @@ -79,11 +79,15 @@ class _BroadcastGameScreenState extends ConsumerState Widget build(BuildContext context) { final broadcastGameState = ref .watch(broadcastGameControllerProvider(widget.roundId, widget.gameId)); + final broadcastRoundState = + ref.watch(broadcastRoundControllerProvider(widget.roundId)); return PlatformScaffold( appBar: PlatformAppBar( title: Text( - widget.roundTitle, + widget.title ?? + broadcastRoundState.value?.round.name ?? + 'BroadcastGame', overflow: TextOverflow.ellipsis, maxLines: 1, ), @@ -113,8 +117,7 @@ class _BroadcastGameScreenState extends ConsumerState AsyncData() => _Body( widget.roundId, widget.gameId, - widget.broadcastTitle, - widget.roundTitle, + widget.roundUrl, tabController: _tabController, ), AsyncError(:final error) => Center( @@ -130,15 +133,13 @@ class _Body extends ConsumerWidget { const _Body( this.roundId, this.gameId, - this.broadcastTitle, - this.roundTitle, { + this.roundUrl, { required this.tabController, }); final BroadcastRoundId roundId; final BroadcastGameId gameId; - final String broadcastTitle; - final String roundTitle; + final String? roundUrl; final TabController tabController; @override @@ -205,8 +206,7 @@ class _Body extends ConsumerWidget { bottomBar: BroadcastGameBottomBar( roundId: roundId, gameId: gameId, - broadcastTitle: broadcastTitle, - roundTitle: roundTitle, + roundUrl: roundUrl, ), children: [ _OpeningExplorerTab(roundId, gameId), diff --git a/lib/src/view/broadcast/broadcast_list_screen.dart b/lib/src/view/broadcast/broadcast_list_screen.dart index dd589f6dc4..0274768015 100644 --- a/lib/src/view/broadcast/broadcast_list_screen.dart +++ b/lib/src/view/broadcast/broadcast_list_screen.dart @@ -238,6 +238,7 @@ class BroadcastGridItem extends StatefulWidget { startsAt: null, finishedAt: null, startsAfterPrevious: false, + url: null, ), group: null, roundToLinkId: BroadcastRoundId(''), 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..cb53bc66ef --- /dev/null +++ b/lib/src/view/broadcast/broadcast_player_results_screen.dart @@ -0,0 +1,351 @@ +import 'dart:math'; + +import 'package:dartchess/dartchess.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_svg/svg.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/network/http.dart'; +import 'package:lichess_mobile/src/styles/styles.dart'; +import 'package:lichess_mobile/src/utils/l10n_context.dart'; +import 'package:lichess_mobile/src/utils/lichess_assets.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 Column( + children: [ + Expanded( + child: 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: [ + SvgPicture.network( + lichessFideFedSrc( + player.federation!, + ), + height: 12, + httpClient: + ref.read(defaultClientProvider), + ), + const SizedBox(width: 5), + Flexible( + child: Text( + fedIdToName[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: [ + 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( + roundId: playerResult.roundId, + gameId: playerResult.gameId, + ), + ); + }, + child: ColoredBox( + color: (index - 1).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 598cc62f46..dd25b72236 100644 --- a/lib/src/view/broadcast/broadcast_player_widget.dart +++ b/lib/src/view/broadcast/broadcast_player_widget.dart @@ -7,7 +7,7 @@ import 'package:lichess_mobile/src/utils/lichess_assets.dart'; class BroadcastPlayerWidget extends ConsumerWidget { const BroadcastPlayerWidget({ - required this.federation, + this.federation, required this.title, required this.name, this.rating, @@ -35,8 +35,10 @@ class BroadcastPlayerWidget extends ConsumerWidget { if (title != null) ...[ Text( title!, - style: const TextStyle().copyWith( - color: context.lichessColors.brag, + style: TextStyle( + color: (title == 'BOT') + ? context.lichessColors.purple + : context.lichessColors.brag, fontWeight: FontWeight.bold, ), ), @@ -53,7 +55,6 @@ class BroadcastPlayerWidget extends ConsumerWidget { const SizedBox(width: 5), Text( rating.toString(), - style: const TextStyle(), overflow: TextOverflow.ellipsis, ), ], diff --git a/lib/src/view/broadcast/broadcast_players_tab.dart b/lib/src/view/broadcast/broadcast_players_tab.dart index 9088af4e2d..c3a46bf5f9 100644 --- a/lib/src/view/broadcast/broadcast_players_tab.dart +++ b/lib/src/view/broadcast/broadcast_players_tab.dart @@ -8,6 +8,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'; @@ -27,7 +29,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( @@ -55,9 +57,10 @@ 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 players; + final BroadcastTournamentId tournamentId; @override ConsumerState createState() => _PlayersListState(); @@ -117,7 +120,7 @@ class _PlayersListState extends ConsumerState { @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, @@ -177,53 +180,72 @@ class _PlayersListState extends ConsumerState { ); } else { final player = players[index - 1]; - return Container( - decoration: BoxDecoration( - color: index.isEven + 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: (index - 1).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}', + ), + ) + : const SizedBox.shrink(), + ), ), - ), - ], + ], + ), ), ); } diff --git a/lib/src/view/broadcast/broadcast_round_screen.dart b/lib/src/view/broadcast/broadcast_round_screen.dart index 814d1e0237..085e8e4e16 100644 --- a/lib/src/view/broadcast/broadcast_round_screen.dart +++ b/lib/src/view/broadcast/broadcast_round_screen.dart @@ -117,7 +117,6 @@ class _BroadcastRoundScreenState extends ConsumerState sliver: switch (asyncTournament) { AsyncData(:final value) => BroadcastBoardsTab( roundId: _selectedRoundId ?? value.defaultRoundId, - broadcastTitle: widget.broadcast.title, ), _ => const SliverFillRemaining( child: SizedBox.shrink(), @@ -182,7 +181,6 @@ class _BroadcastRoundScreenState extends ConsumerState sliver: switch (asyncTournament) { AsyncData(:final value) => BroadcastBoardsTab( roundId: _selectedRoundId ?? value.defaultRoundId, - broadcastTitle: widget.broadcast.title, ), _ => const SliverFillRemaining( child: SizedBox.shrink(), From 0af48bdc156a67f61e476c30dd592024f418384f Mon Sep 17 00:00:00 2001 From: Julien <120588494+julien4215@users.noreply.github.com> Date: Thu, 12 Dec 2024 13:02:27 +0100 Subject: [PATCH 02/16] Use slugs instead of round url to share game pgn --- lib/src/model/broadcast/broadcast.dart | 3 ++- .../model/broadcast/broadcast_repository.dart | 3 ++- .../view/broadcast/broadcast_boards_tab.dart | 17 ++++++++++++----- .../broadcast/broadcast_game_bottom_bar.dart | 15 +++++++-------- .../view/broadcast/broadcast_game_screen.dart | 18 ++++++++++++------ .../view/broadcast/broadcast_list_screen.dart | 3 ++- .../view/broadcast/broadcast_round_screen.dart | 2 ++ 7 files changed, 39 insertions(+), 22 deletions(-) diff --git a/lib/src/model/broadcast/broadcast.dart b/lib/src/model/broadcast/broadcast.dart index a1ad6be868..1e64188614 100644 --- a/lib/src/model/broadcast/broadcast.dart +++ b/lib/src/model/broadcast/broadcast.dart @@ -48,6 +48,7 @@ class BroadcastTournamentData with _$BroadcastTournamentData { const factory BroadcastTournamentData({ required BroadcastTournamentId id, required String name, + required String slug, required String? imageUrl, required String? description, required BroadcastTournamentInformation information, @@ -78,11 +79,11 @@ 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, required bool startsAfterPrevious, - required String? url, }) = _BroadcastRound; } diff --git a/lib/src/model/broadcast/broadcast_repository.dart b/lib/src/model/broadcast/broadcast_repository.dart index c5f943d719..13d2adfe60 100644 --- a/lib/src/model/broadcast/broadcast_repository.dart +++ b/lib/src/model/broadcast/broadcast_repository.dart @@ -99,6 +99,7 @@ BroadcastTournamentData _tournamentDataFromPick( BroadcastTournamentData( id: pick('id').asBroadcastTournamentIdOrThrow(), name: pick('name').asStringOrThrow(), + slug: pick('slug').asStringOrThrow(), imageUrl: pick('image').asStringOrNull(), description: pick('description').asStringOrNull(), information: ( @@ -149,11 +150,11 @@ 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(), startsAfterPrevious: pick('startsAfterPrevious').asBoolOrFalse(), - url: pick('url').asStringOrNull(), ); } diff --git a/lib/src/view/broadcast/broadcast_boards_tab.dart b/lib/src/view/broadcast/broadcast_boards_tab.dart index fa1b3dde91..adb77112c8 100644 --- a/lib/src/view/broadcast/broadcast_boards_tab.dart +++ b/lib/src/view/broadcast/broadcast_boards_tab.dart @@ -26,9 +26,11 @@ const _kPlayerWidgetPadding = EdgeInsets.symmetric(vertical: 5.0); class BroadcastBoardsTab extends ConsumerWidget { const BroadcastBoardsTab({ required this.roundId, + required this.tournamentSlug, }); final BroadcastRoundId roundId; + final String tournamentSlug; @override Widget build(BuildContext context, WidgetRef ref) { @@ -59,7 +61,8 @@ class BroadcastBoardsTab extends ConsumerWidget { games: value.games.values.toIList(), roundId: roundId, title: value.round.name, - roundUrl: value.round.url, + tournamentSlug: tournamentSlug, + roundSlug: value.round.slug, ), AsyncError(:final error) => SliverFillRemaining( child: Center( @@ -77,19 +80,22 @@ class BroadcastPreview extends StatelessWidget { required this.roundId, required this.games, required this.title, - required this.roundUrl, + required this.tournamentSlug, + required this.roundSlug, }); const BroadcastPreview.loading() : roundId = const BroadcastRoundId(''), games = null, title = '', - roundUrl = null; + tournamentSlug = '', + roundSlug = ''; final BroadcastRoundId roundId; final IList? games; final String title; - final String? roundUrl; + final String tournamentSlug; + final String roundSlug; @override Widget build(BuildContext context) { @@ -140,7 +146,8 @@ class BroadcastPreview extends StatelessWidget { builder: (context) => BroadcastGameScreen( roundId: roundId, gameId: game.id, - roundUrl: roundUrl, + 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 d2973ec3d9..e408303f91 100644 --- a/lib/src/view/broadcast/broadcast_game_bottom_bar.dart +++ b/lib/src/view/broadcast/broadcast_game_bottom_bar.dart @@ -3,7 +3,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/model/broadcast/broadcast_game_controller.dart'; import 'package:lichess_mobile/src/model/broadcast/broadcast_repository.dart'; -import 'package:lichess_mobile/src/model/broadcast/broadcast_round_controller.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/game/game_share_service.dart'; import 'package:lichess_mobile/src/network/http.dart'; @@ -19,19 +18,19 @@ class BroadcastGameBottomBar extends ConsumerWidget { const BroadcastGameBottomBar({ required this.roundId, required this.gameId, - this.roundUrl, + this.tournamentSlug, + this.roundSlug, }); final BroadcastRoundId roundId; final BroadcastGameId gameId; - final String? roundUrl; + final String? tournamentSlug; + final String? roundSlug; @override Widget build(BuildContext context, WidgetRef ref) { final ctrlProvider = broadcastGameControllerProvider(roundId, gameId); final broadcastGameState = ref.watch(ctrlProvider).requireValue; - final broadcastRoundState = - ref.watch(broadcastRoundControllerProvider(roundId)); return BottomBar( children: [ @@ -41,15 +40,15 @@ class BroadcastGameBottomBar extends ConsumerWidget { showAdaptiveActionSheet( context: context, actions: [ - if (roundUrl != null || broadcastRoundState.hasValue) + if (tournamentSlug != null && roundSlug != null) BottomSheetAction( makeLabel: (context) => Text(context.l10n.mobileShareGameURL), onPressed: (context) async { launchShareDialog( context, - uri: Uri.parse( - '${roundUrl ?? broadcastRoundState.requireValue.round.url}/$gameId', + uri: lichessUri( + '/broadcast/$tournamentSlug/$roundSlug/$roundId/$gameId', ), ); }, diff --git a/lib/src/view/broadcast/broadcast_game_screen.dart b/lib/src/view/broadcast/broadcast_game_screen.dart index 68f860f08e..ef028d43f1 100644 --- a/lib/src/view/broadcast/broadcast_game_screen.dart +++ b/lib/src/view/broadcast/broadcast_game_screen.dart @@ -33,13 +33,15 @@ import 'package:lichess_mobile/src/widgets/platform_scaffold.dart'; class BroadcastGameScreen extends ConsumerStatefulWidget { final BroadcastRoundId roundId; final BroadcastGameId gameId; - final String? roundUrl; + final String? tournamentSlug; + final String? roundSlug; final String? title; const BroadcastGameScreen({ required this.roundId, required this.gameId, - this.roundUrl, + this.tournamentSlug, + this.roundSlug, this.title, }); @@ -117,7 +119,8 @@ class _BroadcastGameScreenState extends ConsumerState AsyncData() => _Body( widget.roundId, widget.gameId, - widget.roundUrl, + widget.tournamentSlug, + widget.roundSlug, tabController: _tabController, ), AsyncError(:final error) => Center( @@ -133,13 +136,15 @@ class _Body extends ConsumerWidget { const _Body( this.roundId, this.gameId, - this.roundUrl, { + this.tournamentSlug, + this.roundSlug, { required this.tabController, }); final BroadcastRoundId roundId; final BroadcastGameId gameId; - final String? roundUrl; + final String? tournamentSlug; + final String? roundSlug; final TabController tabController; @override @@ -206,7 +211,8 @@ class _Body extends ConsumerWidget { bottomBar: BroadcastGameBottomBar( roundId: roundId, gameId: gameId, - roundUrl: roundUrl, + tournamentSlug: tournamentSlug, + roundSlug: roundSlug, ), children: [ _OpeningExplorerTab(roundId, gameId), diff --git a/lib/src/view/broadcast/broadcast_list_screen.dart b/lib/src/view/broadcast/broadcast_list_screen.dart index 0274768015..172835cefa 100644 --- a/lib/src/view/broadcast/broadcast_list_screen.dart +++ b/lib/src/view/broadcast/broadcast_list_screen.dart @@ -220,6 +220,7 @@ class BroadcastGridItem extends StatefulWidget { tour: BroadcastTournamentData( id: BroadcastTournamentId(''), name: '', + slug: '', imageUrl: null, description: '', information: ( @@ -234,11 +235,11 @@ class BroadcastGridItem extends StatefulWidget { round: BroadcastRound( id: BroadcastRoundId(''), name: '', + slug: '', status: RoundStatus.finished, startsAt: null, finishedAt: null, startsAfterPrevious: false, - url: null, ), group: null, roundToLinkId: BroadcastRoundId(''), diff --git a/lib/src/view/broadcast/broadcast_round_screen.dart b/lib/src/view/broadcast/broadcast_round_screen.dart index 085e8e4e16..26e5c6a2c8 100644 --- a/lib/src/view/broadcast/broadcast_round_screen.dart +++ b/lib/src/view/broadcast/broadcast_round_screen.dart @@ -117,6 +117,7 @@ class _BroadcastRoundScreenState extends ConsumerState sliver: switch (asyncTournament) { AsyncData(:final value) => BroadcastBoardsTab( roundId: _selectedRoundId ?? value.defaultRoundId, + tournamentSlug: widget.broadcast.tour.slug, ), _ => const SliverFillRemaining( child: SizedBox.shrink(), @@ -181,6 +182,7 @@ class _BroadcastRoundScreenState extends ConsumerState sliver: switch (asyncTournament) { AsyncData(:final value) => BroadcastBoardsTab( roundId: _selectedRoundId ?? value.defaultRoundId, + tournamentSlug: widget.broadcast.tour.slug, ), _ => const SliverFillRemaining( child: SizedBox.shrink(), From 56075dc42454133d77d1d4f2317de09ff0454077 Mon Sep 17 00:00:00 2001 From: Julien <120588494+julien4215@users.noreply.github.com> Date: Thu, 12 Dec 2024 13:03:52 +0100 Subject: [PATCH 03/16] Rename federation ID to names variable --- lib/src/model/broadcast/broadcast_federation.dart | 2 +- lib/src/view/broadcast/broadcast_player_results_screen.dart | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/src/model/broadcast/broadcast_federation.dart b/lib/src/model/broadcast/broadcast_federation.dart index 1deb73bb2e..267b53fc69 100644 --- a/lib/src/model/broadcast/broadcast_federation.dart +++ b/lib/src/model/broadcast/broadcast_federation.dart @@ -1,4 +1,4 @@ -const fedIdToName = { +const federationIdToName = { 'FID': 'FIDE', 'USA': 'United States of America', 'IND': 'India', diff --git a/lib/src/view/broadcast/broadcast_player_results_screen.dart b/lib/src/view/broadcast/broadcast_player_results_screen.dart index cb53bc66ef..10b20b40d7 100644 --- a/lib/src/view/broadcast/broadcast_player_results_screen.dart +++ b/lib/src/view/broadcast/broadcast_player_results_screen.dart @@ -157,7 +157,8 @@ class _Body extends ConsumerWidget { const SizedBox(width: 5), Flexible( child: Text( - fedIdToName[player.federation!]!, + federationIdToName[ + player.federation!]!, style: const TextStyle( fontSize: 18.0, ), From cf3a3dec184f3207662167aea3f879f828ef271e Mon Sep 17 00:00:00 2001 From: Julien <120588494+julien4215@users.noreply.github.com> Date: Thu, 12 Dec 2024 15:26:23 +0100 Subject: [PATCH 04/16] Restore list color order for broadcast players --- lib/src/view/broadcast/broadcast_player_results_screen.dart | 4 ++-- lib/src/view/broadcast/broadcast_players_tab.dart | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/src/view/broadcast/broadcast_player_results_screen.dart b/lib/src/view/broadcast/broadcast_player_results_screen.dart index b345d9cbc5..ab90cf1e46 100644 --- a/lib/src/view/broadcast/broadcast_player_results_screen.dart +++ b/lib/src/view/broadcast/broadcast_player_results_screen.dart @@ -232,12 +232,12 @@ class _Body extends ConsumerWidget { }, child: ColoredBox( color: Theme.of(context).platform == TargetPlatform.iOS - ? (index - 1).isEven + ? index.isEven ? CupertinoColors.secondarySystemBackground .resolveFrom(context) : CupertinoColors.tertiarySystemBackground .resolveFrom(context) - : (index - 1).isEven + : index.isEven ? Theme.of(context) .colorScheme .surfaceContainerLow diff --git a/lib/src/view/broadcast/broadcast_players_tab.dart b/lib/src/view/broadcast/broadcast_players_tab.dart index ed4c936411..13585c8422 100644 --- a/lib/src/view/broadcast/broadcast_players_tab.dart +++ b/lib/src/view/broadcast/broadcast_players_tab.dart @@ -197,12 +197,12 @@ class _PlayersListState extends ConsumerState { }, child: ColoredBox( color: Theme.of(context).platform == TargetPlatform.iOS - ? (index - 1).isEven + ? index.isEven ? CupertinoColors.secondarySystemBackground .resolveFrom(context) : CupertinoColors.tertiarySystemBackground .resolveFrom(context) - : (index - 1).isEven + : index.isEven ? Theme.of(context).colorScheme.surfaceContainerLow : Theme.of(context).colorScheme.surfaceContainerHigh, child: Row( From 1b2ed9925306cd195b85fdf6a565bda6e6140e14 Mon Sep 17 00:00:00 2001 From: Julien <120588494+julien4215@users.noreply.github.com> Date: Thu, 12 Dec 2024 16:15:43 +0100 Subject: [PATCH 05/16] Use circular indicator instead of board shimmer --- lib/src/view/broadcast/broadcast_boards_tab.dart | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/src/view/broadcast/broadcast_boards_tab.dart b/lib/src/view/broadcast/broadcast_boards_tab.dart index adb77112c8..e5ab9e9153 100644 --- a/lib/src/view/broadcast/broadcast_boards_tab.dart +++ b/lib/src/view/broadcast/broadcast_boards_tab.dart @@ -69,7 +69,11 @@ class BroadcastBoardsTab extends ConsumerWidget { child: Text('Could not load broadcast: $error'), ), ), - _ => const BroadcastPreview.loading() + _ => const SliverFillRemaining( + child: Center( + child: CircularProgressIndicator.adaptive(), + ), + ), }, ); } From 28a5e398ed8dc72e0b5f5b038cf12b5856461199 Mon Sep 17 00:00:00 2001 From: Julien <120588494+julien4215@users.noreply.github.com> Date: Thu, 12 Dec 2024 16:30:43 +0100 Subject: [PATCH 06/16] Listen only to the name of the round --- lib/src/view/broadcast/broadcast_game_screen.dart | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/src/view/broadcast/broadcast_game_screen.dart b/lib/src/view/broadcast/broadcast_game_screen.dart index ef028d43f1..2f8fb62a7b 100644 --- a/lib/src/view/broadcast/broadcast_game_screen.dart +++ b/lib/src/view/broadcast/broadcast_game_screen.dart @@ -81,15 +81,15 @@ class _BroadcastGameScreenState extends ConsumerState Widget build(BuildContext context) { final broadcastGameState = ref .watch(broadcastGameControllerProvider(widget.roundId, widget.gameId)); - final broadcastRoundState = - ref.watch(broadcastRoundControllerProvider(widget.roundId)); + final title = ref.watch( + broadcastRoundControllerProvider(widget.roundId) + .select((round) => round.value?.round.name), + ); return PlatformScaffold( appBar: PlatformAppBar( title: Text( - widget.title ?? - broadcastRoundState.value?.round.name ?? - 'BroadcastGame', + widget.title ?? title ?? 'BroadcastGame', overflow: TextOverflow.ellipsis, maxLines: 1, ), From e845066a63ac41f693b4b3428f9541035140c939 Mon Sep 17 00:00:00 2001 From: Julien <120588494+julien4215@users.noreply.github.com> Date: Fri, 13 Dec 2024 10:54:02 +0100 Subject: [PATCH 07/16] Fix color of BOT title --- lib/src/view/broadcast/broadcast_player_widget.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/view/broadcast/broadcast_player_widget.dart b/lib/src/view/broadcast/broadcast_player_widget.dart index dd25b72236..b2f081f63a 100644 --- a/lib/src/view/broadcast/broadcast_player_widget.dart +++ b/lib/src/view/broadcast/broadcast_player_widget.dart @@ -37,7 +37,7 @@ class BroadcastPlayerWidget extends ConsumerWidget { title!, style: TextStyle( color: (title == 'BOT') - ? context.lichessColors.purple + ? context.lichessColors.fancy : context.lichessColors.brag, fontWeight: FontWeight.bold, ), From c2f59b62f2fdc6ea136593aa7e2a9ebeac9b61ab Mon Sep 17 00:00:00 2001 From: Julien <120588494+julien4215@users.noreply.github.com> Date: Fri, 13 Dec 2024 11:33:40 +0100 Subject: [PATCH 08/16] Make players results accessible from broadcast game screen --- .../view/broadcast/broadcast_boards_tab.dart | 9 +- .../view/broadcast/broadcast_game_screen.dart | 143 ++++++++++-------- .../broadcast_player_results_screen.dart | 1 + .../broadcast/broadcast_round_screen.dart | 2 + 4 files changed, 94 insertions(+), 61 deletions(-) diff --git a/lib/src/view/broadcast/broadcast_boards_tab.dart b/lib/src/view/broadcast/broadcast_boards_tab.dart index e5ab9e9153..7f0df075f7 100644 --- a/lib/src/view/broadcast/broadcast_boards_tab.dart +++ b/lib/src/view/broadcast/broadcast_boards_tab.dart @@ -25,10 +25,12 @@ 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.tournamentId, required this.roundId, required this.tournamentSlug, }); + final BroadcastTournamentId tournamentId; final BroadcastRoundId roundId; final String tournamentSlug; @@ -59,6 +61,7 @@ class BroadcastBoardsTab extends ConsumerWidget { ) : BroadcastPreview( games: value.games.values.toIList(), + tournamentId: tournamentId, roundId: roundId, title: value.round.name, tournamentSlug: tournamentSlug, @@ -81,6 +84,7 @@ class BroadcastBoardsTab extends ConsumerWidget { class BroadcastPreview extends StatelessWidget { const BroadcastPreview({ + required this.tournamentId, required this.roundId, required this.games, required this.title, @@ -89,12 +93,14 @@ class BroadcastPreview extends StatelessWidget { }); const BroadcastPreview.loading() - : roundId = const BroadcastRoundId(''), + : tournamentId = const BroadcastTournamentId(''), + roundId = const BroadcastRoundId(''), games = null, title = '', tournamentSlug = '', roundSlug = ''; + final BroadcastTournamentId tournamentId; final BroadcastRoundId roundId; final IList? games; final String title; @@ -148,6 +154,7 @@ class BroadcastPreview extends StatelessWidget { context, title: title, builder: (context) => BroadcastGameScreen( + tournamentId: tournamentId, roundId: roundId, gameId: game.id, tournamentSlug: tournamentSlug, diff --git a/lib/src/view/broadcast/broadcast_game_screen.dart b/lib/src/view/broadcast/broadcast_game_screen.dart index 2f8fb62a7b..d11bce316b 100644 --- a/lib/src/view/broadcast/broadcast_game_screen.dart +++ b/lib/src/view/broadcast/broadcast_game_screen.dart @@ -21,6 +21,7 @@ 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_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,6 +32,7 @@ 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? tournamentSlug; @@ -38,6 +40,7 @@ class BroadcastGameScreen extends ConsumerStatefulWidget { final String? title; const BroadcastGameScreen({ + required this.tournamentId, required this.roundId, required this.gameId, this.tournamentSlug, @@ -117,6 +120,7 @@ class _BroadcastGameScreenState extends ConsumerState ), body: switch (broadcastGameState) { AsyncData() => _Body( + widget.tournamentId, widget.roundId, widget.gameId, widget.tournamentSlug, @@ -134,6 +138,7 @@ class _BroadcastGameScreenState extends ConsumerState class _Body extends ConsumerWidget { const _Body( + this.tournamentId, this.roundId, this.gameId, this.tournamentSlug, @@ -141,6 +146,7 @@ class _Body extends ConsumerWidget { required this.tabController, }); + final BroadcastTournamentId tournamentId; final BroadcastRoundId roundId; final BroadcastGameId gameId; final String? tournamentSlug; @@ -169,11 +175,13 @@ class _Body extends ConsumerWidget { borderRadius, ), boardHeader: _PlayerWidget( + tournamentId: tournamentId, roundId: roundId, gameId: gameId, widgetPosition: _PlayerWidgetPosition.top, ), boardFooter: _PlayerWidget( + tournamentId: tournamentId, roundId: roundId, gameId: gameId, widgetPosition: _PlayerWidgetPosition.bottom, @@ -361,11 +369,13 @@ enum _PlayerWidgetPosition { bottom, top } class _PlayerWidget extends ConsumerWidget { 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; @@ -392,71 +402,84 @@ class _PlayerWidget extends ConsumerWidget { final player = game.players[side]!; final gameStatus = game.status; - 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, ), - if (clock != 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: isCursorOnLiveMove - ? CountdownClockBuilder( - timeLeft: clock, - active: side == sideToMove, - builder: (context, timeLeft) => _Clock( - timeLeft: timeLeft, + ); + }, + 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 (clock != 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: isCursorOnLiveMove + ? CountdownClockBuilder( + timeLeft: clock, + active: side == sideToMove, + builder: (context, timeLeft) => _Clock( + timeLeft: timeLeft, + isSideToMove: side == sideToMove, + isLive: true, + ), + tickInterval: const Duration(seconds: 1), + clockUpdatedAt: + side == sideToMove ? game.updatedClockAt : null, + ) + : _Clock( + timeLeft: clock, isSideToMove: side == sideToMove, - isLive: true, + isLive: false, ), - tickInterval: const Duration(seconds: 1), - clockUpdatedAt: - side == sideToMove ? game.updatedClockAt : null, - ) - : _Clock( - timeLeft: clock, - isSideToMove: side == sideToMove, - isLive: false, - ), + ), ), ), - ), - ], + ], + ), ), ); } diff --git a/lib/src/view/broadcast/broadcast_player_results_screen.dart b/lib/src/view/broadcast/broadcast_player_results_screen.dart index ab90cf1e46..6b494ee144 100644 --- a/lib/src/view/broadcast/broadcast_player_results_screen.dart +++ b/lib/src/view/broadcast/broadcast_player_results_screen.dart @@ -225,6 +225,7 @@ class _Body extends ConsumerWidget { pushPlatformRoute( context, builder: (context) => BroadcastGameScreen( + tournamentId: tournamentId, roundId: playerResult.roundId, gameId: playerResult.gameId, ), diff --git a/lib/src/view/broadcast/broadcast_round_screen.dart b/lib/src/view/broadcast/broadcast_round_screen.dart index 946b040ffd..b98465cf54 100644 --- a/lib/src/view/broadcast/broadcast_round_screen.dart +++ b/lib/src/view/broadcast/broadcast_round_screen.dart @@ -117,6 +117,7 @@ class _BroadcastRoundScreenState extends ConsumerState cupertinoTabSwitcher: tabSwitcher, sliver: switch (asyncTournament) { AsyncData(:final value) => BroadcastBoardsTab( + tournamentId: _selectedTournamentId, roundId: _selectedRoundId ?? value.defaultRoundId, tournamentSlug: widget.broadcast.tour.slug, ), @@ -186,6 +187,7 @@ class _BroadcastRoundScreenState extends ConsumerState _TabView( sliver: switch (asyncTournament) { AsyncData(:final value) => BroadcastBoardsTab( + tournamentId: _selectedTournamentId, roundId: _selectedRoundId ?? value.defaultRoundId, tournamentSlug: widget.broadcast.tour.slug, ), From 387c057951177a23f50441145ef476b35cfb6e57 Mon Sep 17 00:00:00 2001 From: Julien <120588494+julien4215@users.noreply.github.com> Date: Fri, 13 Dec 2024 12:16:18 +0100 Subject: [PATCH 09/16] Fix null value error on broadcast game screen --- .../broadcast_player_results_screen.dart | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/lib/src/view/broadcast/broadcast_player_results_screen.dart b/lib/src/view/broadcast/broadcast_player_results_screen.dart index 6b494ee144..ccd962576d 100644 --- a/lib/src/view/broadcast/broadcast_player_results_screen.dart +++ b/lib/src/view/broadcast/broadcast_player_results_screen.dart @@ -184,14 +184,15 @@ class _Body extends ConsumerWidget { mainAxisAlignment: MainAxisAlignment.center, spacing: cardSpacing, children: [ - SizedBox( - width: statWidth, - child: StatCard( - context.l10n.broadcastScore, - value: - '${player.score!.toStringAsFixed((player.score! == player.score!.roundToDouble()) ? 0 : 1)} / ${player.played}', + 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, From 1cb097bb6db686f87b62f9b701a354e71be24016 Mon Sep 17 00:00:00 2001 From: Julien <120588494+julien4215@users.noreply.github.com> Date: Fri, 13 Dec 2024 12:16:39 +0100 Subject: [PATCH 10/16] Check if player information is loaded --- lib/src/view/broadcast/broadcast_game_screen.dart | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/src/view/broadcast/broadcast_game_screen.dart b/lib/src/view/broadcast/broadcast_game_screen.dart index d11bce316b..2c6bac99b1 100644 --- a/lib/src/view/broadcast/broadcast_game_screen.dart +++ b/lib/src/view/broadcast/broadcast_game_screen.dart @@ -397,8 +397,11 @@ class _PlayerWidget extends ConsumerWidget { final game = ref.watch( broadcastRoundControllerProvider(roundId) - .select((round) => round.requireValue.games[gameId]!), + .select((round) => round.value?.games[gameId]), ); + + if (game == null) return const SizedBox.shrink(); + final player = game.players[side]!; final gameStatus = game.status; From 4e9ad4526df696aca9c38f9fd08807e3bf264cd9 Mon Sep 17 00:00:00 2001 From: Julien <120588494+julien4215@users.noreply.github.com> Date: Fri, 13 Dec 2024 18:05:32 +0100 Subject: [PATCH 11/16] Use providers with selectAsync to get values from broadcast round controller --- .../view/broadcast/broadcast_game_screen.dart | 39 +++++++++---------- .../broadcast_game_screen_providers.dart | 30 ++++++++++++++ 2 files changed, 49 insertions(+), 20 deletions(-) create mode 100644 lib/src/view/broadcast/broadcast_game_screen_providers.dart diff --git a/lib/src/view/broadcast/broadcast_game_screen.dart b/lib/src/view/broadcast/broadcast_game_screen.dart index 00ff38a81a..4c721c2d9f 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,6 +18,7 @@ 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'; @@ -82,17 +82,21 @@ class _BroadcastGameScreenState extends ConsumerState @override Widget build(BuildContext context) { - final broadcastGameState = ref - .watch(broadcastGameControllerProvider(widget.roundId, widget.gameId)); - final title = ref.watch( - broadcastRoundControllerProvider(widget.roundId) - .select((round) => round.value?.round.name), + final broadcastGameState = ref.watch( + broadcastGameProvider(widget.roundId, widget.gameId), ); + final broadcastGamePgn = ref + .watch(broadcastGameControllerProvider(widget.roundId, widget.gameId)); + final title = widget.title ?? + (switch (ref.watch(broadcastGameScreenTitleProvider(widget.roundId))) { + AsyncData(value: final title) => title, + _ => 'Broadcast Game', + }); return PlatformScaffold( appBar: PlatformAppBar( title: Text( - widget.title ?? title ?? 'BroadcastGame', + title, overflow: TextOverflow.ellipsis, maxLines: 1, ), @@ -102,7 +106,7 @@ class _BroadcastGameScreenState extends ConsumerState controller: _tabController, ), AppBarIconButton( - onPressed: (broadcastGameState.hasValue) + onPressed: (broadcastGamePgn.hasValue) ? () { pushPlatformRoute( context, @@ -118,8 +122,8 @@ class _BroadcastGameScreenState extends ConsumerState ), ], ), - body: switch (broadcastGameState) { - AsyncData() => _Body( + body: switch ((broadcastGameState, broadcastGamePgn)) { + (AsyncData(), AsyncData()) => _Body( widget.tournamentId, widget.roundId, widget.gameId, @@ -127,7 +131,10 @@ class _BroadcastGameScreenState extends ConsumerState widget.roundSlug, tabController: _tabController, ), - AsyncError(:final error) => Center( + (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()), @@ -385,15 +392,7 @@ class _PlayerWidget extends ConsumerWidget { 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.value?.games[gameId]), - ); - - if (game == null) return const SizedBox.shrink(); + final game = ref.watch(broadcastGameProvider(roundId, gameId)).requireValue; final isCursorOnLiveMove = broadcastGameState.currentPath == broadcastGameState.broadcastLivePath; 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..a78879d2ba --- /dev/null +++ b/lib/src/view/broadcast/broadcast_game_screen_providers.dart @@ -0,0 +1,30 @@ +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( + Ref ref, + BroadcastRoundId roundId, + BroadcastGameId gameId, +) { + return ref.watch( + broadcastRoundControllerProvider(roundId) + .selectAsync((round) => round.games[gameId]!), + ); +} + +@riverpod +Future broadcastGameScreenTitle( + Ref ref, + BroadcastRoundId roundId, +) { + return ref.watch( + broadcastRoundControllerProvider(roundId) + .selectAsync((round) => round.round.name), + ); +} From 7c4098cd360bb3c3fa5bbdb76d4c85ab26502146 Mon Sep 17 00:00:00 2001 From: Julien <120588494+julien4215@users.noreply.github.com> Date: Fri, 13 Dec 2024 18:21:19 +0100 Subject: [PATCH 12/16] Improve score sorting on broadcast tab players --- lib/src/view/broadcast/broadcast_players_tab.dart | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/lib/src/view/broadcast/broadcast_players_tab.dart b/lib/src/view/broadcast/broadcast_players_tab.dart index 13585c8422..fb0255204d 100644 --- a/lib/src/view/broadcast/broadcast_players_tab.dart +++ b/lib/src/view/broadcast/broadcast_players_tab.dart @@ -87,7 +87,12 @@ class _PlayersListState extends ConsumerState { (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; + } } }; From b623b3feb87e9052a815fcb3af46e73257f3cd27 Mon Sep 17 00:00:00 2001 From: Julien <120588494+julien4215@users.noreply.github.com> Date: Sun, 15 Dec 2024 15:43:25 +0100 Subject: [PATCH 13/16] Remove useless Column widget --- .../broadcast_player_results_screen.dart | 502 +++++++++--------- 1 file changed, 243 insertions(+), 259 deletions(-) diff --git a/lib/src/view/broadcast/broadcast_player_results_screen.dart b/lib/src/view/broadcast/broadcast_player_results_screen.dart index ccd962576d..d867b341c3 100644 --- a/lib/src/view/broadcast/broadcast_player_results_screen.dart +++ b/lib/src/view/broadcast/broadcast_player_results_screen.dart @@ -75,286 +75,270 @@ class _Body extends ConsumerWidget { 28.0, ); - return Column( - children: [ - Expanded( - child: ListView.builder( - itemCount: playerResults.games.length + 1, - itemBuilder: (context, index) { - if (index == 0) { - return Padding( - padding: Styles.bodyPadding, - child: Column( + 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 && - 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.ratings.standard != null) + SizedBox( + width: statWidth, + child: StatCard( + context.l10n.classical, + value: fideData.ratings.standard.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: [ - SvgPicture.network( - lichessFideFedSrc( - player.federation!, - ), - height: 12, - httpClient: - ref.read(defaultClientProvider), - ), - 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(), - ), - ), - ], + if (fideData.ratings.rapid != null) + SizedBox( + width: statWidth, + child: StatCard( + context.l10n.rapid, + value: fideData.ratings.rapid.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: [ + if (fideData.ratings.blitz != null) SizedBox( - width: indexWidth, - child: Center( - child: Text( - index.toString(), - style: const TextStyle( - fontWeight: FontWeight.bold, - ), - ), + width: statWidth, + child: StatCard( + context.l10n.blitz, + value: fideData.ratings.blitz.toString(), ), ), - Expanded( - flex: 5, - child: BroadcastPlayerWidget( - federation: playerResult.opponent.federation, - title: playerResult.opponent.title, - name: playerResult.opponent.name, + ], + ), + 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(), ), ), - Expanded( - flex: 3, - child: (playerResult.opponent.rating != null) - ? Center( + if (player.federation != null) + SizedBox( + width: statWidth, + child: StatCard( + context.l10n.broadcastFederation, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SvgPicture.network( + lichessFideFedSrc( + player.federation!, + ), + height: 12, + httpClient: + ref.read(defaultClientProvider), + ), + const SizedBox(width: 5), + Flexible( child: Text( - playerResult.opponent.rating.toString(), + federationIdToName[player.federation!]!, + style: const TextStyle( + fontSize: 18.0, + ), + overflow: TextOverflow.ellipsis, ), - ) - : 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), - }, - ), + ), + ], ), ), ), + if (player.fideId != null) 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 - }, - ), - ), + width: statWidth, + child: StatCard( + 'FIDE ID', + value: player.fideId!.toString(), ), ), - if (showRatingDiff) - SizedBox( - width: 38, - child: (playerResult.ratingDiff != null) - ? ProgressionWidget( - playerResult.ratingDiff!, - fontSize: 14, - ) - : null, + ], + ), + 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')); From 10cb3366b6296430c94941ea0b0424ea3a33fdf5 Mon Sep 17 00:00:00 2001 From: Julien <120588494+julien4215@users.noreply.github.com> Date: Sun, 15 Dec 2024 17:52:37 +0100 Subject: [PATCH 14/16] Don't show a default title if title is not yet loaded --- .../view/broadcast/broadcast_game_screen.dart | 25 +++++++++++-------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/lib/src/view/broadcast/broadcast_game_screen.dart b/lib/src/view/broadcast/broadcast_game_screen.dart index 4c721c2d9f..bcd3ad5c78 100644 --- a/lib/src/view/broadcast/broadcast_game_screen.dart +++ b/lib/src/view/broadcast/broadcast_game_screen.dart @@ -87,19 +87,24 @@ class _BroadcastGameScreenState extends ConsumerState ); final broadcastGamePgn = ref .watch(broadcastGameControllerProvider(widget.roundId, widget.gameId)); - final title = widget.title ?? - (switch (ref.watch(broadcastGameScreenTitleProvider(widget.roundId))) { - AsyncData(value: final title) => title, - _ => 'Broadcast Game', - }); + 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( - title, - overflow: TextOverflow.ellipsis, - maxLines: 1, - ), + title: title, actions: [ AppBarAnalysisTabIndicator( tabs: tabs, From b410cbe0c30144478774062cde2c4a44f60e4729 Mon Sep 17 00:00:00 2001 From: Julien <120588494+julien4215@users.noreply.github.com> Date: Sun, 15 Dec 2024 19:02:11 +0100 Subject: [PATCH 15/16] Rename some broadcast variables --- lib/src/view/broadcast/broadcast_game_screen.dart | 13 +++++++------ .../broadcast/broadcast_game_screen_providers.dart | 2 +- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/lib/src/view/broadcast/broadcast_game_screen.dart b/lib/src/view/broadcast/broadcast_game_screen.dart index bcd3ad5c78..9883ad9c3e 100644 --- a/lib/src/view/broadcast/broadcast_game_screen.dart +++ b/lib/src/view/broadcast/broadcast_game_screen.dart @@ -82,10 +82,10 @@ class _BroadcastGameScreenState extends ConsumerState @override Widget build(BuildContext context) { - final broadcastGameState = ref.watch( - broadcastGameProvider(widget.roundId, widget.gameId), + final broadcastRoundGameState = ref.watch( + broadcastRoundGameProvider(widget.roundId, widget.gameId), ); - final broadcastGamePgn = ref + final broadcastGameState = ref .watch(broadcastGameControllerProvider(widget.roundId, widget.gameId)); final title = (widget.title != null) ? Text( @@ -111,7 +111,7 @@ class _BroadcastGameScreenState extends ConsumerState controller: _tabController, ), AppBarIconButton( - onPressed: (broadcastGamePgn.hasValue) + onPressed: (broadcastGameState.hasValue) ? () { pushPlatformRoute( context, @@ -127,7 +127,7 @@ class _BroadcastGameScreenState extends ConsumerState ), ], ), - body: switch ((broadcastGameState, broadcastGamePgn)) { + body: switch ((broadcastRoundGameState, broadcastGameState)) { (AsyncData(), AsyncData()) => _Body( widget.tournamentId, widget.roundId, @@ -397,7 +397,8 @@ class _PlayerWidget extends ConsumerWidget { final broadcastGameState = ref .watch(broadcastGameControllerProvider(roundId, gameId)) .requireValue; - final game = ref.watch(broadcastGameProvider(roundId, gameId)).requireValue; + final game = + ref.watch(broadcastRoundGameProvider(roundId, gameId)).requireValue; final isCursorOnLiveMove = broadcastGameState.currentPath == broadcastGameState.broadcastLivePath; diff --git a/lib/src/view/broadcast/broadcast_game_screen_providers.dart b/lib/src/view/broadcast/broadcast_game_screen_providers.dart index a78879d2ba..8490ee3988 100644 --- a/lib/src/view/broadcast/broadcast_game_screen_providers.dart +++ b/lib/src/view/broadcast/broadcast_game_screen_providers.dart @@ -7,7 +7,7 @@ import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'broadcast_game_screen_providers.g.dart'; @riverpod -Future broadcastGame( +Future broadcastRoundGame( Ref ref, BroadcastRoundId roundId, BroadcastGameId gameId, From 5ad924633effecd32aabed4450cc9e5fb81b3a23 Mon Sep 17 00:00:00 2001 From: Julien <120588494+julien4215@users.noreply.github.com> Date: Tue, 17 Dec 2024 17:14:34 +0100 Subject: [PATCH 16/16] Use the new flag asset --- .../view/broadcast/broadcast_player_results_screen.dart | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/lib/src/view/broadcast/broadcast_player_results_screen.dart b/lib/src/view/broadcast/broadcast_player_results_screen.dart index 21e88e2326..2b1d14527b 100644 --- a/lib/src/view/broadcast/broadcast_player_results_screen.dart +++ b/lib/src/view/broadcast/broadcast_player_results_screen.dart @@ -4,15 +4,12 @@ import 'package:dartchess/dartchess.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:flutter_svg/svg.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/network/http.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; -import 'package:lichess_mobile/src/utils/lichess_assets.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'; @@ -129,10 +126,9 @@ class _Body extends ConsumerWidget { child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - SvgPicture.network( - lichessFideFedSrc(player.federation!), + Image.asset( + 'assets/images/fide-fed/${player.federation}.png', height: 12, - httpClient: ref.read(defaultClientProvider), ), const SizedBox(width: 5), Flexible(