From ae384261b268239bafbdee2456126384ff23222c Mon Sep 17 00:00:00 2001 From: Julien <120588494+julien4215@users.noreply.github.com> Date: Sat, 28 Dec 2024 15:14:36 +0100 Subject: [PATCH] Listen to broadcast providers to a lower level in the widget tree and disable action buttons until loading is complete --- lib/src/view/analysis/analysis_layout.dart | 40 +- .../view/broadcast/broadcast_game_screen.dart | 373 +++++++++--------- 2 files changed, 218 insertions(+), 195 deletions(-) diff --git a/lib/src/view/analysis/analysis_layout.dart b/lib/src/view/analysis/analysis_layout.dart index ee43d4a8b3..2f2dcea3e4 100644 --- a/lib/src/view/analysis/analysis_layout.dart +++ b/lib/src/view/analysis/analysis_layout.dart @@ -39,7 +39,12 @@ enum AnalysisTab { /// Indicator for the analysis tab, typically shown in the app bar. class AppBarAnalysisTabIndicator extends StatefulWidget { - const AppBarAnalysisTabIndicator({required this.tabs, required this.controller, super.key}); + const AppBarAnalysisTabIndicator({ + required this.tabs, + required this.controller, + this.enable = true, + super.key, + }); final TabController controller; @@ -49,6 +54,8 @@ class AppBarAnalysisTabIndicator extends StatefulWidget { /// and the length of the [AnalysisLayout.children] list. final List tabs; + final bool enable; + @override State createState() => _AppBarAnalysisTabIndicatorState(); } @@ -87,21 +94,24 @@ class _AppBarAnalysisTabIndicatorState extends State return AppBarIconButton( icon: Icon(widget.tabs[widget.controller.index].icon), semanticsLabel: widget.tabs[widget.controller.index].l10n(context.l10n), - onPressed: () { - showAdaptiveActionSheet( - context: context, - actions: - widget.tabs.map((tab) { - return BottomSheetAction( - leading: Icon(tab.icon), - makeLabel: (context) => Text(tab.l10n(context.l10n)), - onPressed: (_) { - widget.controller.animateTo(widget.tabs.indexOf(tab)); - }, + onPressed: + (widget.enable) + ? () { + showAdaptiveActionSheet( + context: context, + actions: + widget.tabs.map((tab) { + return BottomSheetAction( + leading: Icon(tab.icon), + makeLabel: (context) => Text(tab.l10n(context.l10n)), + onPressed: (_) { + widget.controller.animateTo(widget.tabs.indexOf(tab)); + }, + ); + }).toList(), ); - }).toList(), - ); - }, + } + : null, ); } } diff --git a/lib/src/view/broadcast/broadcast_game_screen.dart b/lib/src/view/broadcast/broadcast_game_screen.dart index 0920393665..5dd1479dde 100644 --- a/lib/src/view/broadcast/broadcast_game_screen.dart +++ b/lib/src/view/broadcast/broadcast_game_screen.dart @@ -74,11 +74,11 @@ class _BroadcastGameScreenState extends ConsumerState @override Widget build(BuildContext context) { - final broadcastRoundGameState = ref.watch( - broadcastRoundGameProvider(widget.roundId, widget.gameId), - ); - final broadcastGameState = ref.watch( - broadcastGameControllerProvider(widget.roundId, widget.gameId), + final hasValue = ref.watch( + broadcastGameControllerProvider( + widget.roundId, + widget.gameId, + ).select((state) => state.hasValue), ); final title = (widget.title != null) @@ -96,10 +96,10 @@ class _BroadcastGameScreenState extends ConsumerState appBar: PlatformAppBar( title: title, actions: [ - AppBarAnalysisTabIndicator(tabs: tabs, controller: _tabController), + AppBarAnalysisTabIndicator(tabs: tabs, controller: _tabController, enable: hasValue), AppBarIconButton( onPressed: - (broadcastGameState.hasValue) + hasValue ? () { pushPlatformRoute( context, @@ -112,19 +112,14 @@ class _BroadcastGameScreenState extends ConsumerState ), ], ), - body: - (broadcastRoundGameState.hasValue && broadcastGameState.hasValue) - ? _Body( - widget.tournamentId, - widget.roundId, - widget.gameId, - widget.tournamentSlug, - widget.roundSlug, - tabController: _tabController, - ) - : (broadcastGameState.hasError || broadcastRoundGameState.hasError) - ? const Center(child: Text('Cannot load broadcast game')) - : const Center(child: CircularProgressIndicator.adaptive()), + body: _Body( + widget.tournamentId, + widget.roundId, + widget.gameId, + widget.tournamentSlug, + widget.roundSlug, + tabController: _tabController, + ), ); } } @@ -148,67 +143,77 @@ class _Body extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final broadcastState = ref.watch(broadcastGameControllerProvider(roundId, gameId)).requireValue; - final analysisPrefs = ref.watch(analysisPreferencesProvider); - final showEvaluationGauge = analysisPrefs.showEvaluationGauge; - final numEvalLines = analysisPrefs.numEvalLines; - - final engineGaugeParams = broadcastState.engineGaugeParams; - final isLocalEvaluationEnabled = broadcastState.isLocalEvaluationEnabled; - final currentNode = broadcastState.currentNode; - - return AnalysisLayout( - tabController: tabController, - boardBuilder: - (context, boardSize, borderRadius) => - _BroadcastBoard(roundId, gameId, boardSize, borderRadius), - boardHeader: _PlayerWidget( - tournamentId: tournamentId, - roundId: roundId, - gameId: gameId, - widgetPosition: _PlayerWidgetPosition.top, - ), - boardFooter: _PlayerWidget( - tournamentId: tournamentId, - roundId: roundId, - gameId: gameId, - widgetPosition: _PlayerWidgetPosition.bottom, - ), - engineGaugeBuilder: - isLocalEvaluationEnabled && showEvaluationGauge - ? (context, orientation) { - return orientation == Orientation.portrait - ? EngineGauge( - displayMode: EngineGaugeDisplayMode.horizontal, - params: engineGaugeParams, - ) - : Container( - clipBehavior: Clip.hardEdge, - decoration: BoxDecoration(borderRadius: BorderRadius.circular(4.0)), - child: EngineGauge( - displayMode: EngineGaugeDisplayMode.vertical, - params: engineGaugeParams, - ), - ); - } - : null, - engineLines: - isLocalEvaluationEnabled && numEvalLines > 0 - ? EngineLines( - clientEval: currentNode.eval, - isGameOver: currentNode.position.isGameOver, - onTapMove: - ref.read(broadcastGameControllerProvider(roundId, gameId).notifier).onUserMove, - ) - : null, - bottomBar: BroadcastGameBottomBar( - roundId: roundId, - gameId: gameId, - tournamentSlug: tournamentSlug, - roundSlug: roundSlug, - ), - children: [_OpeningExplorerTab(roundId, gameId), BroadcastGameTreeView(roundId, gameId)], - ); + final broadcastState = ref.watch(broadcastGameControllerProvider(roundId, gameId)); + + switch (broadcastState) { + case AsyncData(value: final broadcastState): + final analysisPrefs = ref.watch(analysisPreferencesProvider); + final showEvaluationGauge = analysisPrefs.showEvaluationGauge; + final numEvalLines = analysisPrefs.numEvalLines; + + final engineGaugeParams = broadcastState.engineGaugeParams; + final isLocalEvaluationEnabled = broadcastState.isLocalEvaluationEnabled; + final currentNode = broadcastState.currentNode; + + return AnalysisLayout( + tabController: tabController, + boardBuilder: + (context, boardSize, borderRadius) => + _BroadcastBoard(roundId, gameId, boardSize, borderRadius), + boardHeader: _PlayerWidget( + tournamentId: tournamentId, + roundId: roundId, + gameId: gameId, + widgetPosition: _PlayerWidgetPosition.top, + ), + boardFooter: _PlayerWidget( + tournamentId: tournamentId, + roundId: roundId, + gameId: gameId, + widgetPosition: _PlayerWidgetPosition.bottom, + ), + engineGaugeBuilder: + isLocalEvaluationEnabled && showEvaluationGauge + ? (context, orientation) { + return orientation == Orientation.portrait + ? EngineGauge( + displayMode: EngineGaugeDisplayMode.horizontal, + params: engineGaugeParams, + ) + : Container( + clipBehavior: Clip.hardEdge, + decoration: BoxDecoration(borderRadius: BorderRadius.circular(4.0)), + child: EngineGauge( + displayMode: EngineGaugeDisplayMode.vertical, + params: engineGaugeParams, + ), + ); + } + : null, + engineLines: + isLocalEvaluationEnabled && numEvalLines > 0 + ? EngineLines( + clientEval: currentNode.eval, + isGameOver: currentNode.position.isGameOver, + onTapMove: + ref + .read(broadcastGameControllerProvider(roundId, gameId).notifier) + .onUserMove, + ) + : null, + bottomBar: BroadcastGameBottomBar( + roundId: roundId, + gameId: gameId, + tournamentSlug: tournamentSlug, + roundSlug: roundSlug, + ), + children: [_OpeningExplorerTab(roundId, gameId), BroadcastGameTreeView(roundId, gameId)], + ); + case AsyncError(:final error): + return Center(child: Text('Cannot load broadcast game: $error')); + case _: + return const CircularProgressIndicator.adaptive(); + } } } @@ -348,108 +353,116 @@ class _PlayerWidget extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final broadcastGameState = - ref.watch(broadcastGameControllerProvider(roundId, gameId)).requireValue; - final game = ref.watch(broadcastRoundGameProvider(roundId, gameId)).requireValue; - - final isCursorOnLiveMove = - broadcastGameState.currentPath == broadcastGameState.broadcastLivePath; - final sideToMove = broadcastGameState.position.turn; - final side = switch (widgetPosition) { - _PlayerWidgetPosition.bottom => broadcastGameState.pov, - _PlayerWidgetPosition.top => broadcastGameState.pov.opposite, - }; - - final player = game.players[side]!; - final liveClock = isCursorOnLiveMove ? player.clock : null; - final gameStatus = game.status; - - final pastClocks = broadcastGameState.clocks; - final pastClock = (sideToMove == side) ? pastClocks?.parentClock : pastClocks?.clock; - - return GestureDetector( - onTap: () { - pushPlatformRoute( - context, - builder: - (context) => BroadcastPlayerResultsScreen( - tournamentId, - (player.fideId != null) ? player.fideId!.toString() : player.name, - player.title, - player.name, - ), - ); - }, - child: Container( - color: - Theme.of(context).platform == TargetPlatform.iOS - ? Styles.cupertinoCardColor.resolveFrom(context) - : Theme.of(context).colorScheme.surfaceContainer, - padding: const EdgeInsets.only(left: 8.0), - child: Row( - children: [ - if (game.isOver) ...[ - Text( - (gameStatus == BroadcastResult.draw) - ? '½' - : (gameStatus == BroadcastResult.whiteWins) - ? side == Side.white + final game = ref.watch(broadcastRoundGameProvider(roundId, gameId)); + + switch (game) { + case AsyncValue(value: final game?, hasValue: true): + final broadcastGameState = + ref.watch(broadcastGameControllerProvider(roundId, gameId)).requireValue; + + final isCursorOnLiveMove = + broadcastGameState.currentPath == broadcastGameState.broadcastLivePath; + final sideToMove = broadcastGameState.position.turn; + final side = switch (widgetPosition) { + _PlayerWidgetPosition.bottom => broadcastGameState.pov, + _PlayerWidgetPosition.top => broadcastGameState.pov.opposite, + }; + + final player = game.players[side]!; + final liveClock = isCursorOnLiveMove ? player.clock : null; + final gameStatus = game.status; + + final pastClocks = broadcastGameState.clocks; + final pastClock = (sideToMove == side) ? pastClocks?.parentClock : pastClocks?.clock; + + return GestureDetector( + onTap: () { + pushPlatformRoute( + context, + builder: + (context) => BroadcastPlayerResultsScreen( + tournamentId, + (player.fideId != null) ? player.fideId!.toString() : player.name, + player.title, + player.name, + ), + ); + }, + child: Container( + color: + Theme.of(context).platform == TargetPlatform.iOS + ? Styles.cupertinoCardColor.resolveFrom(context) + : Theme.of(context).colorScheme.surfaceContainer, + padding: const EdgeInsets.only(left: 8.0), + child: Row( + children: [ + if (game.isOver) ...[ + Text( + (gameStatus == BroadcastResult.draw) + ? '½' + : (gameStatus == BroadcastResult.whiteWins) + ? side == Side.white + ? '1' + : '0' + : side == Side.black ? '1' - : '0' - : side == Side.black - ? '1' - : '0', - style: const TextStyle().copyWith(fontWeight: FontWeight.bold), - ), - const SizedBox(width: 16.0), - ], - Expanded( - child: BroadcastPlayerWidget( - federation: player.federation, - title: player.title, - name: player.name, - rating: player.rating, - textStyle: const TextStyle().copyWith(fontWeight: FontWeight.bold), - ), - ), - if (liveClock != null || pastClock != null) - Container( - height: kAnalysisBoardHeaderOrFooterHeight, - color: - (side == sideToMove) - ? isCursorOnLiveMove - ? Theme.of(context).colorScheme.tertiaryContainer - : Theme.of(context).colorScheme.secondaryContainer - : Colors.transparent, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 6.0), - child: Center( - child: - liveClock != null - ? CountdownClockBuilder( - timeLeft: liveClock, - active: side == sideToMove, - builder: - (context, timeLeft) => _Clock( - timeLeft: timeLeft, - isSideToMove: side == sideToMove, - isLive: true, - ), - tickInterval: const Duration(seconds: 1), - clockUpdatedAt: game.updatedClockAt, - ) - : _Clock( - timeLeft: pastClock!, - isSideToMove: side == sideToMove, - isLive: false, - ), + : '0', + style: const TextStyle().copyWith(fontWeight: FontWeight.bold), + ), + const SizedBox(width: 16.0), + ], + Expanded( + child: BroadcastPlayerWidget( + federation: player.federation, + title: player.title, + name: player.name, + rating: player.rating, + textStyle: const TextStyle().copyWith(fontWeight: FontWeight.bold), ), ), - ), - ], - ), - ), - ); + if (liveClock != null || pastClock != null) + Container( + height: kAnalysisBoardHeaderOrFooterHeight, + color: + (side == sideToMove) + ? isCursorOnLiveMove + ? Theme.of(context).colorScheme.tertiaryContainer + : Theme.of(context).colorScheme.secondaryContainer + : Colors.transparent, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 6.0), + child: Center( + child: + liveClock != null + ? CountdownClockBuilder( + timeLeft: liveClock, + active: side == sideToMove, + builder: + (context, timeLeft) => _Clock( + timeLeft: timeLeft, + isSideToMove: side == sideToMove, + isLive: true, + ), + tickInterval: const Duration(seconds: 1), + clockUpdatedAt: game.updatedClockAt, + ) + : _Clock( + timeLeft: pastClock!, + isSideToMove: side == sideToMove, + isLive: false, + ), + ), + ), + ), + ], + ), + ), + ); + case AsyncValue(:final error?): + return Text('Cannot load player data: $error'); + case _: + return const SizedBox.shrink(); + } } }