diff --git a/lib/src/view/game/game_common_widgets.dart b/lib/src/view/game/game_common_widgets.dart index d68a17fb7f..6c6b22b79b 100644 --- a/lib/src/view/game/game_common_widgets.dart +++ b/lib/src/view/game/game_common_widgets.dart @@ -7,11 +7,15 @@ import 'package:lichess_mobile/src/model/challenge/challenge.dart'; import 'package:lichess_mobile/src/model/common/http.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/common/time_increment.dart'; +import 'package:lichess_mobile/src/model/game/archived_game.dart'; import 'package:lichess_mobile/src/model/game/game.dart'; import 'package:lichess_mobile/src/model/game/game_share_service.dart'; import 'package:lichess_mobile/src/model/lobby/game_seek.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; +import 'package:lichess_mobile/src/utils/navigation.dart'; import 'package:lichess_mobile/src/utils/share.dart'; +import 'package:lichess_mobile/src/view/game/archived_game_screen.dart'; +import 'package:lichess_mobile/src/view/game/game_screen.dart'; import 'package:lichess_mobile/src/widgets/adaptive_action_sheet.dart'; import 'package:lichess_mobile/src/widgets/adaptive_bottom_sheet.dart'; import 'package:lichess_mobile/src/widgets/buttons.dart'; @@ -24,6 +28,31 @@ import 'ping_rating.dart'; final _gameTitledateFormat = DateFormat.yMMMd(); +void openGameScreen( + LightArchivedGame game, + Side orientation, + BuildContext context, +) { + if (game.variant.isReadSupported) { + pushPlatformRoute( + context, + rootNavigator: true, + builder: (context) => game.fullId != null + ? GameScreen(initialGameId: game.fullId) + : ArchivedGameScreen( + gameData: game, + orientation: orientation, + ), + ); + } else { + showPlatformSnackbar( + context, + 'This variant is not supported yet.', + type: SnackBarType.info, + ); + } +} + class GameAppBar extends ConsumerWidget { const GameAppBar({ this.id, diff --git a/lib/src/view/game/game_list_detail_tile.dart b/lib/src/view/game/game_list_detail_tile.dart new file mode 100644 index 0000000000..1249368125 --- /dev/null +++ b/lib/src/view/game/game_list_detail_tile.dart @@ -0,0 +1,216 @@ +import 'package:dartchess/dartchess.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import 'package:lichess_mobile/src/model/common/chess.dart'; +import 'package:lichess_mobile/src/model/game/archived_game.dart'; +import 'package:lichess_mobile/src/model/game/player.dart'; +import 'package:lichess_mobile/src/styles/styles.dart'; +import 'package:lichess_mobile/src/utils/l10n_context.dart'; +import 'package:lichess_mobile/src/view/game/game_common_widgets.dart'; +import 'package:lichess_mobile/src/view/game/game_list_tile.dart'; +import 'package:lichess_mobile/src/view/game/status_l10n.dart'; +import 'package:lichess_mobile/src/widgets/adaptive_bottom_sheet.dart'; +import 'package:lichess_mobile/src/widgets/board_thumbnail.dart'; +import 'package:lichess_mobile/src/widgets/user_full_name.dart'; + +final _dateFormatter = DateFormat.yMMMd(Intl.getCurrentLocale()).add_Hm(); + +/// A list tile that shows more detailed game info than [GameListTile]. +class GameListDetailTile extends StatelessWidget { + const GameListDetailTile({ + required this.item, + }); + + final LightArchivedGameWithPov item; + + Side get mySide => item.pov; + + @override + Widget build(BuildContext context) { + final (game: game, pov: youAre) = item; + final me = youAre == Side.white ? game.white : game.black; + final opponent = youAre == Side.white ? game.black : game.white; + + final customColors = Theme.of(context).extension(); + + final dateStyle = TextStyle( + color: textShade( + context, + Styles.subtitleOpacity, + ), + fontSize: 13, + ); + + return GestureDetector( + onLongPress: () { + showAdaptiveBottomSheet( + context: context, + useRootNavigator: true, + isDismissible: true, + isScrollControlled: true, + showDragHandle: true, + builder: (context) => GameContextMenu( + game: game, + mySide: mySide, + showGameSummary: false, + ), + ); + }, + onTap: () => openGameScreen(item.game, item.pov, context), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0).add( + const EdgeInsets.only(bottom: 8.0), + ), + child: LayoutBuilder( + builder: (context, constraints) { + return IntrinsicHeight( + child: Row( + mainAxisSize: MainAxisSize.max, + children: [ + if (game.lastFen != null) + BoardThumbnail( + size: constraints.maxWidth / 3, + fen: game.lastFen!, + orientation: mySide, + lastMove: game.lastMove, + ), + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: Column( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text.rich( + TextSpan( + children: [ + WidgetSpan( + child: Icon( + game.perf.icon, + color: dateStyle.color, + size: dateStyle.fontSize, + ), + ), + TextSpan( + text: + ' ${_dateFormatter.format(game.lastMoveAt)}', + style: dateStyle, + ), + ], + ), + ), + UserFullNameWidget( + user: opponent.user, + rating: opponent.rating, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), + if (game.lastFen != null) + Text( + gameStatusL10n( + context, + variant: game.variant, + status: game.status, + lastPosition: Position.setupPosition( + game.variant.rule, + Setup.parseFen(game.lastFen!), + ), + winner: game.winner, + ), + style: TextStyle( + fontSize: 12, + color: game.winner == null + ? customColors?.brag + : game.winner == mySide + ? customColors?.good + : customColors?.error, + ), + ), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _Opening(opening: game.opening), + _ComputerAnalysisResult(analysis: me.analysis), + ], + ), + ], + ), + ), + ), + ], + ), + ); + }, + ), + ), + ); + } +} + +class _Opening extends StatelessWidget { + const _Opening({ + required this.opening, + }); + + final LightOpening? opening; + + @override + Widget build(BuildContext context) { + return opening != null + ? Text( + opening!.name, + maxLines: 2, + style: TextStyle( + color: textShade( + context, + Styles.subtitleOpacity, + ), + fontSize: 10, + ), + overflow: TextOverflow.ellipsis, + ) + : const SizedBox.shrink(); + } +} + +class _ComputerAnalysisResult extends StatelessWidget { + const _ComputerAnalysisResult({ + required this.analysis, + }); + + final PlayerAnalysis? analysis; + + @override + Widget build(BuildContext context) { + final textStyle = TextStyle( + color: textShade( + context, + Styles.subtitleOpacity, + ), + fontSize: 10, + ); + + return analysis != null + ? Row( + children: [ + Icon( + CupertinoIcons.chart_bar_alt_fill, + size: 14, + color: textShade(context, 0.5), + ), + const SizedBox(width: 5), + Text( + analysis?.accuracy != null + ? 'Accuracy: ${analysis?.accuracy}%' + : context.l10n.computerAnalysisAvailable, + style: textStyle, + ), + ], + ) + : const SizedBox.shrink(); + } +} diff --git a/lib/src/view/game/game_list_tile.dart b/lib/src/view/game/game_list_tile.dart index 1c1213c1b0..5aadd8f0fd 100644 --- a/lib/src/view/game/game_list_tile.dart +++ b/lib/src/view/game/game_list_tile.dart @@ -15,8 +15,7 @@ import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; import 'package:lichess_mobile/src/utils/share.dart'; import 'package:lichess_mobile/src/view/analysis/analysis_screen.dart'; -import 'package:lichess_mobile/src/view/game/archived_game_screen.dart'; -import 'package:lichess_mobile/src/view/game/game_screen.dart'; +import 'package:lichess_mobile/src/view/game/game_common_widgets.dart'; import 'package:lichess_mobile/src/view/game/status_l10n.dart'; import 'package:lichess_mobile/src/widgets/adaptive_bottom_sheet.dart'; import 'package:lichess_mobile/src/widgets/board_thumbnail.dart'; @@ -61,13 +60,10 @@ class GameListTile extends StatelessWidget { isDismissible: true, isScrollControlled: true, showDragHandle: true, - builder: (context) => _ContextMenu( + builder: (context) => GameContextMenu( game: game, mySide: mySide, - oppponentTitle: opponentTitle, - icon: icon, - subtitle: subtitle, - trailing: trailing, + showGameSummary: true, ), ); }, @@ -87,23 +83,17 @@ class GameListTile extends StatelessWidget { } } -class _ContextMenu extends ConsumerWidget { - const _ContextMenu({ +class GameContextMenu extends ConsumerWidget { + const GameContextMenu({ required this.game, required this.mySide, - required this.oppponentTitle, - this.icon, - this.subtitle, - this.trailing, + required this.showGameSummary, }); final LightArchivedGame game; final Side mySide; - final IconData? icon; - final Widget oppponentTitle; - final Widget? subtitle; - final Widget? trailing; + final bool showGameSummary; @override Widget build(BuildContext context, WidgetRef ref) { @@ -129,43 +119,78 @@ class _ContextMenu extends ConsumerWidget { ), ), ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0).add( - const EdgeInsets.only(bottom: 8.0), - ), - child: LayoutBuilder( - builder: (context, constraints) { - return IntrinsicHeight( - child: Row( - mainAxisSize: MainAxisSize.max, - children: [ - if (game.lastFen != null) - BoardThumbnail( - size: constraints.maxWidth - - (constraints.maxWidth / 1.618), - fen: game.lastFen!, - orientation: mySide, - lastMove: game.lastMove, - ), - Expanded( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 8.0), - child: Column( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ + if (showGameSummary) + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0).add( + const EdgeInsets.only(bottom: 8.0), + ), + child: LayoutBuilder( + builder: (context, constraints) { + return IntrinsicHeight( + child: Row( + mainAxisSize: MainAxisSize.max, + children: [ + if (game.lastFen != null) + BoardThumbnail( + size: constraints.maxWidth - + (constraints.maxWidth / 1.618), + fen: game.lastFen!, + orientation: mySide, + lastMove: game.lastMove, + ), + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: Column( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '${game.clockDisplay} • ${game.rated ? context.l10n.rated : context.l10n.casual}', + style: const TextStyle( + fontWeight: FontWeight.w500, + ), + ), + Text( + _dateFormatter.format(game.lastMoveAt), + style: TextStyle( + color: textShade( + context, + Styles.subtitleOpacity, + ), + fontSize: 12, + ), + ), + ], + ), + if (game.lastFen != null) Text( - '${game.clockDisplay} • ${game.rated ? context.l10n.rated : context.l10n.casual}', - style: const TextStyle( - fontWeight: FontWeight.w500, + gameStatusL10n( + context, + variant: game.variant, + status: game.status, + lastPosition: Position.setupPosition( + game.variant.rule, + Setup.parseFen(game.lastFen!), + ), + winner: game.winner, + ), + style: TextStyle( + color: game.winner == null + ? customColors?.brag + : game.winner == mySide + ? customColors?.good + : customColors?.error, ), ), + if (game.opening != null) Text( - _dateFormatter.format(game.lastMoveAt), + game.opening!.name, + maxLines: 2, style: TextStyle( color: textShade( context, @@ -173,52 +198,18 @@ class _ContextMenu extends ConsumerWidget { ), fontSize: 12, ), + overflow: TextOverflow.ellipsis, ), - ], - ), - if (game.lastFen != null) - Text( - gameStatusL10n( - context, - variant: game.variant, - status: game.status, - lastPosition: Position.setupPosition( - game.variant.rule, - Setup.parseFen(game.lastFen!), - ), - winner: game.winner, - ), - style: TextStyle( - color: game.winner == null - ? customColors?.brag - : game.winner == mySide - ? customColors?.good - : customColors?.error, - ), - ), - if (game.opening != null) - Text( - game.opening!.name, - maxLines: 2, - style: TextStyle( - color: textShade( - context, - Styles.subtitleOpacity, - ), - fontSize: 12, - ), - overflow: TextOverflow.ellipsis, - ), - ], + ], + ), ), ), - ), - ], - ), - ); - }, + ], + ), + ); + }, + ), ), - ), BottomSheetContextMenuAction( icon: Icons.biotech, onPressed: game.variant.isReadSupported @@ -459,32 +450,7 @@ class ExtendedGameListTile extends StatelessWidget { game: game, mySide: youAre, padding: padding, - onTap: game.variant.isReadSupported - ? () { - pushPlatformRoute( - context, - rootNavigator: true, - builder: (context) => game.fullId != null - ? GameScreen( - initialGameId: game.fullId, - loadingFen: game.lastFen, - loadingLastMove: game.lastMove, - loadingOrientation: youAre, - lastMoveAt: game.lastMoveAt, - ) - : ArchivedGameScreen( - gameData: game, - orientation: youAre, - ), - ); - } - : () { - showPlatformSnackbar( - context, - 'This variant is not supported yet.', - type: SnackBarType.info, - ); - }, + onTap: () => openGameScreen(item.game, item.pov, context), icon: game.perf.icon, opponentTitle: UserFullNameWidget.player( user: opponent.user, diff --git a/lib/src/view/user/game_history_screen.dart b/lib/src/view/user/game_history_screen.dart index 973d16a5f4..02f06c2d28 100644 --- a/lib/src/view/user/game_history_screen.dart +++ b/lib/src/view/user/game_history_screen.dart @@ -1,4 +1,5 @@ import 'package:dartchess/dartchess.dart'; +import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/model/auth/auth_session.dart'; @@ -8,6 +9,7 @@ import 'package:lichess_mobile/src/model/game/game_history.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/utils/l10n_context.dart'; +import 'package:lichess_mobile/src/view/game/game_list_detail_tile.dart'; import 'package:lichess_mobile/src/view/game/game_list_tile.dart'; import 'package:lichess_mobile/src/widgets/adaptive_bottom_sheet.dart'; import 'package:lichess_mobile/src/widgets/buttons.dart'; @@ -16,6 +18,8 @@ import 'package:lichess_mobile/src/widgets/filter.dart'; import 'package:lichess_mobile/src/widgets/list.dart'; import 'package:lichess_mobile/src/widgets/platform_scaffold.dart'; +final _isDetailView = StateProvider((ref) => true); + class GameHistoryScreen extends ConsumerWidget { const GameHistoryScreen({ required this.user, @@ -42,11 +46,6 @@ class GameHistoryScreen extends ConsumerWidget { : Text(filtersInUse.selectionLabel(context)); final filterBtn = AppBarIconButton( icon: Badge.count( - backgroundColor: Theme.of(context).colorScheme.secondary, - textStyle: TextStyle( - color: Theme.of(context).colorScheme.onSecondary, - fontWeight: FontWeight.bold, - ), count: filtersInUse.count, isLabelVisible: filtersInUse.count > 0, child: const Icon(Icons.tune), @@ -54,7 +53,6 @@ class GameHistoryScreen extends ConsumerWidget { semanticsLabel: context.l10n.filterGames, onPressed: () => showAdaptiveBottomSheet( context: context, - isScrollControlled: true, builder: (_) => _FilterGames( filter: ref.read(gameFilterProvider(filter: gameFilter)), user: user, @@ -68,10 +66,22 @@ class GameHistoryScreen extends ConsumerWidget { }), ); + final bool isDetailView = ref.watch(_isDetailView); + final actions = [ + AppBarIconButton( + icon: ref.watch(_isDetailView) + ? const Icon(CupertinoIcons.square_grid_2x2) + : const Icon(CupertinoIcons.rectangle_grid_1x2), + semanticsLabel: 'Switch view', + onPressed: () => ref.read(_isDetailView.notifier).state = !isDetailView, + ), + filterBtn, + ]; + return PlatformScaffold( appBar: PlatformAppBar( title: title, - actions: [filterBtn], + actions: actions, ), body: _Body(user: user, isOnline: isOnline, gameFilter: gameFilter), ); @@ -200,17 +210,20 @@ class _BodyState extends ConsumerState<_Body> { ); } - return ExtendedGameListTile( - item: list[index], - userId: widget.user?.id, - // see: https://github.com/flutter/flutter/blob/master/packages/flutter/lib/src/cupertino/list_tile.dart#L30 for horizontal padding value - padding: Theme.of(context).platform == TargetPlatform.iOS - ? const EdgeInsets.symmetric( - horizontal: 14.0, - vertical: 12.0, - ) - : null, - ); + return ref.watch(_isDetailView) + ? GameListDetailTile(item: list[index]) + : ExtendedGameListTile( + item: list[index], + userId: widget.user?.id, + // see: https://github.com/flutter/flutter/blob/master/packages/flutter/lib/src/cupertino/list_tile.dart#L30 for horizontal padding value + padding: + Theme.of(context).platform == TargetPlatform.iOS + ? const EdgeInsets.symmetric( + horizontal: 14.0, + vertical: 12.0, + ) + : null, + ); }, ), ); diff --git a/lib/src/widgets/user_full_name.dart b/lib/src/widgets/user_full_name.dart index 87fc25dc85..4d3ed23bd2 100644 --- a/lib/src/widgets/user_full_name.dart +++ b/lib/src/widgets/user_full_name.dart @@ -123,7 +123,7 @@ class UserFullNameWidget extends ConsumerWidget { ], if (shouldShowRating && ratingStr != null) ...[ const SizedBox(width: 5), - Text(ratingStr), + Text(ratingStr, style: style), ], ], );