mirror of
https://github.com/lichess-org/mobile.git
synced 2026-05-26 13:50:52 +00:00
Broadcast Team Screen
This commit is contained in:
@@ -337,3 +337,26 @@ sealed class BroadcastTeamMatch with _$BroadcastTeamMatch {
|
||||
required IList<BroadcastTeamGame> games,
|
||||
}) = _BroadcastTeamMatch;
|
||||
}
|
||||
|
||||
@freezed
|
||||
sealed class BroadcastTeamStandingMatch with _$BroadcastTeamStandingMatch {
|
||||
const factory BroadcastTeamStandingMatch({
|
||||
required BroadcastRoundId roundId,
|
||||
required String opponent,
|
||||
required String points,
|
||||
required double mp,
|
||||
required double gp,
|
||||
}) = _BroadcastTeamStandingMatch;
|
||||
}
|
||||
|
||||
@freezed
|
||||
sealed class BroadcastTeamStanding with _$BroadcastTeamStanding {
|
||||
const factory BroadcastTeamStanding({
|
||||
required String name,
|
||||
required double mp,
|
||||
required double gp,
|
||||
required IList<BroadcastTeamStandingMatch> matches,
|
||||
required IList<BroadcastPlayerWithOverallResult> players,
|
||||
required int? averageRating,
|
||||
}) = _BroadcastTeamStanding;
|
||||
}
|
||||
|
||||
@@ -104,3 +104,11 @@ final broadcastTeamMatchesProvider = FutureProvider.autoDispose
|
||||
.family<IList<BroadcastTeamMatch>, BroadcastRoundId>((Ref ref, BroadcastRoundId roundId) {
|
||||
return ref.read(broadcastRepositoryProvider).getTeamMatches(roundId);
|
||||
}, name: 'BroadcastTeamMatchesProvider');
|
||||
|
||||
final broadcastTeamStandingsProvider = FutureProvider.autoDispose
|
||||
.family<IList<BroadcastTeamStanding>, BroadcastTournamentId>((
|
||||
Ref ref,
|
||||
BroadcastTournamentId tournamentId,
|
||||
) {
|
||||
return ref.read(broadcastRepositoryProvider).getTeamStandings(tournamentId);
|
||||
}, name: 'BroadcastTeamStandingsProvider');
|
||||
|
||||
@@ -94,6 +94,13 @@ class BroadcastRepository {
|
||||
pick(json, 'table').asListOrThrow<BroadcastTeamMatch>(_teamMatchFromPick).toIList(),
|
||||
);
|
||||
}
|
||||
|
||||
Future<IList<BroadcastTeamStanding>> getTeamStandings(BroadcastTournamentId tournamentId) {
|
||||
return client.readJsonList(
|
||||
Uri(path: 'broadcast/$tournamentId/teams/standings'),
|
||||
mapper: (json) => _teamStandingFromPick(pick(json).required()),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
BroadcastList broadcastListFromServerJson(Map<String, dynamic> json) {
|
||||
@@ -404,3 +411,24 @@ BroadcastTeamGame _teamGameFromPick(RequiredPick pick) {
|
||||
pov: pick('pov').asSideOrThrow(),
|
||||
);
|
||||
}
|
||||
|
||||
BroadcastTeamStanding _teamStandingFromPick(RequiredPick pick) {
|
||||
return BroadcastTeamStanding(
|
||||
name: pick('name').asStringOrThrow(),
|
||||
mp: pick('mp').asDoubleOrThrow(),
|
||||
gp: pick('gp').asDoubleOrThrow(),
|
||||
matches: pick('matches').asListOrEmpty(_teamStandingMatchFromPick).toIList(),
|
||||
players: pick('players').asListOrEmpty(_playerWithOverallResultFromPick).toIList(),
|
||||
averageRating: pick('averageRating').asIntOrNull(),
|
||||
);
|
||||
}
|
||||
|
||||
BroadcastTeamStandingMatch _teamStandingMatchFromPick(RequiredPick pick) {
|
||||
return BroadcastTeamStandingMatch(
|
||||
roundId: pick('roundId').asBroadcastRoundIdOrThrow(),
|
||||
opponent: pick('opponent').asStringOrThrow(),
|
||||
points: pick('points').asStringOrThrow(),
|
||||
mp: pick('mp').asDoubleOrThrow(),
|
||||
gp: pick('gp').asDoubleOrThrow(),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ import 'package:lichess_mobile/src/utils/navigation.dart';
|
||||
import 'package:lichess_mobile/src/utils/share.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/view/broadcast/broadcast_team_screen.dart';
|
||||
import 'package:lichess_mobile/src/widgets/buttons.dart';
|
||||
import 'package:lichess_mobile/src/widgets/network_image.dart';
|
||||
import 'package:lichess_mobile/src/widgets/platform.dart';
|
||||
@@ -308,13 +309,20 @@ class _OverallStatPlayer extends StatelessWidget {
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
if (team != null) ...[
|
||||
Row(
|
||||
children: [
|
||||
const SizedBox(width: 100, child: Text('Team')),
|
||||
Expanded(
|
||||
child: Text(team.trim(), style: Theme.of(context).textTheme.bodyLarge),
|
||||
),
|
||||
],
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
Navigator.of(
|
||||
context,
|
||||
).push(BroadcastTeamScreen.buildRoute(context, tournament.data.id, team));
|
||||
},
|
||||
child: Row(
|
||||
children: [
|
||||
const SizedBox(width: 100, child: Text('Team')),
|
||||
Expanded(
|
||||
child: Text(team, style: Theme.of(context).textTheme.bodyLarge),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
|
||||
@@ -0,0 +1,377 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:intl/intl.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/styles/styles.dart';
|
||||
import 'package:lichess_mobile/src/theme.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/view/broadcast/broadcast_round_screen.dart';
|
||||
import 'package:lichess_mobile/src/widgets/network_image.dart';
|
||||
import 'package:lichess_mobile/src/widgets/platform.dart';
|
||||
import 'package:lichess_mobile/src/widgets/stat_card.dart';
|
||||
|
||||
class BroadcastTeamScreen extends ConsumerWidget {
|
||||
const BroadcastTeamScreen({super.key, required this.tournamentId, required this.teamName});
|
||||
|
||||
final BroadcastTournamentId tournamentId;
|
||||
final String teamName;
|
||||
|
||||
static Route<dynamic> buildRoute(
|
||||
BuildContext context,
|
||||
BroadcastTournamentId tournamentId,
|
||||
String teamName,
|
||||
) {
|
||||
return buildScreenRoute(
|
||||
screen: BroadcastTeamScreen(tournamentId: tournamentId, teamName: teamName),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final standingsAsync = ref.watch(broadcastTeamStandingsProvider(tournamentId));
|
||||
final tournamentAsync = ref.watch(broadcastTournamentProvider(tournamentId));
|
||||
|
||||
return PlatformScaffold(
|
||||
appBar: PlatformAppBar(
|
||||
title: Row(
|
||||
children: [
|
||||
const Icon(Icons.groups_3),
|
||||
const SizedBox(width: 8),
|
||||
Flexible(child: Text(teamName, overflow: .ellipsis)),
|
||||
],
|
||||
),
|
||||
),
|
||||
body: switch ((standingsAsync, tournamentAsync)) {
|
||||
(AsyncData(value: final standings), AsyncData(value: final tournament)) => () {
|
||||
final team = standings.where((t) => t.name == teamName).firstOrNull;
|
||||
if (team == null) {
|
||||
return const Center(child: Text('Team not found'));
|
||||
}
|
||||
return _Body(tournament: tournament, team: team);
|
||||
}(),
|
||||
(AsyncError(:final error), _) ||
|
||||
(_, AsyncError(:final error)) => Center(child: Text('Cannot load data: $error')),
|
||||
_ => const Center(child: CircularProgressIndicator.adaptive()),
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _Body extends StatelessWidget {
|
||||
const _Body({required this.tournament, required this.team});
|
||||
|
||||
final BroadcastTournament tournament;
|
||||
final BroadcastTeamStanding team;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListView(
|
||||
children: [
|
||||
_OverallTeamStat(team: team),
|
||||
if (team.players.isNotEmpty) ...[
|
||||
_SectionHeader(title: context.l10n.players),
|
||||
...team.players.asMap().entries.map(
|
||||
(entry) => _PlayerListTile(
|
||||
tournament: tournament,
|
||||
playerResult: entry.value,
|
||||
index: entry.key,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
if (team.matches.isNotEmpty) ...[
|
||||
_SectionHeader(title: context.l10n.broadcastMatchHistory),
|
||||
_MatchHistoryTable(team: team, tournament: tournament),
|
||||
],
|
||||
const SizedBox(height: 32.0),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _OverallTeamStat extends StatelessWidget {
|
||||
const _OverallTeamStat({required this.team});
|
||||
|
||||
final BroadcastTeamStanding team;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final statWidth =
|
||||
(MediaQuery.sizeOf(context).width - Styles.bodyPadding.horizontal - 10 * 2) / 3;
|
||||
const cardSpacing = 10.0;
|
||||
|
||||
return Padding(
|
||||
padding: Styles.bodyPadding.copyWith(top: 16.0, bottom: 16.0),
|
||||
child: Column(
|
||||
spacing: cardSpacing,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: .center,
|
||||
spacing: cardSpacing,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: statWidth,
|
||||
child: _StatCard(
|
||||
context.l10n.broadcastMatches,
|
||||
value: team.matches.length.toString(),
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
width: statWidth,
|
||||
child: _StatCard(
|
||||
context.l10n.broadcastMatchPoints,
|
||||
value: NumberFormat('#.#').format(team.mp),
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
width: statWidth,
|
||||
child: _StatCard(
|
||||
context.l10n.broadcastGamePoints,
|
||||
value: NumberFormat('#.#').format(team.gp),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (team.averageRating != null)
|
||||
Row(
|
||||
mainAxisAlignment: .center,
|
||||
spacing: cardSpacing,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: statWidth,
|
||||
child: _StatCard(context.l10n.averageElo, value: team.averageRating.toString()),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _SectionHeader extends StatelessWidget {
|
||||
const _SectionHeader({required this.title});
|
||||
|
||||
final String title;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ColoredBox(
|
||||
color: ColorScheme.of(context).surfaceDim,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Text(title, style: Theme.of(context).textTheme.bodyLarge),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _PlayerListTile extends StatelessWidget {
|
||||
const _PlayerListTile({
|
||||
required this.tournament,
|
||||
required this.playerResult,
|
||||
required this.index,
|
||||
});
|
||||
|
||||
final BroadcastTournament tournament;
|
||||
final BroadcastPlayerWithOverallResult playerResult;
|
||||
final int index;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final scoreStr = playerResult.score != null
|
||||
? NumberFormat('#.#').format(playerResult.score)
|
||||
: '-';
|
||||
|
||||
final pic = playerResult.player.fideId != null
|
||||
? tournament.photos?.get(playerResult.player.fideId!)
|
||||
: null;
|
||||
|
||||
return ListTile(
|
||||
tileColor: index.isEven ? context.lichessTheme.rowEven : context.lichessTheme.rowOdd,
|
||||
leading: ClipRRect(
|
||||
borderRadius: Styles.thumbnailBorderRadius,
|
||||
child: pic != null
|
||||
? HttpNetworkImageWidget(pic.smallUrl, width: 40, height: 40)
|
||||
: playerResult.player.isBot
|
||||
? Image.asset('assets/images/anon-engine.webp', width: 40, height: 40)
|
||||
: Image.asset('assets/images/anon-face.webp', width: 40, height: 40),
|
||||
),
|
||||
title: BroadcastPlayerWidget(player: playerResult.player, showRating: false),
|
||||
subtitle: playerResult.player.rating != null
|
||||
? Text(playerResult.player.rating.toString())
|
||||
: null,
|
||||
trailing: Column(
|
||||
mainAxisSize: .min,
|
||||
crossAxisAlignment: .end,
|
||||
mainAxisAlignment: .center,
|
||||
children: [
|
||||
Text(
|
||||
'$scoreStr / ${playerResult.played}',
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(fontWeight: .bold),
|
||||
),
|
||||
Text(
|
||||
'Score',
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.7),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
onTap: () {
|
||||
Navigator.of(context).push(
|
||||
BroadcastPlayerResultsScreen.buildRoute(
|
||||
tournament.data.id,
|
||||
playerResult.player,
|
||||
playerResult.player.id!,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _StatCard extends StatelessWidget {
|
||||
const _StatCard(this.stat, {this.value});
|
||||
|
||||
final String stat;
|
||||
final String? value;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return StatCard(
|
||||
contentPadding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 4.0),
|
||||
stat,
|
||||
value: value,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _MatchHistoryTable extends StatelessWidget {
|
||||
const _MatchHistoryTable({required this.team, required this.tournament});
|
||||
|
||||
final BroadcastTeamStanding team;
|
||||
final BroadcastTournament tournament;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Table(
|
||||
columnWidths: const {
|
||||
0: FixedColumnWidth(100),
|
||||
1: FlexColumnWidth(),
|
||||
2: FixedColumnWidth(70),
|
||||
3: FixedColumnWidth(70),
|
||||
},
|
||||
defaultVerticalAlignment: .middle,
|
||||
children: [
|
||||
const TableRow(
|
||||
children: [
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 16.0, vertical: 12.0),
|
||||
child: Text('Round', style: TextStyle(fontWeight: .bold)),
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 12.0),
|
||||
child: Text('Team', style: TextStyle(fontWeight: .bold)),
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 12.0),
|
||||
child: Text(
|
||||
'MP',
|
||||
textAlign: .center,
|
||||
style: TextStyle(fontWeight: .bold),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 16.0, vertical: 12.0),
|
||||
child: Text(
|
||||
'GP',
|
||||
textAlign: .center,
|
||||
style: TextStyle(fontWeight: .bold),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
...team.matches.asMap().entries.map((entry) {
|
||||
final index = entry.key;
|
||||
final match = entry.value;
|
||||
|
||||
Color? pointsColor;
|
||||
if (match.points == '1') {
|
||||
pointsColor = context.lichessColors.good;
|
||||
} else if (match.points == '0') {
|
||||
pointsColor = context.lichessColors.error;
|
||||
}
|
||||
|
||||
return TableRow(
|
||||
decoration: BoxDecoration(
|
||||
color: index.isEven ? context.lichessTheme.rowEven : context.lichessTheme.rowOdd,
|
||||
),
|
||||
children: [
|
||||
_TableTapCell(
|
||||
match: match,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12.0),
|
||||
child: Text((index + 1).toString()),
|
||||
),
|
||||
),
|
||||
TableRowInkWell(
|
||||
onTap: () {
|
||||
Navigator.of(context).push(
|
||||
BroadcastTeamScreen.buildRoute(context, tournament.data.id, match.opponent),
|
||||
);
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12.0),
|
||||
child: Text(match.opponent, maxLines: 2, overflow: .ellipsis),
|
||||
),
|
||||
),
|
||||
_TableTapCell(
|
||||
match: match,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12.0),
|
||||
child: Text(
|
||||
match.mp.toString(),
|
||||
textAlign: .center,
|
||||
style: TextStyle(fontWeight: .bold, color: pointsColor),
|
||||
),
|
||||
),
|
||||
),
|
||||
_TableTapCell(
|
||||
match: match,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12.0),
|
||||
child: Text(NumberFormat('#.#').format(match.gp), textAlign: .center),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _TableTapCell extends StatelessWidget {
|
||||
const _TableTapCell({required this.match, required this.child});
|
||||
|
||||
final BroadcastTeamStandingMatch match;
|
||||
final Widget child;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return TableRowInkWell(
|
||||
onTap: () {
|
||||
Navigator.of(
|
||||
context,
|
||||
).push(BroadcastRoundScreenLoading.buildRoute(match.roundId, initialTab: .teams));
|
||||
},
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,7 @@ import 'package:lichess_mobile/src/theme.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/view/broadcast/broadcast_team_screen.dart';
|
||||
import 'package:lichess_mobile/src/view/engine/engine_gauge.dart';
|
||||
import 'package:visibility_detector/visibility_detector.dart';
|
||||
|
||||
@@ -142,11 +143,18 @@ class _TeamMatchCard extends StatelessWidget {
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
match.team1.name,
|
||||
maxLines: _kTeamNameMaxLines,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
Navigator.of(context).push(
|
||||
BroadcastTeamScreen.buildRoute(context, tournamentId, match.team1.name),
|
||||
);
|
||||
},
|
||||
child: Text(
|
||||
match.team1.name,
|
||||
maxLines: _kTeamNameMaxLines,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
@@ -174,11 +182,18 @@ class _TeamMatchCard extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Text(
|
||||
match.team2.name,
|
||||
maxLines: _kTeamNameMaxLines,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
Navigator.of(context).push(
|
||||
BroadcastTeamScreen.buildRoute(context, tournamentId, match.team2.name),
|
||||
);
|
||||
},
|
||||
child: Text(
|
||||
match.team2.name,
|
||||
maxLines: _kTeamNameMaxLines,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user