From 32c6c384e64a964abefceeb12916ff2c493235e6 Mon Sep 17 00:00:00 2001 From: Julien <120588494+julien4215@users.noreply.github.com> Date: Mon, 9 Dec 2024 12:53:05 +0100 Subject: [PATCH] wip --- lib/src/model/broadcast/broadcast.dart | 19 ++- .../model/broadcast/broadcast_providers.dart | 45 +++++ .../model/broadcast/broadcast_repository.dart | 30 +++- lib/src/model/common/id.dart | 21 +++ .../view/broadcast/broadcast_boards_tab.dart | 43 +---- .../view/broadcast/broadcast_game_screen.dart | 44 +---- .../broadcast/broadcast_player_widget.dart | 63 +++++++ .../view/broadcast/broadcast_players_tab.dart | 161 ++++++++++++++++++ .../broadcast/broadcast_round_screen.dart | 47 +++-- lib/src/view/user/perf_stats_screen.dart | 47 +---- lib/src/widgets/progression_widget.dart | 48 ++++++ 11 files changed, 428 insertions(+), 140 deletions(-) create mode 100644 lib/src/view/broadcast/broadcast_player_widget.dart create mode 100644 lib/src/view/broadcast/broadcast_players_tab.dart create mode 100644 lib/src/widgets/progression_widget.dart diff --git a/lib/src/model/broadcast/broadcast.dart b/lib/src/model/broadcast/broadcast.dart index e662e90090..fff18f2d78 100644 --- a/lib/src/model/broadcast/broadcast.dart +++ b/lib/src/model/broadcast/broadcast.dart @@ -75,8 +75,6 @@ typedef BroadcastTournamentGroup = ({ @freezed class BroadcastRound with _$BroadcastRound { - const BroadcastRound._(); - const factory BroadcastRound({ required BroadcastRoundId id, required String name, @@ -117,17 +115,30 @@ class BroadcastGame with _$BroadcastGame { @freezed class BroadcastPlayer with _$BroadcastPlayer { - const BroadcastPlayer._(); - const factory BroadcastPlayer({ required String name, required String? title, required int? rating, required Duration? clock, required String? federation, + required FideId? fideId, }) = _BroadcastPlayer; } +@freezed +class BroadcastPlayerExtended with _$BroadcastPlayerExtended { + const factory BroadcastPlayerExtended({ + required String name, + required String? title, + required int? rating, + required String? federation, + required FideId? fideId, + required int played, + required double? score, + required int? ratingDiff, + }) = _BroadcastPlayerExtended; +} + enum RoundStatus { live, finished, diff --git a/lib/src/model/broadcast/broadcast_providers.dart b/lib/src/model/broadcast/broadcast_providers.dart index 0c9f685d32..b782ae7081 100644 --- a/lib/src/model/broadcast/broadcast_providers.dart +++ b/lib/src/model/broadcast/broadcast_providers.dart @@ -1,3 +1,4 @@ +import 'package:fast_immutable_collections/fast_immutable_collections.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_repository.dart'; @@ -54,6 +55,50 @@ Future broadcastTournament( ); } +enum BroadcastPlayersSortingTypes { player, elo, score } + +@riverpod +class BroadcastPlayers extends _$BroadcastPlayers { + @override + Future> build( + BroadcastTournamentId tournamentId, + ) async { + final players = ref.withClient( + (client) => BroadcastRepository(client).getPlayers(tournamentId), + ); + + return players; + } + + void sort(BroadcastPlayersSortingTypes sortingType, [bool reverse = false]) { + if (!state.hasValue) return; + + final compare = switch (sortingType) { + BroadcastPlayersSortingTypes.player => + (BroadcastPlayerExtended a, BroadcastPlayerExtended b) => + a.name.compareTo(b.name), + BroadcastPlayersSortingTypes.elo => + (BroadcastPlayerExtended a, BroadcastPlayerExtended b) { + if (a.rating == null) return -1; + if (b.rating == null) return 1; + return b.rating!.compareTo(a.rating!); + }, + BroadcastPlayersSortingTypes.score => + (BroadcastPlayerExtended a, BroadcastPlayerExtended b) { + if (a.score == null) return -1; + if (b.score == null) return 1; + return b.score!.compareTo(a.score!); + } + }; + + state = AsyncData( + reverse + ? state.requireValue.sortReversed(compare) + : state.requireValue.sort(compare), + ); + } +} + @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 05ddfe2b49..67dec06f47 100644 --- a/lib/src/model/broadcast/broadcast_repository.dart +++ b/lib/src/model/broadcast/broadcast_repository.dart @@ -18,7 +18,6 @@ class BroadcastRepository { path: '/api/broadcast/top', queryParameters: {'page': page.toString()}, ), - headers: {'Accept': 'application/json'}, mapper: _makeBroadcastResponseFromJson, ); } @@ -28,7 +27,6 @@ class BroadcastRepository { ) { return client.readJson( Uri(path: 'api/broadcast/$broadcastTournamentId'), - headers: {'Accept': 'application/json'}, mapper: _makeTournamentFromJson, ); } @@ -40,7 +38,6 @@ class BroadcastRepository { Uri(path: 'api/broadcast/-/-/$broadcastRoundId'), // The path parameters with - are the broadcast tournament and round slugs // They are only used for SEO, so we can safely use - for these parameters - headers: {'Accept': 'application/x-ndjson'}, mapper: _makeRoundWithGamesFromJson, ); } @@ -51,6 +48,15 @@ class BroadcastRepository { ) { return client.read(Uri(path: 'api/study/$roundId/$gameId.pgn')); } + + Future> getPlayers( + BroadcastTournamentId tournamentId, + ) { + return client.readJsonList( + Uri(path: '/broadcast/$tournamentId/players'), + mapper: _makePlayerFromJson, + ); + } } BroadcastList _makeBroadcastResponseFromJson( @@ -195,5 +201,23 @@ BroadcastPlayer _playerFromPick(RequiredPick pick) { rating: pick('rating').asIntOrNull(), clock: pick('clock').asDurationFromCentiSecondsOrNull(), federation: pick('fed').asStringOrNull(), + fideId: pick('fideId').asFideIdOrNull(), + ); +} + +BroadcastPlayerExtended _makePlayerFromJson(Map json) { + return _playerExtendedFromPick(pick(json).required()); +} + +BroadcastPlayerExtended _playerExtendedFromPick(RequiredPick pick) { + return BroadcastPlayerExtended( + name: pick('name').asStringOrThrow(), + title: pick('title').asStringOrNull(), + rating: pick('rating').asIntOrNull(), + federation: pick('fed').asStringOrNull(), + fideId: pick('fideId').asFideIdOrNull(), + played: pick('played').asIntOrThrow(), + score: pick('score').asDoubleOrNull(), + ratingDiff: pick('ratingDiff').asIntOrNull(), ); } diff --git a/lib/src/model/common/id.dart b/lib/src/model/common/id.dart index 783c6f3bb6..b41a4e82a6 100644 --- a/lib/src/model/common/id.dart +++ b/lib/src/model/common/id.dart @@ -65,6 +65,8 @@ 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 IDPick on Pick { UserId asUserIdOrThrow() { final value = required().value; @@ -227,4 +229,23 @@ extension IDPick on Pick { "value $value at $debugParsingExit can't be casted to StudyId", ); } + + FideId asFideIdOrThrow() { + final value = required().value; + if (value is String) { + return FideId(value); + } + throw PickException( + "value $value at $debugParsingExit can't be casted to FideId", + ); + } + + FideId? asFideIdOrNull() { + if (value == null) return null; + try { + return asFideIdOrThrow(); + } catch (_) { + return null; + } + } } diff --git a/lib/src/view/broadcast/broadcast_boards_tab.dart b/lib/src/view/broadcast/broadcast_boards_tab.dart index 89cfc2f49a..0709a800a8 100644 --- a/lib/src/view/broadcast/broadcast_boards_tab.dart +++ b/lib/src/view/broadcast/broadcast_boards_tab.dart @@ -2,18 +2,16 @@ import 'package:dartchess/dartchess.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:flutter_svg/flutter_svg.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:lichess_mobile/src/network/http.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/utils/duration.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/utils/screen.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/board_thumbnail.dart'; import 'package:lichess_mobile/src/widgets/clock.dart'; import 'package:lichess_mobile/src/widgets/shimmer.dart'; @@ -210,40 +208,11 @@ class _PlayerWidget extends StatelessWidget { mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Flexible( - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - if (player.federation != null) ...[ - Consumer( - builder: (context, widgetRef, _) { - return SvgPicture.network( - lichessFideFedSrc(player.federation!), - height: 12, - httpClient: widgetRef.read(defaultClientProvider), - ); - }, - ), - ], - const SizedBox(width: 5), - if (player.title != null) ...[ - Text( - player.title!, - style: const TextStyle().copyWith( - color: context.lichessColors.brag, - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(width: 5), - ], - Flexible( - child: Text( - player.name, - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - ), - ], + Expanded( + child: BroadcastPlayerWidget( + federation: player.federation, + title: player.title, + name: player.name, ), ), const SizedBox(width: 5), diff --git a/lib/src/view/broadcast/broadcast_game_screen.dart b/lib/src/view/broadcast/broadcast_game_screen.dart index ceda09ccb9..75e656fc22 100644 --- a/lib/src/view/broadcast/broadcast_game_screen.dart +++ b/lib/src/view/broadcast/broadcast_game_screen.dart @@ -3,7 +3,6 @@ import 'package:dartchess/dartchess.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:flutter_svg/svg.dart'; 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'; @@ -14,16 +13,15 @@ import 'package:lichess_mobile/src/model/common/eval.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/engine/evaluation_service.dart'; import 'package:lichess_mobile/src/model/settings/board_preferences.dart'; -import 'package:lichess_mobile/src/network/http.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/utils/duration.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/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_widget.dart'; import 'package:lichess_mobile/src/view/engine/engine_gauge.dart'; import 'package:lichess_mobile/src/view/engine/engine_lines.dart'; import 'package:lichess_mobile/src/view/opening_explorer/opening_explorer_view.dart'; @@ -386,40 +384,16 @@ class _PlayerWidget extends ConsumerWidget { ), const SizedBox(width: 16.0), ], - if (player.federation != null) ...[ - SvgPicture.network( - lichessFideFedSrc(player.federation!), - height: 12, - httpClient: ref.read(defaultClientProvider), + Expanded( + child: BroadcastPlayerWidget( + federation: player.federation, + title: player.title, + name: player.name, + rating: player.rating, + textStyle: + const TextStyle().copyWith(fontWeight: FontWeight.bold), ), - const SizedBox(width: 5), - ], - if (player.title != null) ...[ - Text( - player.title!, - style: const TextStyle().copyWith( - color: context.lichessColors.brag, - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(width: 5), - ], - Text( - player.name, - style: const TextStyle().copyWith( - fontWeight: FontWeight.bold, - ), - overflow: TextOverflow.ellipsis, ), - if (player.rating != null) ...[ - const SizedBox(width: 5), - Text( - player.rating.toString(), - style: const TextStyle(), - overflow: TextOverflow.ellipsis, - ), - ], - const Spacer(), if (clock != null) Container( height: kAnalysisBoardHeaderOrFooterHeight, diff --git a/lib/src/view/broadcast/broadcast_player_widget.dart b/lib/src/view/broadcast/broadcast_player_widget.dart new file mode 100644 index 0000000000..598cc62f46 --- /dev/null +++ b/lib/src/view/broadcast/broadcast_player_widget.dart @@ -0,0 +1,63 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:lichess_mobile/src/network/http.dart'; +import 'package:lichess_mobile/src/styles/styles.dart'; +import 'package:lichess_mobile/src/utils/lichess_assets.dart'; + +class BroadcastPlayerWidget extends ConsumerWidget { + const BroadcastPlayerWidget({ + required this.federation, + required this.title, + required this.name, + this.rating, + this.textStyle, + }); + + final String? federation; + final String? title; + final int? rating; + final String name; + final TextStyle? textStyle; + + @override + Widget build(BuildContext context, WidgetRef ref) { + return Row( + children: [ + if (federation != null) ...[ + SvgPicture.network( + lichessFideFedSrc(federation!), + height: 12, + httpClient: ref.read(defaultClientProvider), + ), + const SizedBox(width: 5), + ], + if (title != null) ...[ + Text( + title!, + style: const TextStyle().copyWith( + color: context.lichessColors.brag, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(width: 5), + ], + 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, + ), + ], + ], + ); + } +} diff --git a/lib/src/view/broadcast/broadcast_players_tab.dart b/lib/src/view/broadcast/broadcast_players_tab.dart new file mode 100644 index 0000000000..42a62e7356 --- /dev/null +++ b/lib/src/view/broadcast/broadcast_players_tab.dart @@ -0,0 +1,161 @@ +import 'package:fast_immutable_collections/fast_immutable_collections.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_providers.dart'; +import 'package:lichess_mobile/src/model/common/id.dart'; +import 'package:lichess_mobile/src/utils/l10n_context.dart'; +import 'package:lichess_mobile/src/view/broadcast/broadcast_player_widget.dart'; +import 'package:lichess_mobile/src/widgets/progression_widget.dart'; + +/// A tab that displays the players participating in a broadcast tournament. +class BroadcastPlayersTab extends ConsumerWidget { + const BroadcastPlayersTab({required this.tournamentId}); + + final BroadcastTournamentId tournamentId; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final players = ref.watch(broadcastPlayersProvider(tournamentId)); + + return switch (players) { + AsyncData(value: final players) => + SliverToBoxAdapter(child: PlayersList(players, tournamentId)), + AsyncError(:final error) => SliverToBoxAdapter( + child: Center(child: Text('Cannot load players data: $error')), + ), + _ => const SliverToBoxAdapter( + child: Placeholder(), + ), + }; + } +} + +const _kTableRowVerticalPadding = 10.0; +const _kTableRowHorizontalPadding = 8.0; +const _kTableRowPadding = EdgeInsets.symmetric( + horizontal: _kTableRowHorizontalPadding, + vertical: _kTableRowVerticalPadding, +); +const _kHeaderTextStyle = TextStyle(fontWeight: FontWeight.bold); + +class PlayersList extends ConsumerStatefulWidget { + const PlayersList(this.players, this.tournamentId); + + final IList players; + final BroadcastTournamentId tournamentId; + + @override + ConsumerState createState() => _PlayersListState(); +} + +class _PlayersListState extends ConsumerState { + @override + Widget build(BuildContext context) { + return Table( + columnWidths: const { + 0: FlexColumnWidth(), + 1: FixedColumnWidth(100), + 2: FixedColumnWidth(60), + }, + children: [ + TableRow( + children: [ + GestureDetector( + onTap: () { + ref + .read( + broadcastPlayersProvider(widget.tournamentId).notifier, + ) + .sort(BroadcastPlayersSortingTypes.player); + }, + child: ColoredBox( + color: Theme.of(context).colorScheme.secondaryContainer, + child: Padding( + padding: _kTableRowPadding, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(context.l10n.player, style: _kHeaderTextStyle), + const Icon(Icons.keyboard_arrow_up), + ], + ), + ), + ), + ), + GestureDetector( + onTap: () { + ref + .read( + broadcastPlayersProvider(widget.tournamentId).notifier, + ) + .sort(BroadcastPlayersSortingTypes.elo); + }, + child: ColoredBox( + color: Theme.of(context).colorScheme.outline, + child: const Padding( + padding: _kTableRowPadding, + child: Text('Elo', style: _kHeaderTextStyle), + ), + ), + ), + GestureDetector( + onTap: () { + ref + .read( + broadcastPlayersProvider(widget.tournamentId).notifier, + ) + .sort(BroadcastPlayersSortingTypes.score); + }, + child: ColoredBox( + color: Theme.of(context).colorScheme.secondaryContainer, + child: Padding( + padding: _kTableRowPadding, + child: Text( + context.l10n.broadcastScore, + style: _kHeaderTextStyle, + ), + ), + ), + ), + ], + ), + ...widget.players.indexed.map( + (player) => TableRow( + decoration: BoxDecoration( + color: player.$1.isEven + ? Theme.of(context).colorScheme.surfaceContainerLow + : Theme.of(context).colorScheme.surfaceContainerHigh, + ), + children: [ + Padding( + padding: _kTableRowPadding, + child: BroadcastPlayerWidget( + federation: player.$2.federation, + title: player.$2.title, + name: player.$2.name, + ), + ), + Padding( + padding: _kTableRowPadding, + child: Row( + children: [ + Text(player.$2.rating.toString()), + const SizedBox(width: 5), + if (player.$2.ratingDiff != null) + ProgressionWidget(player.$2.ratingDiff!, fontSize: 16), + ], + ), + ), + if (player.$2.score != null) + Padding( + padding: _kTableRowPadding, + child: Text('${player.$2.score}/${player.$2.played}'), + ), + ], + ), + ), + ], + ); + } +} diff --git a/lib/src/view/broadcast/broadcast_round_screen.dart b/lib/src/view/broadcast/broadcast_round_screen.dart index a0723ad863..d687b04cc2 100644 --- a/lib/src/view/broadcast/broadcast_round_screen.dart +++ b/lib/src/view/broadcast/broadcast_round_screen.dart @@ -12,6 +12,7 @@ import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/view/broadcast/broadcast_boards_tab.dart'; import 'package:lichess_mobile/src/view/broadcast/broadcast_overview_tab.dart'; +import 'package:lichess_mobile/src/view/broadcast/broadcast_players_tab.dart'; import 'package:lichess_mobile/src/widgets/adaptive_bottom_sheet.dart'; import 'package:lichess_mobile/src/widgets/bottom_bar.dart'; import 'package:lichess_mobile/src/widgets/buttons.dart'; @@ -27,7 +28,7 @@ class BroadcastRoundScreen extends ConsumerStatefulWidget { _BroadcastRoundScreenState createState() => _BroadcastRoundScreenState(); } -enum _CupertinoView { overview, boards } +enum _CupertinoView { overview, boards, players } class _BroadcastRoundScreenState extends ConsumerState with SingleTickerProviderStateMixin { @@ -41,7 +42,7 @@ class _BroadcastRoundScreenState extends ConsumerState @override void initState() { super.initState(); - _tabController = TabController(initialIndex: 0, length: 2, vsync: this); + _tabController = TabController(initialIndex: 0, length: 3, vsync: this); _selectedTournamentId = widget.broadcast.tour.id; _selectedRoundId = widget.broadcast.roundToLinkId; } @@ -113,6 +114,7 @@ class _BroadcastRoundScreenState extends ConsumerState children: { _CupertinoView.overview: Text(context.l10n.broadcastOverview), _CupertinoView.boards: Text(context.l10n.broadcastBoards), + _CupertinoView.players: Text(context.l10n.players), }, onValueChanged: (_CupertinoView? view) { if (view != null) { @@ -132,21 +134,28 @@ class _BroadcastRoundScreenState extends ConsumerState child: Column( children: [ Expanded( - child: selectedTab == _CupertinoView.overview - ? _TabView( - cupertinoTabSwitcher: tabSwitcher, - sliver: BroadcastOverviewTab( - broadcast: widget.broadcast, - tournamentId: _selectedTournamentId, - ), - ) - : _TabView( - cupertinoTabSwitcher: tabSwitcher, - sliver: BroadcastBoardsTab( - roundId: _selectedRoundId ?? - tournament.defaultRoundId, - ), + child: switch (selectedTab) { + _CupertinoView.overview => _TabView( + cupertinoTabSwitcher: tabSwitcher, + sliver: BroadcastOverviewTab( + broadcast: widget.broadcast, + tournamentId: _selectedTournamentId, ), + ), + _CupertinoView.boards => _TabView( + cupertinoTabSwitcher: tabSwitcher, + sliver: BroadcastBoardsTab( + roundId: + _selectedRoundId ?? tournament.defaultRoundId, + ), + ), + _CupertinoView.players => _TabView( + cupertinoTabSwitcher: tabSwitcher, + sliver: BroadcastPlayersTab( + tournamentId: _selectedTournamentId, + ), + ), + }, ), _BottomBar( tournament: tournament, @@ -171,6 +180,7 @@ class _BroadcastRoundScreenState extends ConsumerState tabs: [ Tab(text: context.l10n.broadcastOverview), Tab(text: context.l10n.broadcastBoards), + Tab(text: context.l10n.players), ], ), ), @@ -188,6 +198,11 @@ class _BroadcastRoundScreenState extends ConsumerState roundId: _selectedRoundId ?? tournament.defaultRoundId, ), ), + _TabView( + sliver: BroadcastPlayersTab( + tournamentId: _selectedTournamentId, + ), + ), ], ), bottomNavigationBar: _BottomBar( diff --git a/lib/src/view/user/perf_stats_screen.dart b/lib/src/view/user/perf_stats_screen.dart index 9db550fd7d..b26f90f383 100644 --- a/lib/src/view/user/perf_stats_screen.dart +++ b/lib/src/view/user/perf_stats_screen.dart @@ -17,7 +17,6 @@ import 'package:lichess_mobile/src/model/game/game_repository.dart'; import 'package:lichess_mobile/src/model/user/user.dart'; import 'package:lichess_mobile/src/model/user/user_repository_providers.dart'; import 'package:lichess_mobile/src/network/http.dart'; -import 'package:lichess_mobile/src/styles/lichess_icons.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/utils/duration.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; @@ -31,6 +30,7 @@ import 'package:lichess_mobile/src/widgets/feedback.dart'; import 'package:lichess_mobile/src/widgets/list.dart'; import 'package:lichess_mobile/src/widgets/platform.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/rating.dart'; import 'package:lichess_mobile/src/widgets/stat_card.dart'; import 'package:lichess_mobile/src/widgets/user_full_name.dart'; @@ -213,7 +213,7 @@ class _Body extends ConsumerWidget { context.l10n .perfStatProgressOverLastXGames('12') .replaceAll(':', ''), - child: _ProgressionWidget(data.progress), + child: ProgressionWidget(data.progress), ), StatCardRow([ if (data.rank != null) @@ -424,49 +424,6 @@ class _Body extends ConsumerWidget { } } -class _ProgressionWidget extends StatelessWidget { - final int progress; - - const _ProgressionWidget(this.progress); - - @override - Widget build(BuildContext context) { - const progressionFontSize = 20.0; - - return Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - if (progress != 0) ...[ - Icon( - progress > 0 - ? LichessIcons.arrow_full_upperright - : LichessIcons.arrow_full_lowerright, - color: progress > 0 - ? context.lichessColors.good - : context.lichessColors.error, - ), - Text( - progress.abs().toString(), - style: TextStyle( - color: progress > 0 - ? context.lichessColors.good - : context.lichessColors.error, - fontSize: progressionFontSize, - ), - ), - ] else - Text( - '0', - style: TextStyle( - color: textShade(context, _customOpacity), - fontSize: progressionFontSize, - ), - ), - ], - ); - } -} - class _UserGameWidget extends StatelessWidget { final UserPerfGame? game; diff --git a/lib/src/widgets/progression_widget.dart b/lib/src/widgets/progression_widget.dart new file mode 100644 index 0000000000..544c48ecb1 --- /dev/null +++ b/lib/src/widgets/progression_widget.dart @@ -0,0 +1,48 @@ +import 'package:flutter/widgets.dart'; +import 'package:lichess_mobile/src/styles/lichess_icons.dart'; +import 'package:lichess_mobile/src/styles/styles.dart'; + +const _customOpacity = 0.6; + +class ProgressionWidget extends StatelessWidget { + final int progress; + final double fontSize; + + const ProgressionWidget(this.progress, {this.fontSize = 20}); + + @override + Widget build(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (progress != 0) ...[ + Icon( + progress > 0 + ? LichessIcons.arrow_full_upperright + : LichessIcons.arrow_full_lowerright, + size: fontSize, + color: progress > 0 + ? context.lichessColors.good + : context.lichessColors.error, + ), + Text( + progress.abs().toString(), + style: TextStyle( + color: progress > 0 + ? context.lichessColors.good + : context.lichessColors.error, + fontSize: fontSize, + ), + ), + ] else + Text( + '0', + style: TextStyle( + color: textShade(context, _customOpacity), + fontSize: fontSize, + ), + ), + ], + ); + } +}