From c6293bdd67dbe7efe59d17ecdde9ba78946bc548 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Wed, 20 Nov 2024 14:53:03 +0100 Subject: [PATCH 01/23] Wip on analysis tabs --- .../model/analysis/analysis_controller.dart | 3 + lib/src/view/analysis/analysis_layout.dart | 251 ++++++++++++++ lib/src/view/analysis/analysis_screen.dart | 317 +++++------------- lib/src/view/analysis/tree_view.dart | 18 +- lib/src/view/engine/engine_gauge.dart | 4 +- 5 files changed, 340 insertions(+), 253 deletions(-) create mode 100644 lib/src/view/analysis/analysis_layout.dart diff --git a/lib/src/model/analysis/analysis_controller.dart b/lib/src/model/analysis/analysis_controller.dart index 830225ab88..3f937ca4c7 100644 --- a/lib/src/model/analysis/analysis_controller.dart +++ b/lib/src/model/analysis/analysis_controller.dart @@ -52,6 +52,9 @@ class AnalysisOptions with _$AnalysisOptions { ({PlayerAnalysis white, PlayerAnalysis black})? serverAnalysis, }) = _AnalysisOptions; + bool get canShowGameSummary => + serverAnalysis != null || id != standaloneAnalysisId; + /// Whether the analysis is for a lichess game. bool get isLichessGameAnalysis => gameAnyId != null; diff --git a/lib/src/view/analysis/analysis_layout.dart b/lib/src/view/analysis/analysis_layout.dart new file mode 100644 index 0000000000..fbd27d9898 --- /dev/null +++ b/lib/src/view/analysis/analysis_layout.dart @@ -0,0 +1,251 @@ +import 'package:flutter/material.dart'; +import 'package:lichess_mobile/src/constants.dart'; +import 'package:lichess_mobile/src/utils/screen.dart'; +import 'package:lichess_mobile/src/widgets/adaptive_action_sheet.dart'; +import 'package:lichess_mobile/src/widgets/buttons.dart'; +import 'package:lichess_mobile/src/widgets/platform.dart'; + +typedef BoardBuilder = Widget Function( + BuildContext context, + double boardSize, + BorderRadiusGeometry? borderRadius, +); + +typedef EngineGaugeBuilder = Widget Function( + BuildContext context, + Orientation orientation, +); + +class AnalysisTab { + const AnalysisTab({ + required this.title, + required this.icon, + }); + + final String title; + final IconData icon; +} + +/// 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, + }); + + final TabController controller; + + /// Typically a list of two or more [AnalysisTab] widgets. + /// + /// The length of this list must match the [controller]'s [TabController.length] + /// and the length of the [AnalysisLayout.children] list. + final List tabs; + + @override + State createState() => + _AppBarAnalysisTabIndicatorState(); +} + +class _AppBarAnalysisTabIndicatorState + extends State { + @override + void didChangeDependencies() { + super.didChangeDependencies(); + widget.controller.addListener(_listener); + } + + @override + void dispose() { + widget.controller.removeListener(_listener); + super.dispose(); + } + + void _listener() { + setState(() {}); + } + + @override + Widget build(BuildContext context) { + return AppBarIconButton( + icon: Icon(widget.tabs[widget.controller.index].icon), + semanticsLabel: widget.tabs[widget.controller.index].title, + onPressed: () { + showAdaptiveActionSheet( + context: context, + actions: widget.tabs.map((tab) { + return BottomSheetAction( + leading: Icon(tab.icon), + makeLabel: (_) => Text(tab.title), + onPressed: (_) { + widget.controller.animateTo(widget.tabs.indexOf(tab)); + Navigator.of(context).pop(); + }, + ); + }).toList(), + ); + }, + ); + } +} + +/// Layout for the analysis and similar screens (study, broadcast, etc.). +class AnalysisLayout extends StatelessWidget { + const AnalysisLayout({ + required this.tabController, + required this.boardBuilder, + required this.children, + this.engineGaugeBuilder, + this.engineLines, + this.bottomBar, + super.key, + }); + + final TabController tabController; + + /// The builder for the board widget. + final BoardBuilder boardBuilder; + + /// The children of the tab bar view. + final List children; + + final EngineGaugeBuilder? engineGaugeBuilder; + final Widget? engineLines; + final Widget? bottomBar; + + @override + Widget build(BuildContext context) { + return Column( + children: [ + Expanded( + child: SafeArea( + bottom: false, + child: LayoutBuilder( + builder: (context, constraints) { + final aspectRatio = constraints.biggest.aspectRatio; + final defaultBoardSize = constraints.biggest.shortestSide; + final isTablet = isTabletOrLarger(context); + final remainingHeight = + constraints.maxHeight - defaultBoardSize; + final isSmallScreen = + remainingHeight < kSmallRemainingHeightLeftBoardThreshold; + final boardSize = isTablet || isSmallScreen + ? defaultBoardSize - kTabletBoardTableSidePadding * 2 + : defaultBoardSize; + + const tabletBoardRadius = + BorderRadius.all(Radius.circular(4.0)); + + // If the aspect ratio is greater than 1, we are in landscape mode. + if (aspectRatio > 1) { + return Row( + mainAxisSize: MainAxisSize.max, + children: [ + Padding( + padding: const EdgeInsets.only( + left: kTabletBoardTableSidePadding, + top: kTabletBoardTableSidePadding, + bottom: kTabletBoardTableSidePadding, + ), + child: Row( + children: [ + boardBuilder( + context, + boardSize, + isTablet ? tabletBoardRadius : null, + ), + if (engineGaugeBuilder != null) ...[ + const SizedBox(width: 4.0), + engineGaugeBuilder!( + context, + Orientation.landscape, + ), + ], + ], + ), + ), + Flexible( + fit: FlexFit.loose, + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + if (engineLines != null) + Padding( + padding: const EdgeInsets.all( + kTabletBoardTableSidePadding, + ), + child: engineLines, + ), + Expanded( + child: PlatformCard( + clipBehavior: Clip.hardEdge, + borderRadius: const BorderRadius.all( + Radius.circular(4.0), + ), + margin: const EdgeInsets.all( + kTabletBoardTableSidePadding, + ), + semanticContainer: false, + child: TabBarView( + controller: tabController, + children: children, + ), + ), + ), + ], + ), + ), + ], + ); + } + // If the aspect ratio is less than 1, we are in portrait mode. + else { + return Column( + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.max, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + if (engineGaugeBuilder != null) + engineGaugeBuilder!( + context, + Orientation.portrait, + ), + if (engineLines != null) engineLines!, + if (isTablet) + Padding( + padding: const EdgeInsets.all( + kTabletBoardTableSidePadding, + ), + child: boardBuilder( + context, + boardSize, + tabletBoardRadius, + ), + ) + else + boardBuilder(context, boardSize, null), + Expanded( + child: Padding( + padding: isTablet + ? const EdgeInsets.symmetric( + horizontal: kTabletBoardTableSidePadding, + ) + : EdgeInsets.zero, + child: TabBarView( + controller: tabController, + children: children, + ), + ), + ), + ], + ); + } + }, + ), + ), + ), + if (bottomBar != null) bottomBar!, + ], + ); + } +} diff --git a/lib/src/view/analysis/analysis_screen.dart b/lib/src/view/analysis/analysis_screen.dart index a87c4155fa..d663a391b2 100644 --- a/lib/src/view/analysis/analysis_screen.dart +++ b/lib/src/view/analysis/analysis_screen.dart @@ -7,7 +7,6 @@ import 'package:fl_chart/fl_chart.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:lichess_mobile/src/constants.dart'; import 'package:lichess_mobile/src/model/analysis/analysis_controller.dart'; import 'package:lichess_mobile/src/model/analysis/analysis_preferences.dart'; import 'package:lichess_mobile/src/model/analysis/server_analysis_service.dart'; @@ -21,11 +20,10 @@ import 'package:lichess_mobile/src/model/game/game_share_service.dart'; import 'package:lichess_mobile/src/network/connectivity.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/l10n_context.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; -import 'package:lichess_mobile/src/utils/screen.dart'; import 'package:lichess_mobile/src/utils/string.dart'; +import 'package:lichess_mobile/src/view/analysis/analysis_layout.dart'; import 'package:lichess_mobile/src/view/analysis/analysis_share_screen.dart'; import 'package:lichess_mobile/src/view/board_editor/board_editor_screen.dart'; import 'package:lichess_mobile/src/view/engine/engine_gauge.dart'; @@ -38,7 +36,6 @@ import 'package:lichess_mobile/src/widgets/bottom_bar_button.dart'; import 'package:lichess_mobile/src/widgets/buttons.dart'; 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:popover/popover.dart'; @@ -121,7 +118,7 @@ class _LoadGame extends ConsumerWidget { } } -class _LoadedAnalysisScreen extends ConsumerWidget { +class _LoadedAnalysisScreen extends ConsumerStatefulWidget { const _LoadedAnalysisScreen({ required this.options, required this.pgn, @@ -134,30 +131,60 @@ class _LoadedAnalysisScreen extends ConsumerWidget { final bool enableDrawingShapes; @override - Widget build(BuildContext context, WidgetRef ref) { - return ConsumerPlatformWidget( - androidBuilder: _androidBuilder, - iosBuilder: _iosBuilder, - ref: ref, - ); + ConsumerState<_LoadedAnalysisScreen> createState() => + _LoadedAnalysisScreenState(); +} + +class _LoadedAnalysisScreenState extends ConsumerState<_LoadedAnalysisScreen> + with SingleTickerProviderStateMixin { + late final TabController _tabController; + late final List tabs; + + @override + void initState() { + super.initState(); + tabs = [ + const AnalysisTab( + title: 'Moves', + icon: LichessIcons.flow_cascade, + ), + if (widget.options.canShowGameSummary) + const AnalysisTab( + title: 'Summary', + icon: Icons.area_chart, + ), + ]; + + _tabController = TabController(vsync: this, length: tabs.length); } - Widget _androidBuilder(BuildContext context, WidgetRef ref) { - final ctrlProvider = analysisControllerProvider(pgn, options); + @override + void dispose() { + _tabController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final ctrlProvider = analysisControllerProvider(widget.pgn, widget.options); return PlatformScaffold( resizeToAvoidBottomInset: false, appBar: PlatformAppBar( - title: _Title(options: options), + title: _Title(options: widget.options), actions: [ _EngineDepth(ctrlProvider), + AppBarAnalysisTabIndicator( + tabs: tabs, + controller: _tabController, + ), AppBarIconButton( onPressed: () => showAdaptiveBottomSheet( context: context, isScrollControlled: true, showDragHandle: true, isDismissible: true, - builder: (_) => AnalysisSettings(pgn, options), + builder: (_) => AnalysisSettings(widget.pgn, widget.options), ), semanticsLabel: context.l10n.settingsSettings, icon: const Icon(Icons.settings), @@ -165,43 +192,10 @@ class _LoadedAnalysisScreen extends ConsumerWidget { ], ), body: _Body( - pgn: pgn, - options: options, - enableDrawingShapes: enableDrawingShapes, - ), - ); - } - - Widget _iosBuilder(BuildContext context, WidgetRef ref) { - final ctrlProvider = analysisControllerProvider(pgn, options); - - return CupertinoPageScaffold( - resizeToAvoidBottomInset: false, - navigationBar: CupertinoNavigationBar( - padding: Styles.cupertinoAppBarTrailingWidgetPadding, - middle: _Title(options: options), - trailing: Row( - mainAxisSize: MainAxisSize.min, - children: [ - _EngineDepth(ctrlProvider), - AppBarIconButton( - onPressed: () => showAdaptiveBottomSheet( - context: context, - isScrollControlled: true, - showDragHandle: true, - isDismissible: true, - builder: (_) => AnalysisSettings(pgn, options), - ), - semanticsLabel: context.l10n.settingsSettings, - icon: const Icon(Icons.settings), - ), - ], - ), - ), - child: _Body( - pgn: pgn, - options: options, - enableDrawingShapes: enableDrawingShapes, + controller: _tabController, + pgn: widget.pgn, + options: widget.options, + enableDrawingShapes: widget.enableDrawingShapes, ), ); } @@ -230,11 +224,13 @@ class _Title extends StatelessWidget { class _Body extends ConsumerWidget { const _Body({ + required this.controller, required this.pgn, required this.options, required this.enableDrawingShapes, }); + final TabController controller; final String pgn; final AnalysisOptions options; final bool enableDrawingShapes; @@ -255,156 +251,37 @@ class _Body extends ConsumerWidget { final hasEval = ref.watch(ctrlProvider.select((value) => value.hasAvailableEval)); - final displayMode = - ref.watch(ctrlProvider.select((value) => value.displayMode)); - final currentNode = ref.watch( ctrlProvider.select((value) => value.currentNode), ); - return Column( + return AnalysisLayout( + tabController: controller, + boardBuilder: (context, boardSize, borderRadius) => AnalysisBoard( + pgn, + options, + boardSize, + borderRadius: borderRadius, + enableDrawingShapes: enableDrawingShapes, + ), + engineGaugeBuilder: hasEval && showEvaluationGauge + ? (context, orientation) { + return orientation == Orientation.portrait + ? _EngineGaugeHorizontal(ctrlProvider) + : _EngineGaugeVertical(ctrlProvider); + } + : null, + engineLines: isEngineAvailable + ? EngineLines( + onTapMove: ref.read(ctrlProvider.notifier).onUserMove, + clientEval: currentNode.eval, + isGameOver: currentNode.position.isGameOver, + ) + : null, + bottomBar: _BottomBar(pgn: pgn, options: options), children: [ - Expanded( - child: SafeArea( - bottom: false, - child: LayoutBuilder( - builder: (context, constraints) { - final aspectRatio = constraints.biggest.aspectRatio; - final defaultBoardSize = constraints.biggest.shortestSide; - final isTablet = isTabletOrLarger(context); - final remainingHeight = - constraints.maxHeight - defaultBoardSize; - final isSmallScreen = - remainingHeight < kSmallRemainingHeightLeftBoardThreshold; - final boardSize = isTablet || isSmallScreen - ? defaultBoardSize - kTabletBoardTableSidePadding * 2 - : defaultBoardSize; - - const tabletBoardRadius = - BorderRadius.all(Radius.circular(4.0)); - - final display = switch (displayMode) { - DisplayMode.summary => ServerAnalysisSummary(pgn, options), - DisplayMode.moves => AnalysisTreeView( - pgn, - options, - aspectRatio > 1 - ? Orientation.landscape - : Orientation.portrait, - ), - }; - - // If the aspect ratio is greater than 1, we are in landscape mode. - if (aspectRatio > 1) { - return Row( - mainAxisSize: MainAxisSize.max, - children: [ - Padding( - padding: const EdgeInsets.only( - left: kTabletBoardTableSidePadding, - top: kTabletBoardTableSidePadding, - bottom: kTabletBoardTableSidePadding, - ), - child: Row( - children: [ - AnalysisBoard( - pgn, - options, - boardSize, - borderRadius: isTablet ? tabletBoardRadius : null, - enableDrawingShapes: enableDrawingShapes, - ), - if (hasEval && showEvaluationGauge) ...[ - const SizedBox(width: 4.0), - _EngineGaugeVertical(ctrlProvider), - ], - ], - ), - ), - Flexible( - fit: FlexFit.loose, - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - if (isEngineAvailable) - Padding( - padding: const EdgeInsets.all( - kTabletBoardTableSidePadding, - ), - child: EngineLines( - onTapMove: ref - .read(ctrlProvider.notifier) - .onUserMove, - clientEval: currentNode.eval, - isGameOver: currentNode.position.isGameOver, - ), - ), - Expanded( - child: PlatformCard( - clipBehavior: Clip.hardEdge, - borderRadius: const BorderRadius.all( - Radius.circular(4.0), - ), - margin: const EdgeInsets.all( - kTabletBoardTableSidePadding, - ), - semanticContainer: false, - child: display, - ), - ), - ], - ), - ), - ], - ); - } - // If the aspect ratio is less than 1, we are in portrait mode. - else { - return Column( - mainAxisAlignment: MainAxisAlignment.center, - mainAxisSize: MainAxisSize.max, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - _ColumnTopTable(ctrlProvider), - if (isTablet) - Padding( - padding: const EdgeInsets.all( - kTabletBoardTableSidePadding, - ), - child: AnalysisBoard( - pgn, - options, - boardSize, - borderRadius: isTablet ? tabletBoardRadius : null, - enableDrawingShapes: enableDrawingShapes, - ), - ) - else - AnalysisBoard( - pgn, - options, - boardSize, - borderRadius: isTablet ? tabletBoardRadius : null, - enableDrawingShapes: enableDrawingShapes, - ), - Expanded( - child: Padding( - padding: isTablet - ? const EdgeInsets.symmetric( - horizontal: kTabletBoardTableSidePadding, - ) - : EdgeInsets.zero, - child: display, - ), - ), - ], - ); - } - }, - ), - ), - ), - _BottomBar(pgn: pgn, options: options), + AnalysisTreeView(pgn, options), + if (options.canShowGameSummary) ServerAnalysisSummary(pgn, options), ], ); } @@ -432,37 +309,19 @@ class _EngineGaugeVertical extends ConsumerWidget { } } -class _ColumnTopTable extends ConsumerWidget { - const _ColumnTopTable(this.ctrlProvider); +class _EngineGaugeHorizontal extends ConsumerWidget { + const _EngineGaugeHorizontal(this.ctrlProvider); final AnalysisControllerProvider ctrlProvider; @override Widget build(BuildContext context, WidgetRef ref) { final analysisState = ref.watch(ctrlProvider); - final showEvaluationGauge = ref.watch( - analysisPreferencesProvider.select((p) => p.showEvaluationGauge), - ); - return analysisState.hasAvailableEval - ? Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (showEvaluationGauge) - EngineGauge( - displayMode: EngineGaugeDisplayMode.horizontal, - params: analysisState.engineGaugeParams, - ), - if (analysisState.isEngineAvailable) - EngineLines( - clientEval: analysisState.currentNode.eval, - isGameOver: analysisState.currentNode.position.isGameOver, - onTapMove: ref.read(ctrlProvider.notifier).onUserMove, - ), - ], - ) - : kEmptyWidget; + return EngineGauge( + displayMode: EngineGaugeDisplayMode.horizontal, + params: analysisState.engineGaugeParams, + ); } } @@ -491,22 +350,6 @@ class _BottomBar extends ConsumerWidget { }, icon: Icons.menu, ), - if (analysisState.canShowGameSummary) - BottomBarButton( - // TODO: l10n - label: analysisState.displayMode == DisplayMode.summary - ? 'Moves' - : 'Summary', - onTap: () { - final newMode = analysisState.displayMode == DisplayMode.summary - ? DisplayMode.moves - : DisplayMode.summary; - ref.read(ctrlProvider.notifier).setDisplayMode(newMode); - }, - icon: analysisState.displayMode == DisplayMode.summary - ? LichessIcons.flow_cascade - : Icons.area_chart, - ), BottomBarButton( label: context.l10n.openingExplorer, onTap: isOnline diff --git a/lib/src/view/analysis/tree_view.dart b/lib/src/view/analysis/tree_view.dart index 1c132b65bf..69c5b86978 100644 --- a/lib/src/view/analysis/tree_view.dart +++ b/lib/src/view/analysis/tree_view.dart @@ -12,12 +12,10 @@ class AnalysisTreeView extends ConsumerWidget { const AnalysisTreeView( this.pgn, this.options, - this.displayMode, ); final String pgn; final AnalysisOptions options; - final Orientation displayMode; @override Widget build(BuildContext context, WidgetRef ref) { @@ -33,10 +31,7 @@ class AnalysisTreeView extends ConsumerWidget { slivers: [ if (kOpeningAllowedVariants.contains(options.variant)) SliverPersistentHeader( - delegate: _OpeningHeaderDelegate( - ctrlProvider, - displayMode: displayMode, - ), + delegate: _OpeningHeaderDelegate(ctrlProvider), ), SliverFillRemaining( hasScrollBody: false, @@ -53,13 +48,9 @@ class AnalysisTreeView extends ConsumerWidget { } class _OpeningHeaderDelegate extends SliverPersistentHeaderDelegate { - const _OpeningHeaderDelegate( - this.ctrlProvider, { - required this.displayMode, - }); + const _OpeningHeaderDelegate(this.ctrlProvider); final AnalysisControllerProvider ctrlProvider; - final Orientation displayMode; @override Widget build( @@ -67,7 +58,7 @@ class _OpeningHeaderDelegate extends SliverPersistentHeaderDelegate { double shrinkOffset, bool overlapsContent, ) { - return _Opening(ctrlProvider, displayMode); + return _Opening(ctrlProvider); } @override @@ -82,10 +73,9 @@ class _OpeningHeaderDelegate extends SliverPersistentHeaderDelegate { } class _Opening extends ConsumerWidget { - const _Opening(this.ctrlProvider, this.displayMode); + const _Opening(this.ctrlProvider); final AnalysisControllerProvider ctrlProvider; - final Orientation displayMode; @override Widget build(BuildContext context, WidgetRef ref) { diff --git a/lib/src/view/engine/engine_gauge.dart b/lib/src/view/engine/engine_gauge.dart index 4d35a79bb5..d1ef7ae79b 100644 --- a/lib/src/view/engine/engine_gauge.dart +++ b/lib/src/view/engine/engine_gauge.dart @@ -8,8 +8,8 @@ import 'package:lichess_mobile/src/model/settings/brightness.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; -const double kEvalGaugeSize = 26.0; -const double kEvalGaugeFontSize = 11.0; +const double kEvalGaugeSize = 24.0; +const double kEvalGaugeFontSize = 10.0; const Color _kEvalGaugeBackgroundColor = Color(0xFF444444); const Color _kEvalGaugeValueColorDarkBg = Color(0xEEEEEEEE); const Color _kEvalGaugeValueColorLightBg = Color(0xFFFFFFFF); From dbf647de9c321d150260a019706e072283f65535 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Wed, 20 Nov 2024 16:29:12 +0100 Subject: [PATCH 02/23] More wip: analysis and explorer widget refactoring --- lib/src/view/analysis/analysis_screen.dart | 708 +----------------- lib/src/view/analysis/server_analysis.dart | 501 +++++++++++++ lib/src/view/engine/engine_depth.dart | 117 +++ .../opening_explorer_screen.dart | 514 +------------ .../opening_explorer_widgets.dart | 504 +++++++++++++ 5 files changed, 1154 insertions(+), 1190 deletions(-) create mode 100644 lib/src/view/analysis/server_analysis.dart create mode 100644 lib/src/view/engine/engine_depth.dart create mode 100644 lib/src/view/opening_explorer/opening_explorer_widgets.dart diff --git a/lib/src/view/analysis/analysis_screen.dart b/lib/src/view/analysis/analysis_screen.dart index d663a391b2..886fb8005d 100644 --- a/lib/src/view/analysis/analysis_screen.dart +++ b/lib/src/view/analysis/analysis_screen.dart @@ -1,43 +1,30 @@ -import 'dart:math' as math; - -import 'package:collection/collection.dart'; -import 'package:dartchess/dartchess.dart'; -import 'package:fast_immutable_collections/fast_immutable_collections.dart'; -import 'package:fl_chart/fl_chart.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/model/analysis/analysis_controller.dart'; import 'package:lichess_mobile/src/model/analysis/analysis_preferences.dart'; -import 'package:lichess_mobile/src/model/analysis/server_analysis_service.dart'; -import 'package:lichess_mobile/src/model/auth/auth_session.dart'; import 'package:lichess_mobile/src/model/common/chess.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; -import 'package:lichess_mobile/src/model/engine/engine.dart'; -import 'package:lichess_mobile/src/model/engine/evaluation_service.dart'; import 'package:lichess_mobile/src/model/game/game_repository_providers.dart'; import 'package:lichess_mobile/src/model/game/game_share_service.dart'; -import 'package:lichess_mobile/src/network/connectivity.dart'; import 'package:lichess_mobile/src/network/http.dart'; import 'package:lichess_mobile/src/styles/lichess_icons.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; -import 'package:lichess_mobile/src/utils/string.dart'; import 'package:lichess_mobile/src/view/analysis/analysis_layout.dart'; import 'package:lichess_mobile/src/view/analysis/analysis_share_screen.dart'; +import 'package:lichess_mobile/src/view/analysis/server_analysis.dart'; import 'package:lichess_mobile/src/view/board_editor/board_editor_screen.dart'; +import 'package:lichess_mobile/src/view/engine/engine_depth.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_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/bottom_bar.dart'; import 'package:lichess_mobile/src/widgets/bottom_bar_button.dart'; import 'package:lichess_mobile/src/widgets/buttons.dart'; import 'package:lichess_mobile/src/widgets/feedback.dart'; -import 'package:lichess_mobile/src/widgets/list.dart'; import 'package:lichess_mobile/src/widgets/platform_scaffold.dart'; -import 'package:popover/popover.dart'; import '../../utils/share.dart'; import 'analysis_board.dart'; @@ -167,13 +154,15 @@ class _LoadedAnalysisScreenState extends ConsumerState<_LoadedAnalysisScreen> @override Widget build(BuildContext context) { final ctrlProvider = analysisControllerProvider(widget.pgn, widget.options); + final currentNodeEval = + ref.watch(ctrlProvider.select((value) => value.currentNode.eval)); return PlatformScaffold( resizeToAvoidBottomInset: false, appBar: PlatformAppBar( title: _Title(options: widget.options), actions: [ - _EngineDepth(ctrlProvider), + EngineDepth(defaultEval: currentNodeEval), AppBarAnalysisTabIndicator( tabs: tabs, controller: _tabController, @@ -237,23 +226,16 @@ class _Body extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final ctrlProvider = analysisControllerProvider(pgn, options); final showEvaluationGauge = ref.watch( analysisPreferencesProvider.select((value) => value.showEvaluationGauge), ); - final isEngineAvailable = ref.watch( - ctrlProvider.select( - (value) => value.isEngineAvailable, - ), - ); - - final hasEval = - ref.watch(ctrlProvider.select((value) => value.hasAvailableEval)); + final ctrlProvider = analysisControllerProvider(pgn, options); + final analysisState = ref.watch(ctrlProvider); - final currentNode = ref.watch( - ctrlProvider.select((value) => value.currentNode), - ); + final isEngineAvailable = analysisState.isEngineAvailable; + final hasEval = analysisState.hasAvailableEval; + final currentNode = analysisState.currentNode; return AnalysisLayout( tabController: controller, @@ -267,8 +249,20 @@ class _Body extends ConsumerWidget { engineGaugeBuilder: hasEval && showEvaluationGauge ? (context, orientation) { return orientation == Orientation.portrait - ? _EngineGaugeHorizontal(ctrlProvider) - : _EngineGaugeVertical(ctrlProvider); + ? EngineGauge( + displayMode: EngineGaugeDisplayMode.horizontal, + params: analysisState.engineGaugeParams, + ) + : Container( + clipBehavior: Clip.hardEdge, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(4.0), + ), + child: EngineGauge( + displayMode: EngineGaugeDisplayMode.vertical, + params: analysisState.engineGaugeParams, + ), + ); } : null, engineLines: isEngineAvailable @@ -287,44 +281,6 @@ class _Body extends ConsumerWidget { } } -class _EngineGaugeVertical extends ConsumerWidget { - const _EngineGaugeVertical(this.ctrlProvider); - - final AnalysisControllerProvider ctrlProvider; - - @override - Widget build(BuildContext context, WidgetRef ref) { - final analysisState = ref.watch(ctrlProvider); - - return Container( - clipBehavior: Clip.hardEdge, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(4.0), - ), - child: EngineGauge( - displayMode: EngineGaugeDisplayMode.vertical, - params: analysisState.engineGaugeParams, - ), - ); - } -} - -class _EngineGaugeHorizontal extends ConsumerWidget { - const _EngineGaugeHorizontal(this.ctrlProvider); - - final AnalysisControllerProvider ctrlProvider; - - @override - Widget build(BuildContext context, WidgetRef ref) { - final analysisState = ref.watch(ctrlProvider); - - return EngineGauge( - displayMode: EngineGaugeDisplayMode.horizontal, - params: analysisState.engineGaugeParams, - ); - } -} - class _BottomBar extends ConsumerWidget { const _BottomBar({ required this.pgn, @@ -338,8 +294,6 @@ class _BottomBar extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final ctrlProvider = analysisControllerProvider(pgn, options); final analysisState = ref.watch(ctrlProvider); - final isOnline = - ref.watch(connectivityChangesProvider).valueOrNull?.isOnline ?? false; return BottomBar( children: [ @@ -350,22 +304,6 @@ class _BottomBar extends ConsumerWidget { }, icon: Icons.menu, ), - BottomBarButton( - label: context.l10n.openingExplorer, - onTap: isOnline - ? () { - pushPlatformRoute( - context, - title: context.l10n.openingExplorer, - builder: (_) => OpeningExplorerScreen( - pgn: ref.read(ctrlProvider.notifier).makeCurrentNodePgn(), - options: analysisState.openingExplorerOptions, - ), - ); - } - : null, - icon: Icons.explore, - ), RepeatButton( onLongPress: analysisState.canGoBack ? () => _moveBackward(ref) : null, @@ -485,601 +423,3 @@ class _BottomBar extends ConsumerWidget { ); } } - -class _EngineDepth extends ConsumerWidget { - const _EngineDepth(this.ctrlProvider); - - final AnalysisControllerProvider ctrlProvider; - - @override - Widget build(BuildContext context, WidgetRef ref) { - final isEngineAvailable = ref.watch( - ctrlProvider.select( - (value) => value.isEngineAvailable, - ), - ); - final currentNode = ref.watch( - ctrlProvider.select((value) => value.currentNode), - ); - final depth = ref.watch( - engineEvaluationProvider.select((value) => value.eval?.depth), - ) ?? - currentNode.eval?.depth; - - return isEngineAvailable && depth != null - ? AppBarTextButton( - onPressed: () { - showPopover( - context: context, - bodyBuilder: (context) { - return _StockfishInfo(currentNode); - }, - direction: PopoverDirection.top, - width: 240, - backgroundColor: - Theme.of(context).platform == TargetPlatform.android - ? Theme.of(context).dialogBackgroundColor - : CupertinoDynamicColor.resolve( - CupertinoColors.tertiarySystemBackground, - context, - ), - transitionDuration: Duration.zero, - popoverTransitionBuilder: (_, child) => child, - ); - }, - child: RepaintBoundary( - child: Container( - width: 20.0, - height: 20.0, - padding: const EdgeInsets.all(2.0), - decoration: BoxDecoration( - color: Theme.of(context).platform == TargetPlatform.android - ? Theme.of(context).colorScheme.secondary - : CupertinoTheme.of(context).primaryColor, - borderRadius: BorderRadius.circular(4.0), - ), - child: FittedBox( - fit: BoxFit.contain, - child: Text( - '${math.min(99, depth)}', - style: TextStyle( - color: Theme.of(context).platform == - TargetPlatform.android - ? Theme.of(context).colorScheme.onSecondary - : CupertinoTheme.of(context).primaryContrastingColor, - fontFeatures: const [ - FontFeature.tabularFigures(), - ], - ), - ), - ), - ), - ), - ) - : const SizedBox.shrink(); - } -} - -class _StockfishInfo extends ConsumerWidget { - const _StockfishInfo(this.currentNode); - - final AnalysisCurrentNode currentNode; - - @override - Widget build(BuildContext context, WidgetRef ref) { - final (engineName: engineName, eval: eval, state: engineState) = - ref.watch(engineEvaluationProvider); - - final currentEval = eval ?? currentNode.eval; - - final knps = engineState == EngineState.computing - ? ', ${eval?.knps.round()}kn/s' - : ''; - final depth = currentEval?.depth ?? 0; - final maxDepth = math.max(depth, kMaxEngineDepth); - - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - PlatformListTile( - leading: Image.asset( - 'assets/images/stockfish/icon.png', - width: 44, - height: 44, - ), - title: Text(engineName), - subtitle: Text( - context.l10n.depthX( - '$depth/$maxDepth$knps', - ), - ), - ), - ], - ); - } -} - -class ServerAnalysisSummary extends ConsumerWidget { - const ServerAnalysisSummary(this.pgn, this.options); - - final String pgn; - final AnalysisOptions options; - - @override - Widget build(BuildContext context, WidgetRef ref) { - final ctrlProvider = analysisControllerProvider(pgn, options); - final playersAnalysis = - ref.watch(ctrlProvider.select((value) => value.playersAnalysis)); - final pgnHeaders = - ref.watch(ctrlProvider.select((value) => value.pgnHeaders)); - final currentGameAnalysis = ref.watch(currentAnalysisProvider); - - return playersAnalysis != null - ? ListView( - children: [ - if (currentGameAnalysis == options.gameAnyId?.gameId) - const Padding( - padding: EdgeInsets.only(top: 16.0), - child: WaitingForServerAnalysis(), - ), - AcplChart(pgn, options), - Center( - child: SizedBox( - width: math.min(MediaQuery.sizeOf(context).width, 500), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0), - child: Table( - defaultVerticalAlignment: - TableCellVerticalAlignment.middle, - columnWidths: const { - 0: FlexColumnWidth(1), - 1: FlexColumnWidth(1), - 2: FlexColumnWidth(1), - }, - children: [ - TableRow( - decoration: const BoxDecoration( - border: Border( - bottom: BorderSide(color: Colors.grey), - ), - ), - children: [ - _SummaryPlayerName(Side.white, pgnHeaders), - Center( - child: Text( - pgnHeaders.get('Result') ?? '', - style: const TextStyle( - fontWeight: FontWeight.bold, - ), - ), - ), - _SummaryPlayerName(Side.black, pgnHeaders), - ], - ), - if (playersAnalysis.white.accuracy != null && - playersAnalysis.black.accuracy != null) - TableRow( - children: [ - _SummaryNumber( - '${playersAnalysis.white.accuracy}%', - ), - Center( - heightFactor: 1.8, - child: Text( - context.l10n.accuracy, - softWrap: true, - ), - ), - _SummaryNumber( - '${playersAnalysis.black.accuracy}%', - ), - ], - ), - for (final item in [ - ( - playersAnalysis.white.inaccuracies.toString(), - context.l10n - .nbInaccuracies(2) - .replaceAll('2', '') - .trim() - .capitalize(), - playersAnalysis.black.inaccuracies.toString() - ), - ( - playersAnalysis.white.mistakes.toString(), - context.l10n - .nbMistakes(2) - .replaceAll('2', '') - .trim() - .capitalize(), - playersAnalysis.black.mistakes.toString() - ), - ( - playersAnalysis.white.blunders.toString(), - context.l10n - .nbBlunders(2) - .replaceAll('2', '') - .trim() - .capitalize(), - playersAnalysis.black.blunders.toString() - ), - ]) - TableRow( - children: [ - _SummaryNumber(item.$1), - Center( - heightFactor: 1.2, - child: Text( - item.$2, - softWrap: true, - ), - ), - _SummaryNumber(item.$3), - ], - ), - if (playersAnalysis.white.acpl != null && - playersAnalysis.black.acpl != null) - TableRow( - children: [ - _SummaryNumber( - playersAnalysis.white.acpl.toString(), - ), - Center( - heightFactor: 1.5, - child: Text( - context.l10n.averageCentipawnLoss, - softWrap: true, - textAlign: TextAlign.center, - ), - ), - _SummaryNumber( - playersAnalysis.black.acpl.toString(), - ), - ], - ), - ], - ), - ), - ), - ), - ], - ) - : Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Spacer(), - if (currentGameAnalysis == options.gameAnyId?.gameId) - const Center( - child: Padding( - padding: EdgeInsets.symmetric(vertical: 16.0), - child: WaitingForServerAnalysis(), - ), - ) - else - Center( - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 16.0), - child: Builder( - builder: (context) { - Future? pendingRequest; - return StatefulBuilder( - builder: (context, setState) { - return FutureBuilder( - future: pendingRequest, - builder: (context, snapshot) { - return SecondaryButton( - semanticsLabel: - context.l10n.requestAComputerAnalysis, - onPressed: ref.watch(authSessionProvider) == - null - ? () { - showPlatformSnackbar( - context, - context - .l10n.youNeedAnAccountToDoThat, - ); - } - : snapshot.connectionState == - ConnectionState.waiting - ? null - : () { - setState(() { - pendingRequest = ref - .read(ctrlProvider.notifier) - .requestServerAnalysis() - .catchError((Object e) { - if (context.mounted) { - showPlatformSnackbar( - context, - e.toString(), - type: SnackBarType.error, - ); - } - }); - }); - }, - child: Text( - context.l10n.requestAComputerAnalysis, - ), - ); - }, - ); - }, - ); - }, - ), - ), - ), - const Spacer(), - ], - ); - } -} - -class WaitingForServerAnalysis extends StatelessWidget { - const WaitingForServerAnalysis({super.key}); - - @override - Widget build(BuildContext context) { - return Row( - mainAxisAlignment: MainAxisAlignment.center, - mainAxisSize: MainAxisSize.max, - children: [ - Image.asset( - 'assets/images/stockfish/icon.png', - width: 30, - height: 30, - ), - const SizedBox(width: 8.0), - Text(context.l10n.waitingForAnalysis), - const SizedBox(width: 8.0), - const CircularProgressIndicator.adaptive(), - ], - ); - } -} - -class _SummaryNumber extends StatelessWidget { - const _SummaryNumber(this.data); - final String data; - - @override - Widget build(BuildContext context) { - return Center( - child: Text( - data, - softWrap: true, - ), - ); - } -} - -class _SummaryPlayerName extends StatelessWidget { - const _SummaryPlayerName(this.side, this.pgnHeaders); - final Side side; - final IMap pgnHeaders; - - @override - Widget build(BuildContext context) { - final playerTitle = side == Side.white - ? pgnHeaders.get('WhiteTitle') - : pgnHeaders.get('BlackTitle'); - final playerName = side == Side.white - ? pgnHeaders.get('White') ?? context.l10n.white - : pgnHeaders.get('Black') ?? context.l10n.black; - - final brightness = Theme.of(context).brightness; - - return TableCell( - verticalAlignment: TableCellVerticalAlignment.top, - child: Center( - child: Padding( - padding: const EdgeInsets.only(bottom: 5), - child: Column( - children: [ - Icon( - side == Side.white - ? brightness == Brightness.light - ? CupertinoIcons.circle - : CupertinoIcons.circle_filled - : brightness == Brightness.light - ? CupertinoIcons.circle_filled - : CupertinoIcons.circle, - size: 14, - ), - Text( - '${playerTitle != null ? '$playerTitle ' : ''}$playerName', - style: const TextStyle( - fontWeight: FontWeight.bold, - ), - textAlign: TextAlign.center, - softWrap: true, - ), - ], - ), - ), - ), - ); - } -} - -class AcplChart extends ConsumerWidget { - const AcplChart(this.pgn, this.options); - - final String pgn; - final AnalysisOptions options; - - @override - Widget build(BuildContext context, WidgetRef ref) { - final mainLineColor = Theme.of(context).colorScheme.secondary; - // yes it looks like below/above are inverted in fl_chart - final brightness = Theme.of(context).brightness; - final white = Theme.of(context).colorScheme.surfaceContainerHighest; - final black = Theme.of(context).colorScheme.outline; - // yes it looks like below/above are inverted in fl_chart - final belowLineColor = brightness == Brightness.light ? white : black; - final aboveLineColor = brightness == Brightness.light ? black : white; - - VerticalLine phaseVerticalBar(double x, String label) => VerticalLine( - x: x, - color: const Color(0xFF707070), - strokeWidth: 0.5, - label: VerticalLineLabel( - style: TextStyle( - fontSize: 10, - color: Theme.of(context) - .textTheme - .labelMedium - ?.color - ?.withValues(alpha: 0.3), - ), - labelResolver: (line) => label, - padding: const EdgeInsets.only(right: 1), - alignment: Alignment.topRight, - direction: LabelDirection.vertical, - show: true, - ), - ); - - final data = ref.watch( - analysisControllerProvider(pgn, options) - .select((value) => value.acplChartData), - ); - - final rootPly = ref.watch( - analysisControllerProvider(pgn, options) - .select((value) => value.root.position.ply), - ); - - final currentNode = ref.watch( - analysisControllerProvider(pgn, options) - .select((value) => value.currentNode), - ); - - final isOnMainline = ref.watch( - analysisControllerProvider(pgn, options) - .select((value) => value.isOnMainline), - ); - - if (data == null) { - return const SizedBox.shrink(); - } - - final spots = data - .mapIndexed( - (i, e) => FlSpot(i.toDouble(), e.winningChances(Side.white)), - ) - .toList(growable: false); - - final divisionLines = []; - - if (options.division?.middlegame != null) { - if (options.division!.middlegame! > 0) { - divisionLines.add(phaseVerticalBar(0.0, context.l10n.opening)); - divisionLines.add( - phaseVerticalBar( - options.division!.middlegame! - 1, - context.l10n.middlegame, - ), - ); - } else { - divisionLines.add(phaseVerticalBar(0.0, context.l10n.middlegame)); - } - } - - if (options.division?.endgame != null) { - if (options.division!.endgame! > 0) { - divisionLines.add( - phaseVerticalBar( - options.division!.endgame! - 1, - context.l10n.endgame, - ), - ); - } else { - divisionLines.add( - phaseVerticalBar( - 0.0, - context.l10n.endgame, - ), - ); - } - } - return Center( - child: AspectRatio( - aspectRatio: 2.5, - child: Padding( - padding: const EdgeInsets.all(16.0), - child: LineChart( - LineChartData( - lineTouchData: LineTouchData( - enabled: false, - touchCallback: - (FlTouchEvent event, LineTouchResponse? touchResponse) { - if (event is FlTapDownEvent || - event is FlPanUpdateEvent || - event is FlLongPressMoveUpdate) { - final touchX = event.localPosition!.dx; - final chartWidth = context.size!.width - - 32; // Insets on both sides of the chart of 16 - final minX = spots.first.x; - final maxX = spots.last.x; - final touchXDataValue = - minX + (touchX / chartWidth) * (maxX - minX); - final closestSpot = spots.reduce( - (a, b) => (a.x - touchXDataValue).abs() < - (b.x - touchXDataValue).abs() - ? a - : b, - ); - final closestNodeIndex = closestSpot.x.round(); - ref - .read(analysisControllerProvider(pgn, options).notifier) - .jumpToNthNodeOnMainline(closestNodeIndex); - } - }, - ), - minY: -1.0, - maxY: 1.0, - lineBarsData: [ - LineChartBarData( - spots: spots, - isCurved: false, - barWidth: 1, - color: mainLineColor.withValues(alpha: 0.7), - aboveBarData: BarAreaData( - show: true, - color: aboveLineColor, - applyCutOffY: true, - ), - belowBarData: BarAreaData( - show: true, - color: belowLineColor, - applyCutOffY: true, - ), - dotData: const FlDotData( - show: false, - ), - ), - ], - extraLinesData: ExtraLinesData( - verticalLines: [ - if (isOnMainline) - VerticalLine( - x: (currentNode.position.ply - 1 - rootPly).toDouble(), - color: mainLineColor, - strokeWidth: 1.0, - ), - ...divisionLines, - ], - ), - gridData: const FlGridData(show: false), - borderData: FlBorderData(show: false), - titlesData: const FlTitlesData(show: false), - ), - ), - ), - ), - ); - } -} diff --git a/lib/src/view/analysis/server_analysis.dart b/lib/src/view/analysis/server_analysis.dart new file mode 100644 index 0000000000..93510a1f6b --- /dev/null +++ b/lib/src/view/analysis/server_analysis.dart @@ -0,0 +1,501 @@ +import 'dart:math' as math; + +import 'package:collection/collection.dart'; +import 'package:dartchess/dartchess.dart'; +import 'package:fast_immutable_collections/fast_immutable_collections.dart'; +import 'package:fl_chart/fl_chart.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:lichess_mobile/src/model/analysis/analysis_controller.dart'; +import 'package:lichess_mobile/src/model/analysis/server_analysis_service.dart'; +import 'package:lichess_mobile/src/model/auth/auth_session.dart'; +import 'package:lichess_mobile/src/utils/l10n_context.dart'; +import 'package:lichess_mobile/src/utils/string.dart'; +import 'package:lichess_mobile/src/widgets/buttons.dart'; +import 'package:lichess_mobile/src/widgets/feedback.dart'; + +class ServerAnalysisSummary extends ConsumerWidget { + const ServerAnalysisSummary(this.pgn, this.options); + + final String pgn; + final AnalysisOptions options; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final ctrlProvider = analysisControllerProvider(pgn, options); + final playersAnalysis = + ref.watch(ctrlProvider.select((value) => value.playersAnalysis)); + final pgnHeaders = + ref.watch(ctrlProvider.select((value) => value.pgnHeaders)); + final currentGameAnalysis = ref.watch(currentAnalysisProvider); + + return playersAnalysis != null + ? ListView( + children: [ + if (currentGameAnalysis == options.gameAnyId?.gameId) + const Padding( + padding: EdgeInsets.only(top: 16.0), + child: WaitingForServerAnalysis(), + ), + AcplChart(pgn, options), + Center( + child: SizedBox( + width: math.min(MediaQuery.sizeOf(context).width, 500), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Table( + defaultVerticalAlignment: + TableCellVerticalAlignment.middle, + columnWidths: const { + 0: FlexColumnWidth(1), + 1: FlexColumnWidth(1), + 2: FlexColumnWidth(1), + }, + children: [ + TableRow( + decoration: const BoxDecoration( + border: Border( + bottom: BorderSide(color: Colors.grey), + ), + ), + children: [ + _SummaryPlayerName(Side.white, pgnHeaders), + Center( + child: Text( + pgnHeaders.get('Result') ?? '', + style: const TextStyle( + fontWeight: FontWeight.bold, + ), + ), + ), + _SummaryPlayerName(Side.black, pgnHeaders), + ], + ), + if (playersAnalysis.white.accuracy != null && + playersAnalysis.black.accuracy != null) + TableRow( + children: [ + _SummaryNumber( + '${playersAnalysis.white.accuracy}%', + ), + Center( + heightFactor: 1.8, + child: Text( + context.l10n.accuracy, + softWrap: true, + ), + ), + _SummaryNumber( + '${playersAnalysis.black.accuracy}%', + ), + ], + ), + for (final item in [ + ( + playersAnalysis.white.inaccuracies.toString(), + context.l10n + .nbInaccuracies(2) + .replaceAll('2', '') + .trim() + .capitalize(), + playersAnalysis.black.inaccuracies.toString() + ), + ( + playersAnalysis.white.mistakes.toString(), + context.l10n + .nbMistakes(2) + .replaceAll('2', '') + .trim() + .capitalize(), + playersAnalysis.black.mistakes.toString() + ), + ( + playersAnalysis.white.blunders.toString(), + context.l10n + .nbBlunders(2) + .replaceAll('2', '') + .trim() + .capitalize(), + playersAnalysis.black.blunders.toString() + ), + ]) + TableRow( + children: [ + _SummaryNumber(item.$1), + Center( + heightFactor: 1.2, + child: Text( + item.$2, + softWrap: true, + ), + ), + _SummaryNumber(item.$3), + ], + ), + if (playersAnalysis.white.acpl != null && + playersAnalysis.black.acpl != null) + TableRow( + children: [ + _SummaryNumber( + playersAnalysis.white.acpl.toString(), + ), + Center( + heightFactor: 1.5, + child: Text( + context.l10n.averageCentipawnLoss, + softWrap: true, + textAlign: TextAlign.center, + ), + ), + _SummaryNumber( + playersAnalysis.black.acpl.toString(), + ), + ], + ), + ], + ), + ), + ), + ), + ], + ) + : Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Spacer(), + if (currentGameAnalysis == options.gameAnyId?.gameId) + const Center( + child: Padding( + padding: EdgeInsets.symmetric(vertical: 16.0), + child: WaitingForServerAnalysis(), + ), + ) + else + Center( + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 16.0), + child: Builder( + builder: (context) { + Future? pendingRequest; + return StatefulBuilder( + builder: (context, setState) { + return FutureBuilder( + future: pendingRequest, + builder: (context, snapshot) { + return SecondaryButton( + semanticsLabel: + context.l10n.requestAComputerAnalysis, + onPressed: ref.watch(authSessionProvider) == + null + ? () { + showPlatformSnackbar( + context, + context + .l10n.youNeedAnAccountToDoThat, + ); + } + : snapshot.connectionState == + ConnectionState.waiting + ? null + : () { + setState(() { + pendingRequest = ref + .read(ctrlProvider.notifier) + .requestServerAnalysis() + .catchError((Object e) { + if (context.mounted) { + showPlatformSnackbar( + context, + e.toString(), + type: SnackBarType.error, + ); + } + }); + }); + }, + child: Text( + context.l10n.requestAComputerAnalysis, + ), + ); + }, + ); + }, + ); + }, + ), + ), + ), + const Spacer(), + ], + ); + } +} + +class WaitingForServerAnalysis extends StatelessWidget { + const WaitingForServerAnalysis({super.key}); + + @override + Widget build(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.max, + children: [ + Image.asset( + 'assets/images/stockfish/icon.png', + width: 30, + height: 30, + ), + const SizedBox(width: 8.0), + Text(context.l10n.waitingForAnalysis), + const SizedBox(width: 8.0), + const CircularProgressIndicator.adaptive(), + ], + ); + } +} + +class _SummaryNumber extends StatelessWidget { + const _SummaryNumber(this.data); + final String data; + + @override + Widget build(BuildContext context) { + return Center( + child: Text( + data, + softWrap: true, + ), + ); + } +} + +class _SummaryPlayerName extends StatelessWidget { + const _SummaryPlayerName(this.side, this.pgnHeaders); + final Side side; + final IMap pgnHeaders; + + @override + Widget build(BuildContext context) { + final playerTitle = side == Side.white + ? pgnHeaders.get('WhiteTitle') + : pgnHeaders.get('BlackTitle'); + final playerName = side == Side.white + ? pgnHeaders.get('White') ?? context.l10n.white + : pgnHeaders.get('Black') ?? context.l10n.black; + + final brightness = Theme.of(context).brightness; + + return TableCell( + verticalAlignment: TableCellVerticalAlignment.top, + child: Center( + child: Padding( + padding: const EdgeInsets.only(bottom: 5), + child: Column( + children: [ + Icon( + side == Side.white + ? brightness == Brightness.light + ? CupertinoIcons.circle + : CupertinoIcons.circle_filled + : brightness == Brightness.light + ? CupertinoIcons.circle_filled + : CupertinoIcons.circle, + size: 14, + ), + Text( + '${playerTitle != null ? '$playerTitle ' : ''}$playerName', + style: const TextStyle( + fontWeight: FontWeight.bold, + ), + textAlign: TextAlign.center, + softWrap: true, + ), + ], + ), + ), + ), + ); + } +} + +class AcplChart extends ConsumerWidget { + const AcplChart(this.pgn, this.options); + + final String pgn; + final AnalysisOptions options; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final mainLineColor = Theme.of(context).colorScheme.secondary; + // yes it looks like below/above are inverted in fl_chart + final brightness = Theme.of(context).brightness; + final white = Theme.of(context).colorScheme.surfaceContainerHighest; + final black = Theme.of(context).colorScheme.outline; + // yes it looks like below/above are inverted in fl_chart + final belowLineColor = brightness == Brightness.light ? white : black; + final aboveLineColor = brightness == Brightness.light ? black : white; + + VerticalLine phaseVerticalBar(double x, String label) => VerticalLine( + x: x, + color: const Color(0xFF707070), + strokeWidth: 0.5, + label: VerticalLineLabel( + style: TextStyle( + fontSize: 10, + color: Theme.of(context) + .textTheme + .labelMedium + ?.color + ?.withValues(alpha: 0.3), + ), + labelResolver: (line) => label, + padding: const EdgeInsets.only(right: 1), + alignment: Alignment.topRight, + direction: LabelDirection.vertical, + show: true, + ), + ); + + final data = ref.watch( + analysisControllerProvider(pgn, options) + .select((value) => value.acplChartData), + ); + + final rootPly = ref.watch( + analysisControllerProvider(pgn, options) + .select((value) => value.root.position.ply), + ); + + final currentNode = ref.watch( + analysisControllerProvider(pgn, options) + .select((value) => value.currentNode), + ); + + final isOnMainline = ref.watch( + analysisControllerProvider(pgn, options) + .select((value) => value.isOnMainline), + ); + + if (data == null) { + return const SizedBox.shrink(); + } + + final spots = data + .mapIndexed( + (i, e) => FlSpot(i.toDouble(), e.winningChances(Side.white)), + ) + .toList(growable: false); + + final divisionLines = []; + + if (options.division?.middlegame != null) { + if (options.division!.middlegame! > 0) { + divisionLines.add(phaseVerticalBar(0.0, context.l10n.opening)); + divisionLines.add( + phaseVerticalBar( + options.division!.middlegame! - 1, + context.l10n.middlegame, + ), + ); + } else { + divisionLines.add(phaseVerticalBar(0.0, context.l10n.middlegame)); + } + } + + if (options.division?.endgame != null) { + if (options.division!.endgame! > 0) { + divisionLines.add( + phaseVerticalBar( + options.division!.endgame! - 1, + context.l10n.endgame, + ), + ); + } else { + divisionLines.add( + phaseVerticalBar( + 0.0, + context.l10n.endgame, + ), + ); + } + } + return Center( + child: AspectRatio( + aspectRatio: 2.5, + child: Padding( + padding: const EdgeInsets.all(16.0), + child: LineChart( + LineChartData( + lineTouchData: LineTouchData( + enabled: false, + touchCallback: + (FlTouchEvent event, LineTouchResponse? touchResponse) { + if (event is FlTapDownEvent || + event is FlPanUpdateEvent || + event is FlLongPressMoveUpdate) { + final touchX = event.localPosition!.dx; + final chartWidth = context.size!.width - + 32; // Insets on both sides of the chart of 16 + final minX = spots.first.x; + final maxX = spots.last.x; + final touchXDataValue = + minX + (touchX / chartWidth) * (maxX - minX); + final closestSpot = spots.reduce( + (a, b) => (a.x - touchXDataValue).abs() < + (b.x - touchXDataValue).abs() + ? a + : b, + ); + final closestNodeIndex = closestSpot.x.round(); + ref + .read(analysisControllerProvider(pgn, options).notifier) + .jumpToNthNodeOnMainline(closestNodeIndex); + } + }, + ), + minY: -1.0, + maxY: 1.0, + lineBarsData: [ + LineChartBarData( + spots: spots, + isCurved: false, + barWidth: 1, + color: mainLineColor.withValues(alpha: 0.7), + aboveBarData: BarAreaData( + show: true, + color: aboveLineColor, + applyCutOffY: true, + ), + belowBarData: BarAreaData( + show: true, + color: belowLineColor, + applyCutOffY: true, + ), + dotData: const FlDotData( + show: false, + ), + ), + ], + extraLinesData: ExtraLinesData( + verticalLines: [ + if (isOnMainline) + VerticalLine( + x: (currentNode.position.ply - 1 - rootPly).toDouble(), + color: mainLineColor, + strokeWidth: 1.0, + ), + ...divisionLines, + ], + ), + gridData: const FlGridData(show: false), + borderData: FlBorderData(show: false), + titlesData: const FlTitlesData(show: false), + ), + ), + ), + ), + ); + } +} diff --git a/lib/src/view/engine/engine_depth.dart b/lib/src/view/engine/engine_depth.dart new file mode 100644 index 0000000000..d3352d676d --- /dev/null +++ b/lib/src/view/engine/engine_depth.dart @@ -0,0 +1,117 @@ +import 'dart:math' as math; + +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:lichess_mobile/src/model/common/eval.dart'; +import 'package:lichess_mobile/src/model/engine/engine.dart'; +import 'package:lichess_mobile/src/model/engine/evaluation_service.dart'; +import 'package:lichess_mobile/src/utils/l10n_context.dart'; +import 'package:lichess_mobile/src/widgets/buttons.dart'; +import 'package:lichess_mobile/src/widgets/list.dart'; +import 'package:popover/popover.dart'; + +class EngineDepth extends ConsumerWidget { + const EngineDepth({this.defaultEval}); + + final ClientEval? defaultEval; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final depth = ref.watch( + engineEvaluationProvider.select((value) => value.eval?.depth), + ) ?? + defaultEval?.depth; + + return depth != null + ? AppBarTextButton( + onPressed: () { + showPopover( + context: context, + bodyBuilder: (context) { + return _StockfishInfo(defaultEval); + }, + direction: PopoverDirection.top, + width: 240, + backgroundColor: + Theme.of(context).platform == TargetPlatform.android + ? Theme.of(context).dialogBackgroundColor + : CupertinoDynamicColor.resolve( + CupertinoColors.tertiarySystemBackground, + context, + ), + transitionDuration: Duration.zero, + popoverTransitionBuilder: (_, child) => child, + ); + }, + child: RepaintBoundary( + child: Container( + width: 20.0, + height: 20.0, + padding: const EdgeInsets.all(2.0), + decoration: BoxDecoration( + color: Theme.of(context).platform == TargetPlatform.android + ? Theme.of(context).colorScheme.secondary + : CupertinoTheme.of(context).primaryColor, + borderRadius: BorderRadius.circular(4.0), + ), + child: FittedBox( + fit: BoxFit.contain, + child: Text( + '${math.min(99, depth)}', + style: TextStyle( + color: Theme.of(context).platform == + TargetPlatform.android + ? Theme.of(context).colorScheme.onSecondary + : CupertinoTheme.of(context).primaryContrastingColor, + fontFeatures: const [ + FontFeature.tabularFigures(), + ], + ), + ), + ), + ), + ), + ) + : const SizedBox.shrink(); + } +} + +class _StockfishInfo extends ConsumerWidget { + const _StockfishInfo(this.defaultEval); + + final ClientEval? defaultEval; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final (engineName: engineName, eval: eval, state: engineState) = + ref.watch(engineEvaluationProvider); + + final currentEval = eval ?? defaultEval; + + final knps = engineState == EngineState.computing + ? ', ${eval?.knps.round()}kn/s' + : ''; + final depth = currentEval?.depth ?? 0; + final maxDepth = math.max(depth, kMaxEngineDepth); + + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + PlatformListTile( + leading: Image.asset( + 'assets/images/stockfish/icon.png', + width: 44, + height: 44, + ), + title: Text(engineName), + subtitle: Text( + context.l10n.depthX( + '$depth/$maxDepth$knps', + ), + ), + ), + ], + ); + } +} diff --git a/lib/src/view/opening_explorer/opening_explorer_screen.dart b/lib/src/view/opening_explorer/opening_explorer_screen.dart index 0e2d1dbc29..341afecb60 100644 --- a/lib/src/view/opening_explorer/opening_explorer_screen.dart +++ b/lib/src/view/opening_explorer/opening_explorer_screen.dart @@ -1,10 +1,7 @@ import 'package:collection/collection.dart'; -import 'package:dartchess/dartchess.dart'; -import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:intl/intl.dart'; import 'package:lichess_mobile/src/constants.dart'; import 'package:lichess_mobile/src/model/analysis/analysis_controller.dart'; import 'package:lichess_mobile/src/model/common/chess.dart'; @@ -13,10 +10,9 @@ import 'package:lichess_mobile/src/model/opening_explorer/opening_explorer_prefe import 'package:lichess_mobile/src/model/opening_explorer/opening_explorer_repository.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/utils/screen.dart'; import 'package:lichess_mobile/src/view/analysis/analysis_board.dart'; -import 'package:lichess_mobile/src/view/game/archived_game_screen.dart'; +import 'package:lichess_mobile/src/view/opening_explorer/opening_explorer_widgets.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/bottom_bar_button.dart'; @@ -36,16 +32,6 @@ const _kTableRowPadding = EdgeInsets.symmetric( ); const _kTabletBoardRadius = BorderRadius.all(Radius.circular(4.0)); -Color _whiteBoxColor(BuildContext context) => - Theme.of(context).brightness == Brightness.dark - ? Colors.white.withValues(alpha: 0.8) - : Colors.white; - -Color _blackBoxColor(BuildContext context) => - Theme.of(context).brightness == Brightness.light - ? Colors.black.withValues(alpha: 0.7) - : Colors.black; - class OpeningExplorerScreen extends ConsumerStatefulWidget { const OpeningExplorerScreen({required this.pgn, required this.options}); @@ -122,7 +108,7 @@ class _OpeningExplorerState extends ConsumerState { isIndexing: false, children: [ openingHeader, - _OpeningExplorerMoveTable.maxDepth( + OpeningExplorerMoveTable.maxDepth( pgn: widget.pgn, options: widget.options, ), @@ -189,7 +175,7 @@ class _OpeningExplorerState extends ConsumerState { Shimmer( child: ShimmerLoading( isLoading: true, - child: _OpeningExplorerMoveTable.loading( + child: OpeningExplorerMoveTable.loading( pgn: widget.pgn, options: widget.options, ), @@ -205,7 +191,7 @@ class _OpeningExplorerState extends ConsumerState { final children = [ openingHeader, - _OpeningExplorerMoveTable( + OpeningExplorerMoveTable( moves: openingExplorer.entry.moves, whiteWins: openingExplorer.entry.white, draws: openingExplorer.entry.draws, @@ -214,7 +200,7 @@ class _OpeningExplorerState extends ConsumerState { options: widget.options, ), if (topGames != null && topGames.isNotEmpty) ...[ - _OpeningExplorerHeader( + OpeningExplorerHeaderTile( key: const Key('topGamesHeader'), child: Text(context.l10n.topGames), ), @@ -234,7 +220,7 @@ class _OpeningExplorerState extends ConsumerState { ), ], if (recentGames != null && recentGames.isNotEmpty) ...[ - _OpeningExplorerHeader( + OpeningExplorerHeaderTile( key: const Key('recentGamesHeader'), child: Text(context.l10n.recentGames), ), @@ -265,7 +251,7 @@ class _OpeningExplorerState extends ConsumerState { Shimmer( child: ShimmerLoading( isLoading: true, - child: _OpeningExplorerMoveTable.loading( + child: OpeningExplorerMoveTable.loading( pgn: widget.pgn, options: widget.options, ), @@ -338,15 +324,7 @@ class _OpeningExplorerView extends StatelessWidget { final isLandscape = aspectRatio > 1; final loadingOverlay = Positioned.fill( - child: IgnorePointer( - ignoring: !isLoading, - child: AnimatedOpacity( - duration: const Duration(milliseconds: 300), - curve: Curves.fastOutSlowIn, - opacity: isLoading ? 0.10 : 0.0, - child: const ColoredBox(color: Colors.black), - ), - ), + child: IgnorePointer(ignoring: !isLoading), ); if (isLandscape) { @@ -501,482 +479,6 @@ class _IndexingIndicatorState extends State<_IndexingIndicator> } } -/// Table of moves for the opening explorer. -class _OpeningExplorerMoveTable extends ConsumerWidget { - const _OpeningExplorerMoveTable({ - required this.moves, - required this.whiteWins, - required this.draws, - required this.blackWins, - required this.pgn, - required this.options, - }) : _isLoading = false, - _maxDepthReached = false; - - const _OpeningExplorerMoveTable.loading({ - required this.pgn, - required this.options, - }) : _isLoading = true, - moves = const IListConst([]), - whiteWins = 0, - draws = 0, - blackWins = 0, - _maxDepthReached = false; - - const _OpeningExplorerMoveTable.maxDepth({ - required this.pgn, - required this.options, - }) : _isLoading = false, - moves = const IListConst([]), - whiteWins = 0, - draws = 0, - blackWins = 0, - _maxDepthReached = true; - - final IList moves; - final int whiteWins; - final int draws; - final int blackWins; - final String pgn; - final AnalysisOptions options; - - final bool _isLoading; - final bool _maxDepthReached; - - String formatNum(int num) => NumberFormat.decimalPatternDigits().format(num); - - static const columnWidths = { - 0: FractionColumnWidth(0.15), - 1: FractionColumnWidth(0.35), - 2: FractionColumnWidth(0.50), - }; - - @override - Widget build(BuildContext context, WidgetRef ref) { - if (_isLoading) { - return loadingTable; - } - - final games = whiteWins + draws + blackWins; - final ctrlProvider = analysisControllerProvider(pgn, options); - - const topPadding = EdgeInsets.only(top: _kTableRowVerticalPadding / 2); - const headerTextStyle = TextStyle(fontSize: 12); - - return Table( - columnWidths: columnWidths, - children: [ - TableRow( - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.secondaryContainer, - ), - children: [ - Padding( - padding: _kTableRowPadding.subtract(topPadding), - child: Text(context.l10n.move, style: headerTextStyle), - ), - Padding( - padding: _kTableRowPadding.subtract(topPadding), - child: Text(context.l10n.games, style: headerTextStyle), - ), - Padding( - padding: _kTableRowPadding.subtract(topPadding), - child: Text(context.l10n.whiteDrawBlack, style: headerTextStyle), - ), - ], - ), - ...List.generate( - moves.length, - (int index) { - final move = moves.get(index); - final percentGames = ((move.games / games) * 100).round(); - return TableRow( - decoration: BoxDecoration( - color: index.isEven - ? Theme.of(context).colorScheme.surfaceContainerLow - : Theme.of(context).colorScheme.surfaceContainerHigh, - ), - children: [ - TableRowInkWell( - onTap: () => ref - .read(ctrlProvider.notifier) - .onUserMove(NormalMove.fromUci(move.uci)), - child: Padding( - padding: _kTableRowPadding, - child: Text(move.san), - ), - ), - TableRowInkWell( - onTap: () => ref - .read(ctrlProvider.notifier) - .onUserMove(NormalMove.fromUci(move.uci)), - child: Padding( - padding: _kTableRowPadding, - child: Text('${formatNum(move.games)} ($percentGames%)'), - ), - ), - TableRowInkWell( - onTap: () => ref - .read(ctrlProvider.notifier) - .onUserMove(NormalMove.fromUci(move.uci)), - child: Padding( - padding: _kTableRowPadding, - child: _WinPercentageChart( - whiteWins: move.white, - draws: move.draws, - blackWins: move.black, - ), - ), - ), - ], - ); - }, - ), - if (_maxDepthReached) - TableRow( - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surfaceContainerLow, - ), - children: [ - Padding( - padding: _kTableRowPadding, - child: Text( - String.fromCharCode(Icons.not_interested_outlined.codePoint), - style: TextStyle( - fontFamily: Icons.not_interested_outlined.fontFamily, - ), - ), - ), - Padding( - padding: _kTableRowPadding, - child: Text(context.l10n.maxDepthReached), - ), - const Padding( - padding: _kTableRowPadding, - child: SizedBox.shrink(), - ), - ], - ) - else if (moves.isNotEmpty) - TableRow( - decoration: BoxDecoration( - color: moves.length.isEven - ? Theme.of(context).colorScheme.surfaceContainerLow - : Theme.of(context).colorScheme.surfaceContainerHigh, - ), - children: [ - Container( - padding: _kTableRowPadding, - alignment: Alignment.centerLeft, - child: const Icon(Icons.functions), - ), - Padding( - padding: _kTableRowPadding, - child: Text('${formatNum(games)} (100%)'), - ), - Padding( - padding: _kTableRowPadding, - child: _WinPercentageChart( - whiteWins: whiteWins, - draws: draws, - blackWins: blackWins, - ), - ), - ], - ) - else - TableRow( - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surfaceContainerLow, - ), - children: [ - Padding( - padding: _kTableRowPadding, - child: Text( - String.fromCharCode(Icons.not_interested_outlined.codePoint), - style: TextStyle( - fontFamily: Icons.not_interested_outlined.fontFamily, - ), - ), - ), - Padding( - padding: _kTableRowPadding, - child: Text(context.l10n.noGameFound), - ), - const Padding( - padding: _kTableRowPadding, - child: SizedBox.shrink(), - ), - ], - ), - ], - ); - } - - static final loadingTable = Table( - columnWidths: columnWidths, - children: List.generate( - 10, - (int index) => TableRow( - children: [ - Padding( - padding: _kTableRowPadding, - child: Container( - height: 20, - width: double.infinity, - decoration: BoxDecoration( - color: Colors.black, - borderRadius: BorderRadius.circular(5), - ), - ), - ), - Padding( - padding: _kTableRowPadding, - child: Container( - height: 20, - width: double.infinity, - decoration: BoxDecoration( - color: Colors.black, - borderRadius: BorderRadius.circular(5), - ), - ), - ), - Padding( - padding: _kTableRowPadding, - child: Container( - height: 20, - width: double.infinity, - decoration: BoxDecoration( - color: Colors.black, - borderRadius: BorderRadius.circular(5), - ), - ), - ), - ], - ), - ), - ); -} - -/// A game tile for the opening explorer. -class OpeningExplorerGameTile extends ConsumerStatefulWidget { - const OpeningExplorerGameTile({ - required this.game, - required this.color, - required this.ply, - super.key, - }); - - final OpeningExplorerGame game; - final Color color; - final int ply; - - @override - ConsumerState createState() => - _OpeningExplorerGameTileState(); -} - -class _OpeningExplorerGameTileState - extends ConsumerState { - @override - Widget build(BuildContext context) { - const widthResultBox = 50.0; - const paddingResultBox = EdgeInsets.all(5); - - return Container( - padding: _kTableRowPadding, - color: widget.color, - child: AdaptiveInkWell( - onTap: () { - pushPlatformRoute( - context, - builder: (_) => ArchivedGameScreen( - gameId: widget.game.id, - orientation: Side.white, - initialCursor: widget.ply, - ), - ); - }, - child: Row( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(widget.game.white.rating.toString()), - Text(widget.game.black.rating.toString()), - ], - ), - const SizedBox(width: 10), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - widget.game.white.name, - overflow: TextOverflow.ellipsis, - ), - Text( - widget.game.black.name, - overflow: TextOverflow.ellipsis, - ), - ], - ), - ), - Row( - children: [ - if (widget.game.winner == 'white') - Container( - width: widthResultBox, - padding: paddingResultBox, - decoration: BoxDecoration( - color: _whiteBoxColor(context), - borderRadius: BorderRadius.circular(5), - ), - child: const Text( - '1-0', - textAlign: TextAlign.center, - style: TextStyle( - color: Colors.black, - ), - ), - ) - else if (widget.game.winner == 'black') - Container( - width: widthResultBox, - padding: paddingResultBox, - decoration: BoxDecoration( - color: _blackBoxColor(context), - borderRadius: BorderRadius.circular(5), - ), - child: const Text( - '0-1', - textAlign: TextAlign.center, - style: TextStyle( - color: Colors.white, - ), - ), - ) - else - Container( - width: widthResultBox, - padding: paddingResultBox, - decoration: BoxDecoration( - color: Colors.grey, - borderRadius: BorderRadius.circular(5), - ), - child: const Text( - '½-½', - textAlign: TextAlign.center, - style: TextStyle( - color: Colors.white, - ), - ), - ), - if (widget.game.month != null) ...[ - const SizedBox(width: 10.0), - Text( - widget.game.month!, - style: const TextStyle( - fontFeatures: [FontFeature.tabularFigures()], - ), - ), - ], - if (widget.game.speed != null) ...[ - const SizedBox(width: 10.0), - Icon(widget.game.speed!.icon, size: 20), - ], - ], - ), - ], - ), - ), - ); - } -} - -class _OpeningExplorerHeader extends StatelessWidget { - const _OpeningExplorerHeader({required this.child, super.key}); - - final Widget child; - - @override - Widget build(BuildContext context) { - return Container( - width: double.infinity, - padding: _kTableRowPadding, - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.secondaryContainer, - ), - child: child, - ); - } -} - -class _WinPercentageChart extends StatelessWidget { - const _WinPercentageChart({ - required this.whiteWins, - required this.draws, - required this.blackWins, - }); - - final int whiteWins; - final int draws; - final int blackWins; - - int percentGames(int games) => - ((games / (whiteWins + draws + blackWins)) * 100).round(); - String label(int percent) => percent < 20 ? '' : '$percent%'; - - @override - Widget build(BuildContext context) { - final percentWhite = percentGames(whiteWins); - final percentDraws = percentGames(draws); - final percentBlack = percentGames(blackWins); - - return ClipRRect( - borderRadius: BorderRadius.circular(5), - child: Row( - children: [ - Expanded( - flex: percentWhite, - child: ColoredBox( - color: _whiteBoxColor(context), - child: Text( - label(percentWhite), - textAlign: TextAlign.center, - style: const TextStyle(color: Colors.black), - ), - ), - ), - Expanded( - flex: percentDraws, - child: ColoredBox( - color: Colors.grey, - child: Text( - label(percentDraws), - textAlign: TextAlign.center, - style: const TextStyle(color: Colors.white), - ), - ), - ), - Expanded( - flex: percentBlack, - child: ColoredBox( - color: _blackBoxColor(context), - child: Text( - label(percentBlack), - textAlign: TextAlign.center, - style: const TextStyle(color: Colors.white), - ), - ), - ), - ], - ), - ); - } -} - class _MoveList extends ConsumerWidget implements PreferredSizeWidget { const _MoveList({ required this.pgn, diff --git a/lib/src/view/opening_explorer/opening_explorer_widgets.dart b/lib/src/view/opening_explorer/opening_explorer_widgets.dart new file mode 100644 index 0000000000..f0374e07ab --- /dev/null +++ b/lib/src/view/opening_explorer/opening_explorer_widgets.dart @@ -0,0 +1,504 @@ +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:intl/intl.dart'; +import 'package:lichess_mobile/src/model/analysis/analysis_controller.dart'; +import 'package:lichess_mobile/src/model/opening_explorer/opening_explorer.dart'; +import 'package:lichess_mobile/src/utils/l10n_context.dart'; +import 'package:lichess_mobile/src/utils/navigation.dart'; +import 'package:lichess_mobile/src/view/game/archived_game_screen.dart'; +import 'package:lichess_mobile/src/widgets/buttons.dart'; + +const _kTableRowVerticalPadding = 12.0; +const _kTableRowHorizontalPadding = 8.0; +const _kTableRowPadding = EdgeInsets.symmetric( + horizontal: _kTableRowHorizontalPadding, + vertical: _kTableRowVerticalPadding, +); + +Color _whiteBoxColor(BuildContext context) => + Theme.of(context).brightness == Brightness.dark + ? Colors.white.withValues(alpha: 0.8) + : Colors.white; + +Color _blackBoxColor(BuildContext context) => + Theme.of(context).brightness == Brightness.light + ? Colors.black.withValues(alpha: 0.7) + : Colors.black; + +/// Table of moves for the opening explorer. +class OpeningExplorerMoveTable extends ConsumerWidget { + const OpeningExplorerMoveTable({ + required this.moves, + required this.whiteWins, + required this.draws, + required this.blackWins, + required this.pgn, + required this.options, + }) : _isLoading = false, + _maxDepthReached = false; + + const OpeningExplorerMoveTable.loading({ + required this.pgn, + required this.options, + }) : _isLoading = true, + moves = const IListConst([]), + whiteWins = 0, + draws = 0, + blackWins = 0, + _maxDepthReached = false; + + const OpeningExplorerMoveTable.maxDepth({ + required this.pgn, + required this.options, + }) : _isLoading = false, + moves = const IListConst([]), + whiteWins = 0, + draws = 0, + blackWins = 0, + _maxDepthReached = true; + + final IList moves; + final int whiteWins; + final int draws; + final int blackWins; + final String pgn; + final AnalysisOptions options; + + final bool _isLoading; + final bool _maxDepthReached; + + String formatNum(int num) => NumberFormat.decimalPatternDigits().format(num); + + static const columnWidths = { + 0: FractionColumnWidth(0.15), + 1: FractionColumnWidth(0.35), + 2: FractionColumnWidth(0.50), + }; + + @override + Widget build(BuildContext context, WidgetRef ref) { + if (_isLoading) { + return loadingTable; + } + + final games = whiteWins + draws + blackWins; + final ctrlProvider = analysisControllerProvider(pgn, options); + + const topPadding = EdgeInsets.only(top: _kTableRowVerticalPadding / 2); + const headerTextStyle = TextStyle(fontSize: 12); + + return Table( + columnWidths: columnWidths, + children: [ + TableRow( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.secondaryContainer, + ), + children: [ + Padding( + padding: _kTableRowPadding.subtract(topPadding), + child: Text(context.l10n.move, style: headerTextStyle), + ), + Padding( + padding: _kTableRowPadding.subtract(topPadding), + child: Text(context.l10n.games, style: headerTextStyle), + ), + Padding( + padding: _kTableRowPadding.subtract(topPadding), + child: Text(context.l10n.whiteDrawBlack, style: headerTextStyle), + ), + ], + ), + ...List.generate( + moves.length, + (int index) { + final move = moves.get(index); + final percentGames = ((move.games / games) * 100).round(); + return TableRow( + decoration: BoxDecoration( + color: index.isEven + ? Theme.of(context).colorScheme.surfaceContainerLow + : Theme.of(context).colorScheme.surfaceContainerHigh, + ), + children: [ + TableRowInkWell( + onTap: () => ref + .read(ctrlProvider.notifier) + .onUserMove(NormalMove.fromUci(move.uci)), + child: Padding( + padding: _kTableRowPadding, + child: Text(move.san), + ), + ), + TableRowInkWell( + onTap: () => ref + .read(ctrlProvider.notifier) + .onUserMove(NormalMove.fromUci(move.uci)), + child: Padding( + padding: _kTableRowPadding, + child: Text('${formatNum(move.games)} ($percentGames%)'), + ), + ), + TableRowInkWell( + onTap: () => ref + .read(ctrlProvider.notifier) + .onUserMove(NormalMove.fromUci(move.uci)), + child: Padding( + padding: _kTableRowPadding, + child: _WinPercentageChart( + whiteWins: move.white, + draws: move.draws, + blackWins: move.black, + ), + ), + ), + ], + ); + }, + ), + if (_maxDepthReached) + TableRow( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceContainerLow, + ), + children: [ + Padding( + padding: _kTableRowPadding, + child: Text( + String.fromCharCode(Icons.not_interested_outlined.codePoint), + style: TextStyle( + fontFamily: Icons.not_interested_outlined.fontFamily, + ), + ), + ), + Padding( + padding: _kTableRowPadding, + child: Text(context.l10n.maxDepthReached), + ), + const Padding( + padding: _kTableRowPadding, + child: SizedBox.shrink(), + ), + ], + ) + else if (moves.isNotEmpty) + TableRow( + decoration: BoxDecoration( + color: moves.length.isEven + ? Theme.of(context).colorScheme.surfaceContainerLow + : Theme.of(context).colorScheme.surfaceContainerHigh, + ), + children: [ + Container( + padding: _kTableRowPadding, + alignment: Alignment.centerLeft, + child: const Icon(Icons.functions), + ), + Padding( + padding: _kTableRowPadding, + child: Text('${formatNum(games)} (100%)'), + ), + Padding( + padding: _kTableRowPadding, + child: _WinPercentageChart( + whiteWins: whiteWins, + draws: draws, + blackWins: blackWins, + ), + ), + ], + ) + else + TableRow( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceContainerLow, + ), + children: [ + Padding( + padding: _kTableRowPadding, + child: Text( + String.fromCharCode(Icons.not_interested_outlined.codePoint), + style: TextStyle( + fontFamily: Icons.not_interested_outlined.fontFamily, + ), + ), + ), + Padding( + padding: _kTableRowPadding, + child: Text(context.l10n.noGameFound), + ), + const Padding( + padding: _kTableRowPadding, + child: SizedBox.shrink(), + ), + ], + ), + ], + ); + } + + static final loadingTable = Table( + columnWidths: columnWidths, + children: List.generate( + 10, + (int index) => TableRow( + children: [ + Padding( + padding: _kTableRowPadding, + child: Container( + height: 20, + width: double.infinity, + decoration: BoxDecoration( + color: Colors.black, + borderRadius: BorderRadius.circular(5), + ), + ), + ), + Padding( + padding: _kTableRowPadding, + child: Container( + height: 20, + width: double.infinity, + decoration: BoxDecoration( + color: Colors.black, + borderRadius: BorderRadius.circular(5), + ), + ), + ), + Padding( + padding: _kTableRowPadding, + child: Container( + height: 20, + width: double.infinity, + decoration: BoxDecoration( + color: Colors.black, + borderRadius: BorderRadius.circular(5), + ), + ), + ), + ], + ), + ), + ); +} + +class OpeningExplorerHeaderTile extends StatelessWidget { + const OpeningExplorerHeaderTile({required this.child, super.key}); + + final Widget child; + + @override + Widget build(BuildContext context) { + return Container( + width: double.infinity, + padding: _kTableRowPadding, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.secondaryContainer, + ), + child: child, + ); + } +} + +/// A game tile for the opening explorer. +class OpeningExplorerGameTile extends ConsumerStatefulWidget { + const OpeningExplorerGameTile({ + required this.game, + required this.color, + required this.ply, + super.key, + }); + + final OpeningExplorerGame game; + final Color color; + final int ply; + + @override + ConsumerState createState() => + _OpeningExplorerGameTileState(); +} + +class _OpeningExplorerGameTileState + extends ConsumerState { + @override + Widget build(BuildContext context) { + const widthResultBox = 50.0; + const paddingResultBox = EdgeInsets.all(5); + + return Container( + padding: _kTableRowPadding, + color: widget.color, + child: AdaptiveInkWell( + onTap: () { + pushPlatformRoute( + context, + builder: (_) => ArchivedGameScreen( + gameId: widget.game.id, + orientation: Side.white, + initialCursor: widget.ply, + ), + ); + }, + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(widget.game.white.rating.toString()), + Text(widget.game.black.rating.toString()), + ], + ), + const SizedBox(width: 10), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + widget.game.white.name, + overflow: TextOverflow.ellipsis, + ), + Text( + widget.game.black.name, + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + Row( + children: [ + if (widget.game.winner == 'white') + Container( + width: widthResultBox, + padding: paddingResultBox, + decoration: BoxDecoration( + color: _whiteBoxColor(context), + borderRadius: BorderRadius.circular(5), + ), + child: const Text( + '1-0', + textAlign: TextAlign.center, + style: TextStyle( + color: Colors.black, + ), + ), + ) + else if (widget.game.winner == 'black') + Container( + width: widthResultBox, + padding: paddingResultBox, + decoration: BoxDecoration( + color: _blackBoxColor(context), + borderRadius: BorderRadius.circular(5), + ), + child: const Text( + '0-1', + textAlign: TextAlign.center, + style: TextStyle( + color: Colors.white, + ), + ), + ) + else + Container( + width: widthResultBox, + padding: paddingResultBox, + decoration: BoxDecoration( + color: Colors.grey, + borderRadius: BorderRadius.circular(5), + ), + child: const Text( + '½-½', + textAlign: TextAlign.center, + style: TextStyle( + color: Colors.white, + ), + ), + ), + if (widget.game.month != null) ...[ + const SizedBox(width: 10.0), + Text( + widget.game.month!, + style: const TextStyle( + fontFeatures: [FontFeature.tabularFigures()], + ), + ), + ], + if (widget.game.speed != null) ...[ + const SizedBox(width: 10.0), + Icon(widget.game.speed!.icon, size: 20), + ], + ], + ), + ], + ), + ), + ); + } +} + +class _WinPercentageChart extends StatelessWidget { + const _WinPercentageChart({ + required this.whiteWins, + required this.draws, + required this.blackWins, + }); + + final int whiteWins; + final int draws; + final int blackWins; + + int percentGames(int games) => + ((games / (whiteWins + draws + blackWins)) * 100).round(); + String label(int percent) => percent < 20 ? '' : '$percent%'; + + @override + Widget build(BuildContext context) { + final percentWhite = percentGames(whiteWins); + final percentDraws = percentGames(draws); + final percentBlack = percentGames(blackWins); + + return ClipRRect( + borderRadius: BorderRadius.circular(5), + child: Row( + children: [ + Expanded( + flex: percentWhite, + child: ColoredBox( + color: _whiteBoxColor(context), + child: Text( + label(percentWhite), + textAlign: TextAlign.center, + style: const TextStyle(color: Colors.black), + ), + ), + ), + Expanded( + flex: percentDraws, + child: ColoredBox( + color: Colors.grey, + child: Text( + label(percentDraws), + textAlign: TextAlign.center, + style: const TextStyle(color: Colors.white), + ), + ), + ), + Expanded( + flex: percentBlack, + child: ColoredBox( + color: _blackBoxColor(context), + child: Text( + label(percentBlack), + textAlign: TextAlign.center, + style: const TextStyle(color: Colors.white), + ), + ), + ), + ], + ), + ); + } +} From ce6743e464cc814fb60ed9838a5b2a86e9ccdc69 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Wed, 20 Nov 2024 17:42:24 +0100 Subject: [PATCH 03/23] More work on analysis explorer view --- .../opening_explorer_repository.dart | 2 + lib/src/view/analysis/analysis_layout.dart | 39 ++- lib/src/view/analysis/analysis_screen.dart | 26 +- lib/src/view/analysis/analysis_settings.dart | 23 +- .../view/analysis/opening_explorer_view.dart | 236 ++++++++++++++++++ .../opening_explorer_screen.dart | 2 +- .../opening_explorer_widgets.dart | 9 +- 7 files changed, 299 insertions(+), 38 deletions(-) create mode 100644 lib/src/view/analysis/opening_explorer_view.dart diff --git a/lib/src/model/opening_explorer/opening_explorer_repository.dart b/lib/src/model/opening_explorer/opening_explorer_repository.dart index f8f6d5210b..a6b0921fc5 100644 --- a/lib/src/model/opening_explorer/opening_explorer_repository.dart +++ b/lib/src/model/opening_explorer/opening_explorer_repository.dart @@ -8,6 +8,7 @@ import 'package:lichess_mobile/src/model/common/speed.dart'; import 'package:lichess_mobile/src/model/opening_explorer/opening_explorer.dart'; import 'package:lichess_mobile/src/model/opening_explorer/opening_explorer_preferences.dart'; import 'package:lichess_mobile/src/network/http.dart'; +import 'package:lichess_mobile/src/utils/riverpod.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'opening_explorer_repository.g.dart'; @@ -20,6 +21,7 @@ class OpeningExplorer extends _$OpeningExplorer { Future<({OpeningExplorerEntry entry, bool isIndexing})?> build({ required String fen, }) async { + await ref.debounce(const Duration(milliseconds: 300)); ref.onDispose(() { _openingExplorerSubscription?.cancel(); }); diff --git a/lib/src/view/analysis/analysis_layout.dart b/lib/src/view/analysis/analysis_layout.dart index fbd27d9898..5ca457b409 100644 --- a/lib/src/view/analysis/analysis_layout.dart +++ b/lib/src/view/analysis/analysis_layout.dart @@ -1,5 +1,8 @@ import 'package:flutter/material.dart'; +import 'package:lichess_mobile/l10n/l10n.dart'; import 'package:lichess_mobile/src/constants.dart'; +import 'package:lichess_mobile/src/styles/lichess_icons.dart'; +import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/screen.dart'; import 'package:lichess_mobile/src/widgets/adaptive_action_sheet.dart'; import 'package:lichess_mobile/src/widgets/buttons.dart'; @@ -16,14 +19,27 @@ typedef EngineGaugeBuilder = Widget Function( Orientation orientation, ); -class AnalysisTab { - const AnalysisTab({ - required this.title, - required this.icon, - }); +enum AnalysisTab { + opening(Icons.explore), + moves(LichessIcons.flow_cascade), + summary(Icons.area_chart); + + const AnalysisTab(this.icon); - final String title; final IconData icon; + + String l10n(AppLocalizations l10n) { + switch (this) { + case AnalysisTab.opening: + return l10n.openingExplorer; + case AnalysisTab.moves: + // TODO: Add l10n + return 'Moves'; + case AnalysisTab.summary: + // TODO: Add l10n + return 'Summary'; + } + } } /// Indicator for the analysis tab, typically shown in the app bar. @@ -69,17 +85,16 @@ class _AppBarAnalysisTabIndicatorState Widget build(BuildContext context) { return AppBarIconButton( icon: Icon(widget.tabs[widget.controller.index].icon), - semanticsLabel: widget.tabs[widget.controller.index].title, + 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: (_) => Text(tab.title), + makeLabel: (context) => Text(tab.l10n(context.l10n)), onPressed: (_) { widget.controller.animateTo(widget.tabs.indexOf(tab)); - Navigator.of(context).pop(); }, ); }).toList(), @@ -101,12 +116,16 @@ class AnalysisLayout extends StatelessWidget { super.key, }); + /// The tab controller for the tab view. final TabController tabController; /// The builder for the board widget. final BoardBuilder boardBuilder; - /// The children of the tab bar view. + /// The children of the tab view. + /// + /// The length of this list must match the [controller]'s [TabController.length] + /// and the length of the [AppBarAnalysisTabIndicator.tabs] list. final List children; final EngineGaugeBuilder? engineGaugeBuilder; diff --git a/lib/src/view/analysis/analysis_screen.dart b/lib/src/view/analysis/analysis_screen.dart index 886fb8005d..d9a6476420 100644 --- a/lib/src/view/analysis/analysis_screen.dart +++ b/lib/src/view/analysis/analysis_screen.dart @@ -8,11 +8,11 @@ import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/game/game_repository_providers.dart'; import 'package:lichess_mobile/src/model/game/game_share_service.dart'; import 'package:lichess_mobile/src/network/http.dart'; -import 'package:lichess_mobile/src/styles/lichess_icons.dart'; 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/analysis/analysis_share_screen.dart'; +import 'package:lichess_mobile/src/view/analysis/opening_explorer_view.dart'; import 'package:lichess_mobile/src/view/analysis/server_analysis.dart'; import 'package:lichess_mobile/src/view/board_editor/board_editor_screen.dart'; import 'package:lichess_mobile/src/view/engine/engine_depth.dart'; @@ -131,18 +131,16 @@ class _LoadedAnalysisScreenState extends ConsumerState<_LoadedAnalysisScreen> void initState() { super.initState(); tabs = [ - const AnalysisTab( - title: 'Moves', - icon: LichessIcons.flow_cascade, - ), - if (widget.options.canShowGameSummary) - const AnalysisTab( - title: 'Summary', - icon: Icons.area_chart, - ), + AnalysisTab.opening, + AnalysisTab.moves, + if (widget.options.canShowGameSummary) AnalysisTab.summary, ]; - _tabController = TabController(vsync: this, length: tabs.length); + _tabController = TabController( + vsync: this, + initialIndex: 1, + length: tabs.length, + ); } @override @@ -274,6 +272,7 @@ class _Body extends ConsumerWidget { : null, bottomBar: _BottomBar(pgn: pgn, options: options), children: [ + OpeningExplorerView(pgn: pgn, options: options), AnalysisTreeView(pgn, options), if (options.canShowGameSummary) ServerAnalysisSummary(pgn, options), ], @@ -304,6 +303,11 @@ class _BottomBar extends ConsumerWidget { }, icon: Icons.menu, ), + BottomBarButton( + label: context.l10n.flipBoard, + onTap: () => ref.read(ctrlProvider.notifier).toggleBoard(), + icon: CupertinoIcons.arrow_2_squarepath, + ), RepeatButton( onLongPress: analysisState.canGoBack ? () => _moveBackward(ref) : null, diff --git a/lib/src/view/analysis/analysis_settings.dart b/lib/src/view/analysis/analysis_settings.dart index f80fb44473..5f7b409bc2 100644 --- a/lib/src/view/analysis/analysis_settings.dart +++ b/lib/src/view/analysis/analysis_settings.dart @@ -4,8 +4,8 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/model/analysis/analysis_controller.dart'; import 'package:lichess_mobile/src/model/analysis/analysis_preferences.dart'; import 'package:lichess_mobile/src/model/engine/evaluation_service.dart'; -import 'package:lichess_mobile/src/model/settings/general_preferences.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; +import 'package:lichess_mobile/src/view/opening_explorer/opening_explorer_settings.dart'; import 'package:lichess_mobile/src/widgets/adaptive_bottom_sheet.dart'; import 'package:lichess_mobile/src/widgets/list.dart'; import 'package:lichess_mobile/src/widgets/non_linear_slider.dart'; @@ -26,12 +26,20 @@ class AnalysisSettings extends ConsumerWidget { ctrlProvider.select((s) => s.isEngineAvailable), ); final prefs = ref.watch(analysisPreferencesProvider); - final isSoundEnabled = ref.watch( - generalPreferencesProvider.select((pref) => pref.isSoundEnabled), - ); return BottomSheetScrollableContainer( children: [ + PlatformListTile( + title: Text(context.l10n.openingExplorer), + onTap: () => showAdaptiveBottomSheet( + context: context, + isScrollControlled: true, + showDragHandle: true, + isDismissible: true, + builder: (_) => OpeningExplorerSettings(pgn, options), + ), + trailing: const Icon(CupertinoIcons.chevron_right), + ), SwitchSettingTile( title: Text(context.l10n.toggleLocalEvaluation), value: prefs.enableLocalEvaluation, @@ -128,13 +136,6 @@ class AnalysisSettings extends ConsumerWidget { .read(analysisPreferencesProvider.notifier) .togglePgnComments(), ), - SwitchSettingTile( - title: Text(context.l10n.sound), - value: isSoundEnabled, - onChanged: (value) { - ref.read(generalPreferencesProvider.notifier).toggleSoundEnabled(); - }, - ), ], ); } diff --git a/lib/src/view/analysis/opening_explorer_view.dart b/lib/src/view/analysis/opening_explorer_view.dart new file mode 100644 index 0000000000..80c0b0a964 --- /dev/null +++ b/lib/src/view/analysis/opening_explorer_view.dart @@ -0,0 +1,236 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:lichess_mobile/src/constants.dart'; +import 'package:lichess_mobile/src/model/analysis/analysis_controller.dart'; +import 'package:lichess_mobile/src/model/opening_explorer/opening_explorer.dart'; +import 'package:lichess_mobile/src/model/opening_explorer/opening_explorer_preferences.dart'; +import 'package:lichess_mobile/src/model/opening_explorer/opening_explorer_repository.dart'; +import 'package:lichess_mobile/src/utils/l10n_context.dart'; +import 'package:lichess_mobile/src/utils/screen.dart'; +import 'package:lichess_mobile/src/view/opening_explorer/opening_explorer_widgets.dart'; +import 'package:lichess_mobile/src/widgets/shimmer.dart'; + +const _kTableRowVerticalPadding = 12.0; +const _kTableRowHorizontalPadding = 8.0; +const _kTableRowPadding = EdgeInsets.symmetric( + horizontal: _kTableRowHorizontalPadding, + vertical: _kTableRowVerticalPadding, +); + +class OpeningExplorerView extends ConsumerStatefulWidget { + const OpeningExplorerView({required this.pgn, required this.options}); + + final String pgn; + final AnalysisOptions options; + + @override + ConsumerState createState() => _OpeningExplorerState(); +} + +class _OpeningExplorerState extends ConsumerState { + final Map cache = {}; + + /// Last explorer content that was successfully loaded. This is used to + /// display a loading indicator while the new content is being fetched. + List? lastExplorerWidgets; + + @override + Widget build(BuildContext context) { + final analysisState = + ref.watch(analysisControllerProvider(widget.pgn, widget.options)); + + if (analysisState.position.ply >= 50) { + return _OpeningExplorerView( + isLoading: false, + children: [ + OpeningExplorerMoveTable.maxDepth( + pgn: widget.pgn, + options: widget.options, + ), + ], + ); + } + + final prefs = ref.watch(openingExplorerPreferencesProvider); + + if (prefs.db == OpeningDatabase.player && prefs.playerDb.username == null) { + return const _OpeningExplorerView( + isLoading: false, + children: [ + Padding( + padding: _kTableRowPadding, + child: Center( + // TODO: l10n + child: Text('Select a Lichess player in the settings.'), + ), + ), + ], + ); + } + + final cacheKey = OpeningExplorerCacheKey( + fen: analysisState.position.fen, + prefs: prefs, + ); + final cacheOpeningExplorer = cache[cacheKey]; + final openingExplorerAsync = cacheOpeningExplorer != null + ? AsyncValue.data( + (entry: cacheOpeningExplorer, isIndexing: false), + ) + : ref.watch(openingExplorerProvider(fen: analysisState.position.fen)); + + if (cacheOpeningExplorer == null) { + ref.listen(openingExplorerProvider(fen: analysisState.position.fen), + (_, curAsync) { + curAsync.whenData((cur) { + if (cur != null && !cur.isIndexing) { + cache[cacheKey] = cur.entry; + } + }); + }); + } + + final isLoading = + openingExplorerAsync.isLoading || openingExplorerAsync.value == null; + + return _OpeningExplorerView( + isLoading: isLoading, + children: openingExplorerAsync.when( + data: (openingExplorer) { + if (openingExplorer == null) { + return lastExplorerWidgets ?? + [ + Shimmer( + child: ShimmerLoading( + isLoading: true, + child: OpeningExplorerMoveTable.loading( + pgn: widget.pgn, + options: widget.options, + ), + ), + ), + ]; + } + + final topGames = openingExplorer.entry.topGames; + final recentGames = openingExplorer.entry.recentGames; + + final ply = analysisState.position.ply; + + final children = [ + OpeningExplorerMoveTable( + moves: openingExplorer.entry.moves, + whiteWins: openingExplorer.entry.white, + draws: openingExplorer.entry.draws, + blackWins: openingExplorer.entry.black, + pgn: widget.pgn, + options: widget.options, + ), + if (topGames != null && topGames.isNotEmpty) ...[ + OpeningExplorerHeaderTile( + key: const Key('topGamesHeader'), + child: Text(context.l10n.topGames), + ), + ...List.generate( + topGames.length, + (int index) { + return OpeningExplorerGameTile( + key: Key('top-game-${topGames.get(index).id}'), + game: topGames.get(index), + color: index.isEven + ? Theme.of(context).colorScheme.surfaceContainerLow + : Theme.of(context).colorScheme.surfaceContainerHigh, + ply: ply, + ); + }, + growable: false, + ), + ], + if (recentGames != null && recentGames.isNotEmpty) ...[ + OpeningExplorerHeaderTile( + key: const Key('recentGamesHeader'), + child: Text(context.l10n.recentGames), + ), + ...List.generate( + recentGames.length, + (int index) { + return OpeningExplorerGameTile( + key: Key('recent-game-${recentGames.get(index).id}'), + game: recentGames.get(index), + color: index.isEven + ? Theme.of(context).colorScheme.surfaceContainerLow + : Theme.of(context).colorScheme.surfaceContainerHigh, + ply: ply, + ); + }, + growable: false, + ), + ], + ]; + + lastExplorerWidgets = children; + + return children; + }, + loading: () => + lastExplorerWidgets ?? + [ + Shimmer( + child: ShimmerLoading( + isLoading: true, + child: OpeningExplorerMoveTable.loading( + pgn: widget.pgn, + options: widget.options, + ), + ), + ), + ], + error: (e, s) { + debugPrint( + 'SEVERE: [OpeningExplorerView] could not load opening explorer data; $e\n$s', + ); + return [ + Center( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Text(e.toString()), + ), + ), + ]; + }, + ), + ); + } +} + +class _OpeningExplorerView extends StatelessWidget { + const _OpeningExplorerView({ + required this.children, + required this.isLoading, + }); + + final List children; + final bool isLoading; + + @override + Widget build(BuildContext context) { + final isTablet = isTabletOrLarger(context); + final loadingOverlay = Positioned.fill( + child: IgnorePointer(ignoring: !isLoading), + ); + + return Stack( + children: [ + ListView( + padding: isTablet + ? const EdgeInsets.symmetric( + horizontal: kTabletBoardTableSidePadding, + ) + : EdgeInsets.zero, + children: children, + ), + loadingOverlay, + ], + ); + } +} diff --git a/lib/src/view/opening_explorer/opening_explorer_screen.dart b/lib/src/view/opening_explorer/opening_explorer_screen.dart index 341afecb60..9ade504e4e 100644 --- a/lib/src/view/opening_explorer/opening_explorer_screen.dart +++ b/lib/src/view/opening_explorer/opening_explorer_screen.dart @@ -86,7 +86,7 @@ class _OpeningExplorerState extends ConsumerState { const SizedBox(width: 6.0), Expanded( child: Text( - '${opening.eco.isEmpty ? "" : "${opening.eco} "}${opening.name}', + opening.name, style: TextStyle( color: Theme.of(context).colorScheme.onSecondaryContainer, diff --git a/lib/src/view/opening_explorer/opening_explorer_widgets.dart b/lib/src/view/opening_explorer/opening_explorer_widgets.dart index f0374e07ab..2b19204c49 100644 --- a/lib/src/view/opening_explorer/opening_explorer_widgets.dart +++ b/lib/src/view/opening_explorer/opening_explorer_widgets.dart @@ -10,7 +10,7 @@ import 'package:lichess_mobile/src/utils/navigation.dart'; import 'package:lichess_mobile/src/view/game/archived_game_screen.dart'; import 'package:lichess_mobile/src/widgets/buttons.dart'; -const _kTableRowVerticalPadding = 12.0; +const _kTableRowVerticalPadding = 10.0; const _kTableRowHorizontalPadding = 8.0; const _kTableRowPadding = EdgeInsets.symmetric( horizontal: _kTableRowHorizontalPadding, @@ -86,7 +86,6 @@ class OpeningExplorerMoveTable extends ConsumerWidget { final games = whiteWins + draws + blackWins; final ctrlProvider = analysisControllerProvider(pgn, options); - const topPadding = EdgeInsets.only(top: _kTableRowVerticalPadding / 2); const headerTextStyle = TextStyle(fontSize: 12); return Table( @@ -98,15 +97,15 @@ class OpeningExplorerMoveTable extends ConsumerWidget { ), children: [ Padding( - padding: _kTableRowPadding.subtract(topPadding), + padding: _kTableRowPadding, child: Text(context.l10n.move, style: headerTextStyle), ), Padding( - padding: _kTableRowPadding.subtract(topPadding), + padding: _kTableRowPadding, child: Text(context.l10n.games, style: headerTextStyle), ), Padding( - padding: _kTableRowPadding.subtract(topPadding), + padding: _kTableRowPadding, child: Text(context.l10n.whiteDrawBlack, style: headerTextStyle), ), ], From 417ce8ba5c4bfe4b381e0b0be5176454353dfada Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Wed, 20 Nov 2024 17:47:03 +0100 Subject: [PATCH 04/23] Remove unused code --- .../model/analysis/analysis_controller.dart | 25 ------------------- lib/src/view/analysis/analysis_settings.dart | 1 - 2 files changed, 26 deletions(-) diff --git a/lib/src/model/analysis/analysis_controller.dart b/lib/src/model/analysis/analysis_controller.dart index 3f937ca4c7..62cd5bafab 100644 --- a/lib/src/model/analysis/analysis_controller.dart +++ b/lib/src/model/analysis/analysis_controller.dart @@ -163,7 +163,6 @@ class AnalysisController extends _$AnalysisController contextOpening: options.opening, isLocalEvaluationAllowed: options.isLocalEvaluationAllowed, isLocalEvaluationEnabled: prefs.enableLocalEvaluation, - displayMode: DisplayMode.moves, playersAnalysis: options.serverAnalysis, acplChartData: options.serverAnalysis != null ? _makeAcplChartData() : null, @@ -380,10 +379,6 @@ class AnalysisController extends _$AnalysisController state = state.copyWith(pgnHeaders: headers); } - void setDisplayMode(DisplayMode mode) { - state = state.copyWith(displayMode: mode); - } - Future requestServerAnalysis() { if (state.canRequestServerAnalysis) { final service = ref.read(serverAnalysisServiceProvider); @@ -649,11 +644,6 @@ class AnalysisController extends _$AnalysisController } } -enum DisplayMode { - moves, - summary, -} - @freezed class AnalysisState with _$AnalysisState { const AnalysisState._(); @@ -690,11 +680,6 @@ class AnalysisState with _$AnalysisState { /// Whether the user has enabled local evaluation. required bool isLocalEvaluationEnabled, - /// The display mode of the analysis. - /// - /// It can be either moves, summary or opening explorer. - required DisplayMode displayMode, - /// The last move played. Move? lastMove, @@ -743,8 +728,6 @@ class AnalysisState with _$AnalysisState { !hasServerAnalysis && pgnHeaders['Result'] != '*'; - bool get canShowGameSummary => hasServerAnalysis || canRequestServerAnalysis; - bool get hasServerAnalysis => playersAnalysis != null; /// Whether an evaluation can be available @@ -771,14 +754,6 @@ class AnalysisState with _$AnalysisState { position: position, savedEval: currentNode.eval ?? currentNode.serverEval, ); - - AnalysisOptions get openingExplorerOptions => AnalysisOptions( - id: standaloneOpeningExplorerId, - isLocalEvaluationAllowed: false, - orientation: pov, - variant: variant, - initialMoveCursor: currentPath.size, - ); } @freezed diff --git a/lib/src/view/analysis/analysis_settings.dart b/lib/src/view/analysis/analysis_settings.dart index 5f7b409bc2..415548cb49 100644 --- a/lib/src/view/analysis/analysis_settings.dart +++ b/lib/src/view/analysis/analysis_settings.dart @@ -1,5 +1,4 @@ import 'package:flutter/cupertino.dart'; -import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/model/analysis/analysis_controller.dart'; import 'package:lichess_mobile/src/model/analysis/analysis_preferences.dart'; From d4383bf61f8334f39b1ffabec4b95ad6bebbf0bb Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Wed, 20 Nov 2024 17:51:55 +0100 Subject: [PATCH 05/23] Fix explorer tests --- .../opening_explorer_screen_test.dart | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/test/view/opening_explorer/opening_explorer_screen_test.dart b/test/view/opening_explorer/opening_explorer_screen_test.dart index a442d6f50e..32b7e88a4f 100644 --- a/test/view/opening_explorer/opening_explorer_screen_test.dart +++ b/test/view/opening_explorer/opening_explorer_screen_test.dart @@ -74,8 +74,8 @@ void main() { ); await tester.pumpWidget(app); - // wait for opening explorer data to load - await tester.pump(const Duration(milliseconds: 50)); + // wait for opening explorer data to load (taking debounce delay into account) + await tester.pump(const Duration(milliseconds: 350)); final moves = [ 'e4', @@ -102,8 +102,6 @@ void main() { // find.byType(OpeningExplorerGameTile), // findsNWidgets(2), // ); - - await tester.pump(const Duration(milliseconds: 50)); }, variant: kPlatformVariant, ); @@ -133,8 +131,8 @@ void main() { ); await tester.pumpWidget(app); - // wait for opening explorer data to load - await tester.pump(const Duration(milliseconds: 50)); + // wait for opening explorer data to load (taking debounce delay into account) + await tester.pump(const Duration(milliseconds: 350)); final moves = [ 'd4', @@ -157,8 +155,6 @@ void main() { // find.byType(OpeningExplorerGameTile), // findsOneWidget, // ); - - await tester.pump(const Duration(milliseconds: 50)); }, variant: kPlatformVariant, ); @@ -189,8 +185,8 @@ void main() { ); await tester.pumpWidget(app); - // wait for opening explorer data to load - await tester.pump(const Duration(milliseconds: 50)); + // wait for opening explorer data to load (taking debounce delay into account) + await tester.pump(const Duration(milliseconds: 350)); final moves = [ 'c4', @@ -213,8 +209,6 @@ void main() { // find.byType(OpeningExplorerGameTile), // findsOneWidget, // ); - - await tester.pump(const Duration(milliseconds: 50)); }, variant: kPlatformVariant, ); From bd40e1ac9196dd40879ea662277bc67e4262dfdc Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Wed, 20 Nov 2024 22:14:02 +0100 Subject: [PATCH 06/23] Start to simplify analysis controller interface --- .../model/analysis/analysis_controller.dart | 6 ++- lib/src/model/game/game_controller.dart | 1 + lib/src/view/analysis/analysis_board.dart | 4 +- lib/src/view/analysis/analysis_screen.dart | 54 ++++++++----------- lib/src/view/analysis/analysis_settings.dart | 7 ++- .../view/analysis/analysis_share_screen.dart | 17 +++--- .../view/analysis/opening_explorer_view.dart | 10 +--- lib/src/view/analysis/server_analysis.dart | 22 ++++---- lib/src/view/analysis/tree_view.dart | 8 +-- .../board_editor/board_editor_screen.dart | 1 + .../offline_correspondence_game_screen.dart | 1 + lib/src/view/game/archived_game_screen.dart | 1 + lib/src/view/game/game_list_tile.dart | 1 + lib/src/view/game/game_result_dialog.dart | 1 + .../opening_explorer_screen.dart | 48 +++++------------ .../opening_explorer_settings.dart | 3 +- .../opening_explorer_widgets.dart | 6 +-- lib/src/view/puzzle/puzzle_screen.dart | 1 + lib/src/view/puzzle/streak_screen.dart | 1 + lib/src/view/study/study_bottom_bar.dart | 1 + lib/src/view/tools/load_position_screen.dart | 4 +- lib/src/view/tools/tools_tab_screen.dart | 3 +- test/view/analysis/analysis_screen_test.dart | 3 ++ .../opening_explorer_screen_test.dart | 4 +- 24 files changed, 82 insertions(+), 126 deletions(-) diff --git a/lib/src/model/analysis/analysis_controller.dart b/lib/src/model/analysis/analysis_controller.dart index 62cd5bafab..6be0809cce 100644 --- a/lib/src/model/analysis/analysis_controller.dart +++ b/lib/src/model/analysis/analysis_controller.dart @@ -39,6 +39,8 @@ bool _isStandaloneAnalysis(StringId id) => class AnalysisOptions with _$AnalysisOptions { const AnalysisOptions._(); const factory AnalysisOptions({ + required String pgn, + /// The ID of the analysis. Can be a game ID or a standalone ID. required StringId id, required bool isLocalEvaluationAllowed, @@ -73,7 +75,7 @@ class AnalysisController extends _$AnalysisController Timer? _startEngineEvalTimer; @override - AnalysisState build(String pgn, AnalysisOptions options) { + AnalysisState build(AnalysisOptions options) { final evaluationService = ref.watch(evaluationServiceProvider); final serverAnalysisService = ref.watch(serverAnalysisServiceProvider); @@ -97,7 +99,7 @@ class AnalysisController extends _$AnalysisController Move? lastMove; final game = PgnGame.parsePgn( - pgn, + options.pgn, initHeaders: () => options.isLichessGameAnalysis ? {} : { diff --git a/lib/src/model/game/game_controller.dart b/lib/src/model/game/game_controller.dart index 51a8cbb023..60a7efd0b7 100644 --- a/lib/src/model/game/game_controller.dart +++ b/lib/src/model/game/game_controller.dart @@ -1183,6 +1183,7 @@ class GameState with _$GameState { String get analysisPgn => game.makePgn(); AnalysisOptions get analysisOptions => AnalysisOptions( + pgn: analysisPgn, isLocalEvaluationAllowed: true, variant: game.meta.variant, initialMoveCursor: stepCursor, diff --git a/lib/src/view/analysis/analysis_board.dart b/lib/src/view/analysis/analysis_board.dart index bfb74f672e..6e60630a7e 100644 --- a/lib/src/view/analysis/analysis_board.dart +++ b/lib/src/view/analysis/analysis_board.dart @@ -15,14 +15,12 @@ import 'package:lichess_mobile/src/widgets/pgn.dart'; class AnalysisBoard extends ConsumerStatefulWidget { const AnalysisBoard( - this.pgn, this.options, this.boardSize, { this.borderRadius, this.enableDrawingShapes = true, }); - final String pgn; final AnalysisOptions options; final double boardSize; final BorderRadiusGeometry? borderRadius; @@ -38,7 +36,7 @@ class AnalysisBoardState extends ConsumerState { @override Widget build(BuildContext context) { - final ctrlProvider = analysisControllerProvider(widget.pgn, widget.options); + final ctrlProvider = analysisControllerProvider(widget.options); final analysisState = ref.watch(ctrlProvider); final boardPrefs = ref.watch(boardPreferencesProvider); final showBestMoveArrow = ref.watch( diff --git a/lib/src/view/analysis/analysis_screen.dart b/lib/src/view/analysis/analysis_screen.dart index d9a6476420..2276f60d94 100644 --- a/lib/src/view/analysis/analysis_screen.dart +++ b/lib/src/view/analysis/analysis_screen.dart @@ -55,8 +55,9 @@ class AnalysisScreen extends StatelessWidget { enableDrawingShapes: enableDrawingShapes, ) : _LoadedAnalysisScreen( - options: options, - pgn: pgnOrId, + options: options.copyWith( + pgn: pgnOrId, + ), enableDrawingShapes: enableDrawingShapes, ); } @@ -90,8 +91,8 @@ class _LoadGame extends ConsumerWidget { opening: game.meta.opening, division: game.meta.division, serverAnalysis: serverAnalysis, + pgn: game.makePgn(), ), - pgn: game.makePgn(), enableDrawingShapes: enableDrawingShapes, ); }, @@ -108,12 +109,10 @@ class _LoadGame extends ConsumerWidget { class _LoadedAnalysisScreen extends ConsumerStatefulWidget { const _LoadedAnalysisScreen({ required this.options, - required this.pgn, required this.enableDrawingShapes, }); final AnalysisOptions options; - final String pgn; final bool enableDrawingShapes; @@ -151,7 +150,7 @@ class _LoadedAnalysisScreenState extends ConsumerState<_LoadedAnalysisScreen> @override Widget build(BuildContext context) { - final ctrlProvider = analysisControllerProvider(widget.pgn, widget.options); + final ctrlProvider = analysisControllerProvider(widget.options); final currentNodeEval = ref.watch(ctrlProvider.select((value) => value.currentNode.eval)); @@ -171,7 +170,7 @@ class _LoadedAnalysisScreenState extends ConsumerState<_LoadedAnalysisScreen> isScrollControlled: true, showDragHandle: true, isDismissible: true, - builder: (_) => AnalysisSettings(widget.pgn, widget.options), + builder: (_) => AnalysisSettings(widget.options), ), semanticsLabel: context.l10n.settingsSettings, icon: const Icon(Icons.settings), @@ -180,7 +179,6 @@ class _LoadedAnalysisScreenState extends ConsumerState<_LoadedAnalysisScreen> ), body: _Body( controller: _tabController, - pgn: widget.pgn, options: widget.options, enableDrawingShapes: widget.enableDrawingShapes, ), @@ -212,13 +210,11 @@ class _Title extends StatelessWidget { class _Body extends ConsumerWidget { const _Body({ required this.controller, - required this.pgn, required this.options, required this.enableDrawingShapes, }); final TabController controller; - final String pgn; final AnalysisOptions options; final bool enableDrawingShapes; @@ -228,7 +224,7 @@ class _Body extends ConsumerWidget { analysisPreferencesProvider.select((value) => value.showEvaluationGauge), ); - final ctrlProvider = analysisControllerProvider(pgn, options); + final ctrlProvider = analysisControllerProvider(options); final analysisState = ref.watch(ctrlProvider); final isEngineAvailable = analysisState.isEngineAvailable; @@ -238,7 +234,6 @@ class _Body extends ConsumerWidget { return AnalysisLayout( tabController: controller, boardBuilder: (context, boardSize, borderRadius) => AnalysisBoard( - pgn, options, boardSize, borderRadius: borderRadius, @@ -270,28 +265,24 @@ class _Body extends ConsumerWidget { isGameOver: currentNode.position.isGameOver, ) : null, - bottomBar: _BottomBar(pgn: pgn, options: options), + bottomBar: _BottomBar(options: options), children: [ - OpeningExplorerView(pgn: pgn, options: options), - AnalysisTreeView(pgn, options), - if (options.canShowGameSummary) ServerAnalysisSummary(pgn, options), + OpeningExplorerView(options: options), + AnalysisTreeView(options), + if (options.canShowGameSummary) ServerAnalysisSummary(options), ], ); } } class _BottomBar extends ConsumerWidget { - const _BottomBar({ - required this.pgn, - required this.options, - }); + const _BottomBar({required this.options}); - final String pgn; final AnalysisOptions options; @override Widget build(BuildContext context, WidgetRef ref) { - final ctrlProvider = analysisControllerProvider(pgn, options); + final ctrlProvider = analysisControllerProvider(options); final analysisState = ref.watch(ctrlProvider); return BottomBar( @@ -334,10 +325,9 @@ class _BottomBar extends ConsumerWidget { } void _moveForward(WidgetRef ref) => - ref.read(analysisControllerProvider(pgn, options).notifier).userNext(); - void _moveBackward(WidgetRef ref) => ref - .read(analysisControllerProvider(pgn, options).notifier) - .userPrevious(); + ref.read(analysisControllerProvider(options).notifier).userNext(); + void _moveBackward(WidgetRef ref) => + ref.read(analysisControllerProvider(options).notifier).userPrevious(); Future _showAnalysisMenu(BuildContext context, WidgetRef ref) { return showAdaptiveActionSheet( @@ -347,15 +337,14 @@ class _BottomBar extends ConsumerWidget { makeLabel: (context) => Text(context.l10n.flipBoard), onPressed: (context) { ref - .read(analysisControllerProvider(pgn, options).notifier) + .read(analysisControllerProvider(options).notifier) .toggleBoard(); }, ), BottomSheetAction( makeLabel: (context) => Text(context.l10n.boardEditor), onPressed: (context) { - final analysisState = - ref.read(analysisControllerProvider(pgn, options)); + final analysisState = ref.read(analysisControllerProvider(options)); final boardFen = analysisState.position.fen; pushPlatformRoute( context, @@ -372,15 +361,14 @@ class _BottomBar extends ConsumerWidget { pushPlatformRoute( context, title: context.l10n.studyShareAndExport, - builder: (_) => AnalysisShareScreen(pgn: pgn, options: options), + builder: (_) => AnalysisShareScreen(options: options), ); }, ), BottomSheetAction( makeLabel: (context) => Text(context.l10n.mobileSharePositionAsFEN), onPressed: (_) { - final analysisState = - ref.read(analysisControllerProvider(pgn, options)); + final analysisState = ref.read(analysisControllerProvider(options)); launchShareDialog( context, text: analysisState.position.fen, @@ -394,7 +382,7 @@ class _BottomBar extends ConsumerWidget { onPressed: (_) async { final gameId = options.gameAnyId!.gameId; final analysisState = - ref.read(analysisControllerProvider(pgn, options)); + ref.read(analysisControllerProvider(options)); try { final image = await ref.read(gameShareServiceProvider).screenshotPosition( diff --git a/lib/src/view/analysis/analysis_settings.dart b/lib/src/view/analysis/analysis_settings.dart index 415548cb49..380687363a 100644 --- a/lib/src/view/analysis/analysis_settings.dart +++ b/lib/src/view/analysis/analysis_settings.dart @@ -11,14 +11,13 @@ import 'package:lichess_mobile/src/widgets/non_linear_slider.dart'; import 'package:lichess_mobile/src/widgets/settings.dart'; class AnalysisSettings extends ConsumerWidget { - const AnalysisSettings(this.pgn, this.options); + const AnalysisSettings(this.options); - final String pgn; final AnalysisOptions options; @override Widget build(BuildContext context, WidgetRef ref) { - final ctrlProvider = analysisControllerProvider(pgn, options); + final ctrlProvider = analysisControllerProvider(options); final isLocalEvaluationAllowed = ref.watch(ctrlProvider.select((s) => s.isLocalEvaluationAllowed)); final isEngineAvailable = ref.watch( @@ -35,7 +34,7 @@ class AnalysisSettings extends ConsumerWidget { isScrollControlled: true, showDragHandle: true, isDismissible: true, - builder: (_) => OpeningExplorerSettings(pgn, options), + builder: (_) => OpeningExplorerSettings(options), ), trailing: const Icon(CupertinoIcons.chevron_right), ), diff --git a/lib/src/view/analysis/analysis_share_screen.dart b/lib/src/view/analysis/analysis_share_screen.dart index 97e626ad3f..785e8b0cf7 100644 --- a/lib/src/view/analysis/analysis_share_screen.dart +++ b/lib/src/view/analysis/analysis_share_screen.dart @@ -17,9 +17,8 @@ import 'package:lichess_mobile/src/widgets/platform_scaffold.dart'; final _dateFormatter = DateFormat('yyyy.MM.dd'); class AnalysisShareScreen extends StatelessWidget { - const AnalysisShareScreen({required this.pgn, required this.options}); + const AnalysisShareScreen({required this.options}); - final String pgn; final AnalysisOptions options; @override @@ -28,7 +27,7 @@ class AnalysisShareScreen extends StatelessWidget { appBar: PlatformAppBar( title: Text(context.l10n.studyShareAndExport), ), - body: _EditPgnTagsForm(pgn, options), + body: _EditPgnTagsForm(options), ); } } @@ -41,9 +40,8 @@ const Set _ratingHeaders = { }; class _EditPgnTagsForm extends ConsumerStatefulWidget { - const _EditPgnTagsForm(this.pgn, this.options); + const _EditPgnTagsForm(this.options); - final String pgn; final AnalysisOptions options; @override @@ -57,7 +55,7 @@ class _EditPgnTagsFormState extends ConsumerState<_EditPgnTagsForm> { @override void initState() { super.initState(); - final ctrlProvider = analysisControllerProvider(widget.pgn, widget.options); + final ctrlProvider = analysisControllerProvider(widget.options); final pgnHeaders = ref.read(ctrlProvider).pgnHeaders; for (final entry in pgnHeaders.entries) { @@ -87,7 +85,7 @@ class _EditPgnTagsFormState extends ConsumerState<_EditPgnTagsForm> { @override Widget build(BuildContext context) { - final ctrlProvider = analysisControllerProvider(widget.pgn, widget.options); + final ctrlProvider = analysisControllerProvider(widget.options); final pgnHeaders = ref.watch(ctrlProvider.select((c) => c.pgnHeaders)); final showRatingAsync = ref.watch(showRatingsPrefProvider); @@ -181,7 +179,6 @@ class _EditPgnTagsFormState extends ConsumerState<_EditPgnTagsForm> { text: ref .read( analysisControllerProvider( - widget.pgn, widget.options, ).notifier, ) @@ -207,7 +204,7 @@ class _EditPgnTagsFormState extends ConsumerState<_EditPgnTagsForm> { required BuildContext context, required void Function() onEntryChanged, }) { - final ctrlProvider = analysisControllerProvider(widget.pgn, widget.options); + final ctrlProvider = analysisControllerProvider(widget.options); if (Theme.of(context).platform == TargetPlatform.iOS) { return showCupertinoModalPopup( context: context, @@ -272,7 +269,7 @@ class _EditPgnTagsFormState extends ConsumerState<_EditPgnTagsForm> { onSelectedItemChanged: (choice) { ref .read( - analysisControllerProvider(widget.pgn, widget.options).notifier, + analysisControllerProvider(widget.options).notifier, ) .updatePgnHeader( entry.key, diff --git a/lib/src/view/analysis/opening_explorer_view.dart b/lib/src/view/analysis/opening_explorer_view.dart index 80c0b0a964..b06f1efc34 100644 --- a/lib/src/view/analysis/opening_explorer_view.dart +++ b/lib/src/view/analysis/opening_explorer_view.dart @@ -18,9 +18,8 @@ const _kTableRowPadding = EdgeInsets.symmetric( ); class OpeningExplorerView extends ConsumerStatefulWidget { - const OpeningExplorerView({required this.pgn, required this.options}); + const OpeningExplorerView({required this.options}); - final String pgn; final AnalysisOptions options; @override @@ -36,15 +35,13 @@ class _OpeningExplorerState extends ConsumerState { @override Widget build(BuildContext context) { - final analysisState = - ref.watch(analysisControllerProvider(widget.pgn, widget.options)); + final analysisState = ref.watch(analysisControllerProvider(widget.options)); if (analysisState.position.ply >= 50) { return _OpeningExplorerView( isLoading: false, children: [ OpeningExplorerMoveTable.maxDepth( - pgn: widget.pgn, options: widget.options, ), ], @@ -104,7 +101,6 @@ class _OpeningExplorerState extends ConsumerState { child: ShimmerLoading( isLoading: true, child: OpeningExplorerMoveTable.loading( - pgn: widget.pgn, options: widget.options, ), ), @@ -123,7 +119,6 @@ class _OpeningExplorerState extends ConsumerState { whiteWins: openingExplorer.entry.white, draws: openingExplorer.entry.draws, blackWins: openingExplorer.entry.black, - pgn: widget.pgn, options: widget.options, ), if (topGames != null && topGames.isNotEmpty) ...[ @@ -179,7 +174,6 @@ class _OpeningExplorerState extends ConsumerState { child: ShimmerLoading( isLoading: true, child: OpeningExplorerMoveTable.loading( - pgn: widget.pgn, options: widget.options, ), ), diff --git a/lib/src/view/analysis/server_analysis.dart b/lib/src/view/analysis/server_analysis.dart index 93510a1f6b..ba0ac8f359 100644 --- a/lib/src/view/analysis/server_analysis.dart +++ b/lib/src/view/analysis/server_analysis.dart @@ -16,14 +16,13 @@ import 'package:lichess_mobile/src/widgets/buttons.dart'; import 'package:lichess_mobile/src/widgets/feedback.dart'; class ServerAnalysisSummary extends ConsumerWidget { - const ServerAnalysisSummary(this.pgn, this.options); + const ServerAnalysisSummary(this.options); - final String pgn; final AnalysisOptions options; @override Widget build(BuildContext context, WidgetRef ref) { - final ctrlProvider = analysisControllerProvider(pgn, options); + final ctrlProvider = analysisControllerProvider(options); final playersAnalysis = ref.watch(ctrlProvider.select((value) => value.playersAnalysis)); final pgnHeaders = @@ -38,7 +37,7 @@ class ServerAnalysisSummary extends ConsumerWidget { padding: EdgeInsets.only(top: 16.0), child: WaitingForServerAnalysis(), ), - AcplChart(pgn, options), + AcplChart(options), Center( child: SizedBox( width: math.min(MediaQuery.sizeOf(context).width, 500), @@ -321,9 +320,8 @@ class _SummaryPlayerName extends StatelessWidget { } class AcplChart extends ConsumerWidget { - const AcplChart(this.pgn, this.options); + const AcplChart(this.options); - final String pgn; final AnalysisOptions options; @override @@ -359,23 +357,21 @@ class AcplChart extends ConsumerWidget { ); final data = ref.watch( - analysisControllerProvider(pgn, options) + analysisControllerProvider(options) .select((value) => value.acplChartData), ); final rootPly = ref.watch( - analysisControllerProvider(pgn, options) + analysisControllerProvider(options) .select((value) => value.root.position.ply), ); final currentNode = ref.watch( - analysisControllerProvider(pgn, options) - .select((value) => value.currentNode), + analysisControllerProvider(options).select((value) => value.currentNode), ); final isOnMainline = ref.watch( - analysisControllerProvider(pgn, options) - .select((value) => value.isOnMainline), + analysisControllerProvider(options).select((value) => value.isOnMainline), ); if (data == null) { @@ -450,7 +446,7 @@ class AcplChart extends ConsumerWidget { ); final closestNodeIndex = closestSpot.x.round(); ref - .read(analysisControllerProvider(pgn, options).notifier) + .read(analysisControllerProvider(options).notifier) .jumpToNthNodeOnMainline(closestNodeIndex); } }, diff --git a/lib/src/view/analysis/tree_view.dart b/lib/src/view/analysis/tree_view.dart index 69c5b86978..de648427d7 100644 --- a/lib/src/view/analysis/tree_view.dart +++ b/lib/src/view/analysis/tree_view.dart @@ -9,17 +9,13 @@ import 'package:lichess_mobile/src/widgets/pgn.dart'; const kOpeningHeaderHeight = 32.0; class AnalysisTreeView extends ConsumerWidget { - const AnalysisTreeView( - this.pgn, - this.options, - ); + const AnalysisTreeView(this.options); - final String pgn; final AnalysisOptions options; @override Widget build(BuildContext context, WidgetRef ref) { - final ctrlProvider = analysisControllerProvider(pgn, options); + final ctrlProvider = analysisControllerProvider(options); final root = ref.watch(ctrlProvider.select((value) => value.root)); final currentPath = diff --git a/lib/src/view/board_editor/board_editor_screen.dart b/lib/src/view/board_editor/board_editor_screen.dart index 128e81cd06..7b4006a4ba 100644 --- a/lib/src/view/board_editor/board_editor_screen.dart +++ b/lib/src/view/board_editor/board_editor_screen.dart @@ -337,6 +337,7 @@ class _BottomBar extends ConsumerWidget { builder: (context) => AnalysisScreen( pgnOrId: editorState.pgn!, options: AnalysisOptions( + pgn: '', isLocalEvaluationAllowed: true, variant: Variant.fromPosition, orientation: editorState.orientation, diff --git a/lib/src/view/correspondence/offline_correspondence_game_screen.dart b/lib/src/view/correspondence/offline_correspondence_game_screen.dart index 0ce0cdae57..c816be5561 100644 --- a/lib/src/view/correspondence/offline_correspondence_game_screen.dart +++ b/lib/src/view/correspondence/offline_correspondence_game_screen.dart @@ -261,6 +261,7 @@ class _BodyState extends ConsumerState<_Body> { builder: (_) => AnalysisScreen( pgnOrId: game.makePgn(), options: AnalysisOptions( + pgn: '', isLocalEvaluationAllowed: false, variant: game.variant, initialMoveCursor: stepCursor, diff --git a/lib/src/view/game/archived_game_screen.dart b/lib/src/view/game/archived_game_screen.dart index ab7c41cec3..a8b0193202 100644 --- a/lib/src/view/game/archived_game_screen.dart +++ b/lib/src/view/game/archived_game_screen.dart @@ -386,6 +386,7 @@ class _BottomBar extends ConsumerWidget { builder: (context) => AnalysisScreen( pgnOrId: game.makePgn(), options: AnalysisOptions( + pgn: '', isLocalEvaluationAllowed: true, variant: gameData.variant, initialMoveCursor: cursor, diff --git a/lib/src/view/game/game_list_tile.dart b/lib/src/view/game/game_list_tile.dart index 5009b84947..c54d6c5106 100644 --- a/lib/src/view/game/game_list_tile.dart +++ b/lib/src/view/game/game_list_tile.dart @@ -228,6 +228,7 @@ class _ContextMenu extends ConsumerWidget { builder: (context) => AnalysisScreen( pgnOrId: game.id.value, options: AnalysisOptions( + pgn: '', isLocalEvaluationAllowed: true, variant: game.variant, orientation: orientation, diff --git a/lib/src/view/game/game_result_dialog.dart b/lib/src/view/game/game_result_dialog.dart index 2611a15696..20eeb61b7b 100644 --- a/lib/src/view/game/game_result_dialog.dart +++ b/lib/src/view/game/game_result_dialog.dart @@ -263,6 +263,7 @@ class OverTheBoardGameResultDialog extends StatelessWidget { builder: (_) => AnalysisScreen( pgnOrId: game.makePgn(), options: AnalysisOptions( + pgn: '', isLocalEvaluationAllowed: true, variant: game.meta.variant, orientation: Side.white, diff --git a/lib/src/view/opening_explorer/opening_explorer_screen.dart b/lib/src/view/opening_explorer/opening_explorer_screen.dart index 9ade504e4e..6823005fed 100644 --- a/lib/src/view/opening_explorer/opening_explorer_screen.dart +++ b/lib/src/view/opening_explorer/opening_explorer_screen.dart @@ -33,9 +33,8 @@ const _kTableRowPadding = EdgeInsets.symmetric( const _kTabletBoardRadius = BorderRadius.all(Radius.circular(4.0)); class OpeningExplorerScreen extends ConsumerStatefulWidget { - const OpeningExplorerScreen({required this.pgn, required this.options}); + const OpeningExplorerScreen({required this.options}); - final String pgn; final AnalysisOptions options; @override @@ -51,8 +50,7 @@ class _OpeningExplorerState extends ConsumerState { @override Widget build(BuildContext context) { - final analysisState = - ref.watch(analysisControllerProvider(widget.pgn, widget.options)); + final analysisState = ref.watch(analysisControllerProvider(widget.options)); final opening = analysisState.currentNode.isRoot ? LightOpening( @@ -102,14 +100,12 @@ class _OpeningExplorerState extends ConsumerState { if (analysisState.position.ply >= 50) { return _OpeningExplorerView( - pgn: widget.pgn, options: widget.options, isLoading: false, isIndexing: false, children: [ openingHeader, OpeningExplorerMoveTable.maxDepth( - pgn: widget.pgn, options: widget.options, ), ], @@ -120,7 +116,6 @@ class _OpeningExplorerState extends ConsumerState { if (prefs.db == OpeningDatabase.player && prefs.playerDb.username == null) { return _OpeningExplorerView( - pgn: widget.pgn, options: widget.options, isLoading: false, isIndexing: false, @@ -163,7 +158,6 @@ class _OpeningExplorerState extends ConsumerState { openingExplorerAsync.isLoading || openingExplorerAsync.value == null; return _OpeningExplorerView( - pgn: widget.pgn, options: widget.options, isLoading: isLoading, isIndexing: openingExplorerAsync.value?.isIndexing ?? false, @@ -176,7 +170,6 @@ class _OpeningExplorerState extends ConsumerState { child: ShimmerLoading( isLoading: true, child: OpeningExplorerMoveTable.loading( - pgn: widget.pgn, options: widget.options, ), ), @@ -196,7 +189,6 @@ class _OpeningExplorerState extends ConsumerState { whiteWins: openingExplorer.entry.white, draws: openingExplorer.entry.draws, blackWins: openingExplorer.entry.black, - pgn: widget.pgn, options: widget.options, ), if (topGames != null && topGames.isNotEmpty) ...[ @@ -252,7 +244,6 @@ class _OpeningExplorerState extends ConsumerState { child: ShimmerLoading( isLoading: true, child: OpeningExplorerMoveTable.loading( - pgn: widget.pgn, options: widget.options, ), ), @@ -278,14 +269,12 @@ class _OpeningExplorerState extends ConsumerState { class _OpeningExplorerView extends StatelessWidget { const _OpeningExplorerView({ - required this.pgn, required this.options, required this.children, required this.isLoading, required this.isIndexing, }); - final String pgn; final AnalysisOptions options; final List children; final bool isLoading; @@ -306,7 +295,7 @@ class _OpeningExplorerView extends StatelessWidget { horizontal: kTabletBoardTableSidePadding, ) : EdgeInsets.zero, - child: _MoveList(pgn: pgn, options: options), + child: _MoveList(options: options), ), Expanded( child: LayoutBuilder( @@ -338,7 +327,6 @@ class _OpeningExplorerView extends StatelessWidget { bottom: kTabletBoardTableSidePadding, ), child: AnalysisBoard( - pgn, options, boardSize, borderRadius: isTablet ? _kTabletBoardRadius : null, @@ -389,7 +377,6 @@ class _OpeningExplorerView extends StatelessWidget { // disable scrolling when dragging the board onVerticalDragStart: (_) {}, child: AnalysisBoard( - pgn, options, boardSize, ), @@ -404,7 +391,7 @@ class _OpeningExplorerView extends StatelessWidget { }, ), ), - _BottomBar(pgn: pgn, options: options), + _BottomBar(options: options), ], ), ); @@ -414,7 +401,7 @@ class _OpeningExplorerView extends StatelessWidget { body: body, appBar: AppBar( title: Text(context.l10n.openingExplorer), - bottom: _MoveList(pgn: pgn, options: options), + bottom: _MoveList(options: options), actions: [ if (isIndexing) const _IndexingIndicator(), ], @@ -480,12 +467,8 @@ class _IndexingIndicatorState extends State<_IndexingIndicator> } class _MoveList extends ConsumerWidget implements PreferredSizeWidget { - const _MoveList({ - required this.pgn, - required this.options, - }); + const _MoveList({required this.options}); - final String pgn; final AnalysisOptions options; @override @@ -493,7 +476,7 @@ class _MoveList extends ConsumerWidget implements PreferredSizeWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final ctrlProvider = analysisControllerProvider(pgn, options); + final ctrlProvider = analysisControllerProvider(options); final state = ref.watch(ctrlProvider); final slicedMoves = state.root.mainline .map((e) => e.sanMove.san) @@ -526,19 +509,15 @@ class _MoveList extends ConsumerWidget implements PreferredSizeWidget { } class _BottomBar extends ConsumerWidget { - const _BottomBar({ - required this.pgn, - required this.options, - }); + const _BottomBar({required this.options}); - final String pgn; final AnalysisOptions options; @override Widget build(BuildContext context, WidgetRef ref) { final db = ref .watch(openingExplorerPreferencesProvider.select((value) => value.db)); - final ctrlProvider = analysisControllerProvider(pgn, options); + final ctrlProvider = analysisControllerProvider(options); final canGoBack = ref.watch(ctrlProvider.select((value) => value.canGoBack)); final canGoNext = @@ -560,7 +539,7 @@ class _BottomBar extends ConsumerWidget { isScrollControlled: true, showDragHandle: true, isDismissible: true, - builder: (_) => OpeningExplorerSettings(pgn, options), + builder: (_) => OpeningExplorerSettings(options), ), icon: Icons.tune, ), @@ -596,8 +575,7 @@ class _BottomBar extends ConsumerWidget { } void _moveForward(WidgetRef ref) => - ref.read(analysisControllerProvider(pgn, options).notifier).userNext(); - void _moveBackward(WidgetRef ref) => ref - .read(analysisControllerProvider(pgn, options).notifier) - .userPrevious(); + ref.read(analysisControllerProvider(options).notifier).userNext(); + void _moveBackward(WidgetRef ref) => + ref.read(analysisControllerProvider(options).notifier).userPrevious(); } diff --git a/lib/src/view/opening_explorer/opening_explorer_settings.dart b/lib/src/view/opening_explorer/opening_explorer_settings.dart index 51c3c38d8f..a880d8fb33 100644 --- a/lib/src/view/opening_explorer/opening_explorer_settings.dart +++ b/lib/src/view/opening_explorer/opening_explorer_settings.dart @@ -14,9 +14,8 @@ import 'package:lichess_mobile/src/widgets/adaptive_bottom_sheet.dart'; import 'package:lichess_mobile/src/widgets/list.dart'; class OpeningExplorerSettings extends ConsumerWidget { - const OpeningExplorerSettings(this.pgn, this.options); + const OpeningExplorerSettings(this.options); - final String pgn; final AnalysisOptions options; @override diff --git a/lib/src/view/opening_explorer/opening_explorer_widgets.dart b/lib/src/view/opening_explorer/opening_explorer_widgets.dart index 2b19204c49..43bf9f9deb 100644 --- a/lib/src/view/opening_explorer/opening_explorer_widgets.dart +++ b/lib/src/view/opening_explorer/opening_explorer_widgets.dart @@ -34,13 +34,11 @@ class OpeningExplorerMoveTable extends ConsumerWidget { required this.whiteWins, required this.draws, required this.blackWins, - required this.pgn, required this.options, }) : _isLoading = false, _maxDepthReached = false; const OpeningExplorerMoveTable.loading({ - required this.pgn, required this.options, }) : _isLoading = true, moves = const IListConst([]), @@ -50,7 +48,6 @@ class OpeningExplorerMoveTable extends ConsumerWidget { _maxDepthReached = false; const OpeningExplorerMoveTable.maxDepth({ - required this.pgn, required this.options, }) : _isLoading = false, moves = const IListConst([]), @@ -63,7 +60,6 @@ class OpeningExplorerMoveTable extends ConsumerWidget { final int whiteWins; final int draws; final int blackWins; - final String pgn; final AnalysisOptions options; final bool _isLoading; @@ -84,7 +80,7 @@ class OpeningExplorerMoveTable extends ConsumerWidget { } final games = whiteWins + draws + blackWins; - final ctrlProvider = analysisControllerProvider(pgn, options); + final ctrlProvider = analysisControllerProvider(options); const headerTextStyle = TextStyle(fontSize: 12); diff --git a/lib/src/view/puzzle/puzzle_screen.dart b/lib/src/view/puzzle/puzzle_screen.dart index ef8681c596..b79254f87c 100644 --- a/lib/src/view/puzzle/puzzle_screen.dart +++ b/lib/src/view/puzzle/puzzle_screen.dart @@ -486,6 +486,7 @@ class _BottomBar extends ConsumerWidget { builder: (context) => AnalysisScreen( pgnOrId: ref.read(ctrlProvider.notifier).makePgn(), options: AnalysisOptions( + pgn: '', isLocalEvaluationAllowed: true, variant: Variant.standard, orientation: puzzleState.pov, diff --git a/lib/src/view/puzzle/streak_screen.dart b/lib/src/view/puzzle/streak_screen.dart index b682b49ebc..6be35275dc 100644 --- a/lib/src/view/puzzle/streak_screen.dart +++ b/lib/src/view/puzzle/streak_screen.dart @@ -293,6 +293,7 @@ class _BottomBar extends ConsumerWidget { builder: (context) => AnalysisScreen( pgnOrId: ref.read(ctrlProvider.notifier).makePgn(), options: AnalysisOptions( + pgn: '', isLocalEvaluationAllowed: true, variant: Variant.standard, orientation: puzzleState.pov, diff --git a/lib/src/view/study/study_bottom_bar.dart b/lib/src/view/study/study_bottom_bar.dart index ade01ed711..d94d6e4d57 100644 --- a/lib/src/view/study/study_bottom_bar.dart +++ b/lib/src/view/study/study_bottom_bar.dart @@ -167,6 +167,7 @@ class _GamebookBottomBar extends ConsumerWidget { builder: (context) => AnalysisScreen( pgnOrId: state.pgn, options: AnalysisOptions( + pgn: state.pgn, isLocalEvaluationAllowed: true, variant: state.variant, orientation: state.pov, diff --git a/lib/src/view/tools/load_position_screen.dart b/lib/src/view/tools/load_position_screen.dart index ac3b4d97b7..27040a0d70 100644 --- a/lib/src/view/tools/load_position_screen.dart +++ b/lib/src/view/tools/load_position_screen.dart @@ -132,7 +132,8 @@ class _BodyState extends State<_Body> { return ( pgn: '[FEN "${pos.fen}"]', fen: pos.fen, - options: const AnalysisOptions( + options: AnalysisOptions( + pgn: '[FEN "${pos.fen}"]', isLocalEvaluationAllowed: true, variant: Variant.standard, orientation: Side.white, @@ -164,6 +165,7 @@ class _BodyState extends State<_Body> { pgn: textInput!, fen: lastPosition.fen, options: AnalysisOptions( + pgn: textInput!, isLocalEvaluationAllowed: true, variant: rule != null ? Variant.fromRule(rule) : Variant.standard, initialMoveCursor: mainlineMoves.isEmpty ? 0 : 1, diff --git a/lib/src/view/tools/tools_tab_screen.dart b/lib/src/view/tools/tools_tab_screen.dart index 8270f84bcc..b2bba9d51c 100644 --- a/lib/src/view/tools/tools_tab_screen.dart +++ b/lib/src/view/tools/tools_tab_screen.dart @@ -150,6 +150,7 @@ class _Body extends ConsumerWidget { builder: (context) => const AnalysisScreen( pgnOrId: '', options: AnalysisOptions( + pgn: '', isLocalEvaluationAllowed: true, variant: Variant.standard, orientation: Side.white, @@ -166,8 +167,8 @@ class _Body extends ConsumerWidget { context, rootNavigator: true, builder: (context) => const OpeningExplorerScreen( - pgn: '', options: AnalysisOptions( + pgn: '', isLocalEvaluationAllowed: false, variant: Variant.standard, orientation: Side.white, diff --git a/test/view/analysis/analysis_screen_test.dart b/test/view/analysis/analysis_screen_test.dart index 156380c906..337fdc7785 100644 --- a/test/view/analysis/analysis_screen_test.dart +++ b/test/view/analysis/analysis_screen_test.dart @@ -36,6 +36,7 @@ void main() { home: AnalysisScreen( pgnOrId: sanMoves, options: AnalysisOptions( + pgn: '', isLocalEvaluationAllowed: false, variant: Variant.standard, opening: opening, @@ -70,6 +71,7 @@ void main() { home: AnalysisScreen( pgnOrId: sanMoves, options: AnalysisOptions( + pgn: '', isLocalEvaluationAllowed: false, variant: Variant.standard, opening: opening, @@ -135,6 +137,7 @@ void main() { home: AnalysisScreen( pgnOrId: pgn, options: const AnalysisOptions( + pgn: '', isLocalEvaluationAllowed: false, variant: Variant.standard, orientation: Side.white, diff --git a/test/view/opening_explorer/opening_explorer_screen_test.dart b/test/view/opening_explorer/opening_explorer_screen_test.dart index 32b7e88a4f..5811d32f6f 100644 --- a/test/view/opening_explorer/opening_explorer_screen_test.dart +++ b/test/view/opening_explorer/opening_explorer_screen_test.dart @@ -40,6 +40,7 @@ void main() { }); const options = AnalysisOptions( + pgn: '', id: standaloneOpeningExplorerId, isLocalEvaluationAllowed: false, orientation: Side.white, @@ -65,7 +66,6 @@ void main() { final app = await makeTestProviderScopeApp( tester, home: const OpeningExplorerScreen( - pgn: '', options: options, ), overrides: [ @@ -112,7 +112,6 @@ void main() { final app = await makeTestProviderScopeApp( tester, home: const OpeningExplorerScreen( - pgn: '', options: options, ), overrides: [ @@ -165,7 +164,6 @@ void main() { final app = await makeTestProviderScopeApp( tester, home: const OpeningExplorerScreen( - pgn: '', options: options, ), overrides: [ From 3ab16e6f0fea1ea5b179895537fd6747c173c3d8 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Thu, 21 Nov 2024 11:31:38 +0100 Subject: [PATCH 07/23] Refactor analysis controller to be async --- .../model/analysis/analysis_controller.dart | 347 ++++++++------- .../analysis/server_analysis_service.dart | 6 +- lib/src/model/game/game_controller.dart | 25 +- lib/src/view/analysis/analysis_board.dart | 9 +- lib/src/view/analysis/analysis_screen.dart | 193 +++----- lib/src/view/analysis/analysis_settings.dart | 207 ++++----- .../view/analysis/analysis_share_screen.dart | 5 +- .../view/analysis/opening_explorer_view.dart | 3 +- lib/src/view/analysis/server_analysis.dart | 47 +- lib/src/view/analysis/tree_view.dart | 29 +- .../board_editor/board_editor_screen.dart | 12 +- .../offline_correspondence_game_screen.dart | 13 +- lib/src/view/game/archived_game_screen.dart | 19 +- lib/src/view/game/game_body.dart | 4 +- lib/src/view/game/game_list_tile.dart | 9 +- lib/src/view/game/game_result_dialog.dart | 13 +- .../opening_explorer_screen.dart | 417 ++++++++++-------- lib/src/view/puzzle/puzzle_screen.dart | 12 +- lib/src/view/puzzle/streak_screen.dart | 12 +- lib/src/view/study/study_bottom_bar.dart | 12 +- lib/src/view/tools/load_position_screen.dart | 27 +- lib/src/view/tools/tools_tab_screen.dart | 23 +- lib/src/widgets/feedback.dart | 1 - test/view/analysis/analysis_screen_test.dart | 99 ++--- .../opening_explorer_screen_test.dart | 29 +- 25 files changed, 758 insertions(+), 815 deletions(-) diff --git a/lib/src/model/analysis/analysis_controller.dart b/lib/src/model/analysis/analysis_controller.dart index 6be0809cce..645a922ef9 100644 --- a/lib/src/model/analysis/analysis_controller.dart +++ b/lib/src/model/analysis/analysis_controller.dart @@ -17,6 +17,7 @@ import 'package:lichess_mobile/src/model/common/service/sound_service.dart'; import 'package:lichess_mobile/src/model/common/uci.dart'; import 'package:lichess_mobile/src/model/engine/evaluation_service.dart'; import 'package:lichess_mobile/src/model/engine/work.dart'; +import 'package:lichess_mobile/src/model/game/game_repository_providers.dart'; import 'package:lichess_mobile/src/model/game/player.dart'; import 'package:lichess_mobile/src/utils/rate_limit.dart'; import 'package:lichess_mobile/src/view/engine/engine_gauge.dart'; @@ -26,80 +27,73 @@ import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'analysis_controller.freezed.dart'; part 'analysis_controller.g.dart'; -const standaloneAnalysisId = StringId('standalone_analysis'); -const standaloneOpeningExplorerId = StringId('standalone_opening_explorer'); - final _dateFormat = DateFormat('yyyy.MM.dd'); -/// Whether the analysis is a standalone analysis (not a lichess game analysis). -bool _isStandaloneAnalysis(StringId id) => - id == standaloneAnalysisId || id == standaloneOpeningExplorerId; +typedef StandaloneAnalysis = ({ + String pgn, + Variant variant, + Side orientation, + bool isLocalEvaluationAllowed, +}); @freezed class AnalysisOptions with _$AnalysisOptions { const AnalysisOptions._(); - const factory AnalysisOptions({ - required String pgn, - /// The ID of the analysis. Can be a game ID or a standalone ID. - required StringId id, - required bool isLocalEvaluationAllowed, - required Side orientation, - required Variant variant, + @Assert('standalone != null || gameId != null') + const factory AnalysisOptions({ + StandaloneAnalysis? standalone, + GameId? gameId, int? initialMoveCursor, - LightOpening? opening, - Division? division, - - /// Optional server analysis to display player stats. - ({PlayerAnalysis white, PlayerAnalysis black})? serverAnalysis, }) = _AnalysisOptions; - bool get canShowGameSummary => - serverAnalysis != null || id != standaloneAnalysisId; - - /// Whether the analysis is for a lichess game. - bool get isLichessGameAnalysis => gameAnyId != null; - - /// The game ID of the analysis, if it's a lichess game. - GameAnyId? get gameAnyId => - _isStandaloneAnalysis(id) ? null : GameAnyId(id.value); + bool get isLichessGameAnalysis => gameId != null; } @riverpod class AnalysisController extends _$AnalysisController implements PgnTreeNotifier { - late Root _root; + late final Root _root; + late final Variant _variant; + late final Side _orientation; final _engineEvalDebounce = Debouncer(const Duration(milliseconds: 150)); Timer? _startEngineEvalTimer; @override - AnalysisState build(AnalysisOptions options) { + Future build(AnalysisOptions options) async { final evaluationService = ref.watch(evaluationServiceProvider); final serverAnalysisService = ref.watch(serverAnalysisServiceProvider); - final isEngineAllowed = options.isLocalEvaluationAllowed && - engineSupportedVariants.contains(options.variant); - - ref.onDispose(() { - _startEngineEvalTimer?.cancel(); - _engineEvalDebounce.dispose(); - if (isEngineAllowed) { - evaluationService.disposeEngine(); - } - serverAnalysisService.lastAnalysisEvent - .removeListener(_listenToServerAnalysisEvents); - }); - - serverAnalysisService.lastAnalysisEvent - .addListener(_listenToServerAnalysisEvents); + late final String pgn; + late final LightOpening? opening; + late final ({PlayerAnalysis white, PlayerAnalysis black})? serverAnalysis; + late final Division? division; + + if (options.gameId != null) { + final game = + await ref.watch(archivedGameProvider(id: options.gameId!).future); + _variant = game.meta.variant; + _orientation = game.youAre ?? Side.white; + pgn = game.makePgn(); + opening = game.data.opening; + serverAnalysis = game.serverAnalysis; + division = game.meta.division; + } else { + _variant = options.standalone!.variant; + _orientation = options.standalone!.orientation; + pgn = options.standalone!.pgn; + opening = null; + serverAnalysis = null; + division = null; + } UciPath path = UciPath.empty; Move? lastMove; final game = PgnGame.parsePgn( - options.pgn, + pgn, initHeaders: () => options.isLichessGameAnalysis ? {} : { @@ -118,7 +112,9 @@ class AnalysisController extends _$AnalysisController final pgnHeaders = IMap(game.headers); final rootComments = IList(game.comments.map((c) => PgnComment.fromPgn(c))); - Future? openingFuture; + final isLocalEvaluationAllowed = options.isLichessGameAnalysis + ? pgnHeaders['Result'] != '*' + : options.standalone!.isLocalEvaluationAllowed; _root = Root.fromPgnGame( game, @@ -132,17 +128,9 @@ class AnalysisController extends _$AnalysisController path = path + branch.id; lastMove = branch.sanMove.move; } - if (isMainline && options.opening == null && branch.position.ply <= 5) { - openingFuture = _fetchOpening(root, path); - } }, ); - // wait for the opening to be fetched to recompute the branch opening - openingFuture?.then((_) { - _setPath(state.currentPath); - }); - final currentPath = options.initialMoveCursor == null ? _root.mainlinePath : path; final currentNode = _root.nodeAt(currentPath); @@ -151,9 +139,24 @@ class AnalysisController extends _$AnalysisController // analysis preferences change final prefs = ref.read(analysisPreferencesProvider); + final isEngineAllowed = engineSupportedVariants.contains(_variant); + + ref.onDispose(() { + _startEngineEvalTimer?.cancel(); + _engineEvalDebounce.dispose(); + if (isEngineAllowed) { + evaluationService.disposeEngine(); + } + serverAnalysisService.lastAnalysisEvent + .removeListener(_listenToServerAnalysisEvents); + }); + + serverAnalysisService.lastAnalysisEvent + .addListener(_listenToServerAnalysisEvents); + final analysisState = AnalysisState( - variant: options.variant, - id: options.id, + variant: _variant, + gameId: options.gameId, currentPath: currentPath, isOnMainline: _root.isOnMainline(currentPath), root: _root.view, @@ -161,13 +164,13 @@ class AnalysisController extends _$AnalysisController pgnHeaders: pgnHeaders, pgnRootComments: rootComments, lastMove: lastMove, - pov: options.orientation, - contextOpening: options.opening, - isLocalEvaluationAllowed: options.isLocalEvaluationAllowed, + pov: _orientation, + contextOpening: opening, + isLocalEvaluationAllowed: isLocalEvaluationAllowed, isLocalEvaluationEnabled: prefs.enableLocalEvaluation, - playersAnalysis: options.serverAnalysis, - acplChartData: - options.serverAnalysis != null ? _makeAcplChartData() : null, + playersAnalysis: serverAnalysis, + acplChartData: serverAnalysis != null ? _makeAcplChartData() : null, + division: division, ); if (analysisState.isEngineAvailable) { @@ -190,23 +193,25 @@ class AnalysisController extends _$AnalysisController } EvaluationContext get _evaluationContext => EvaluationContext( - variant: options.variant, + variant: _variant, initialPosition: _root.position, ); - void onUserMove(NormalMove move) { - if (!state.position.isLegal(move)) return; + void onUserMove(NormalMove move, {bool shouldReplace = false}) { + if (!state.requireValue.position.isLegal(move)) return; - if (isPromotionPawnMove(state.position, move)) { - state = state.copyWith(promotionMove: move); + if (isPromotionPawnMove(state.requireValue.position, move)) { + state = AsyncValue.data( + state.requireValue.copyWith(promotionMove: move), + ); return; } - // For the opening explorer, last played move should always be the mainline - final shouldReplace = options.id == standaloneOpeningExplorerId; - - final (newPath, isNewNode) = - _root.addMoveAt(state.currentPath, move, replace: shouldReplace); + final (newPath, isNewNode) = _root.addMoveAt( + state.requireValue.currentPath, + move, + replace: shouldReplace, + ); if (newPath != null) { _setPath( newPath, @@ -218,10 +223,10 @@ class AnalysisController extends _$AnalysisController void onPromotionSelection(Role? role) { if (role == null) { - state = state.copyWith(promotionMove: null); + state = AsyncData(state.requireValue.copyWith(promotionMove: null)); return; } - final promotionMove = state.promotionMove; + final promotionMove = state.requireValue.promotionMove; if (promotionMove != null) { final promotion = promotionMove.withPromotion(role); onUserMove(promotion); @@ -229,9 +234,11 @@ class AnalysisController extends _$AnalysisController } void userNext() { - if (!state.currentNode.hasChild) return; + final curState = state.requireValue; + if (!curState.currentNode.hasChild) return; _setPath( - state.currentPath + _root.nodeAt(state.currentPath).children.first.id, + curState.currentPath + + _root.nodeAt(curState.currentPath).children.first.id, replaying: true, ); } @@ -260,11 +267,12 @@ class AnalysisController extends _$AnalysisController } void toggleBoard() { - state = state.copyWith(pov: state.pov.opposite); + final curState = state.requireValue; + state = AsyncData(curState.copyWith(pov: curState.pov.opposite)); } void userPrevious() { - _setPath(state.currentPath.penultimate, replaying: true); + _setPath(state.requireValue.currentPath.penultimate, replaying: true); } @override @@ -285,7 +293,7 @@ class AnalysisController extends _$AnalysisController grandChild.isHidden = false; } } - state = state.copyWith(root: _root.view); + state = AsyncData(state.requireValue.copyWith(root: _root.view)); } @override @@ -296,15 +304,18 @@ class AnalysisController extends _$AnalysisController child.isHidden = true; } - state = state.copyWith(root: _root.view); + state = AsyncData(state.requireValue.copyWith(root: _root.view)); } @override void promoteVariation(UciPath path, bool toMainline) { _root.promoteAt(path, toMainline: toMainline); - state = state.copyWith( - isOnMainline: _root.isOnMainline(state.currentPath), - root: _root.view, + final curState = state.requireValue; + state = AsyncData( + curState.copyWith( + isOnMainline: _root.isOnMainline(curState.currentPath), + root: _root.view, + ), ); } @@ -319,11 +330,14 @@ class AnalysisController extends _$AnalysisController .read(analysisPreferencesProvider.notifier) .toggleEnableLocalEvaluation(); - state = state.copyWith( - isLocalEvaluationEnabled: !state.isLocalEvaluationEnabled, + final curState = state.requireValue; + state = AsyncData( + curState.copyWith( + isLocalEvaluationEnabled: !curState.isLocalEvaluationEnabled, + ), ); - if (state.isEngineAvailable) { + if (state.requireValue.isEngineAvailable) { final prefs = ref.read(analysisPreferencesProvider); await ref.read(evaluationServiceProvider).initEngine( _evaluationContext, @@ -353,9 +367,12 @@ class AnalysisController extends _$AnalysisController _root.updateAll((node) => node.eval = null); - state = state.copyWith( - currentNode: - AnalysisCurrentNode.fromNode(_root.nodeAt(state.currentPath)), + final curState = state.requireValue; + state = AsyncData( + curState.copyWith( + currentNode: + AnalysisCurrentNode.fromNode(_root.nodeAt(curState.currentPath)), + ), ); _startEngineEval(); @@ -377,17 +394,14 @@ class AnalysisController extends _$AnalysisController } void updatePgnHeader(String key, String value) { - final headers = state.pgnHeaders.add(key, value); - state = state.copyWith(pgnHeaders: headers); + final headers = state.requireValue.pgnHeaders.add(key, value); + state = AsyncData(state.requireValue.copyWith(pgnHeaders: headers)); } Future requestServerAnalysis() { - if (state.canRequestServerAnalysis) { + if (state.requireValue.canRequestServerAnalysis) { final service = ref.read(serverAnalysisServiceProvider); - return service.requestAnalysis( - options.id as GameAnyId, - options.orientation, - ); + return service.requestAnalysis(options.gameId!, _orientation); } return Future.error('Cannot request server analysis'); } @@ -405,12 +419,13 @@ class AnalysisController extends _$AnalysisController /// Makes a full PGN string (including headers and comments) of the current game state. String makeExportPgn() { - return _root.makePgn(state.pgnHeaders, state.pgnRootComments); + final curState = state.requireValue; + return _root.makePgn(curState.pgnHeaders, curState.pgnRootComments); } /// Makes a PGN string up to the current node only. String makeCurrentNodePgn() { - final nodes = _root.branchesOn(state.currentPath); + final nodes = _root.branchesOn(state.requireValue.currentPath); return nodes.map((node) => node.sanMove.san).join(' '); } @@ -420,7 +435,8 @@ class AnalysisController extends _$AnalysisController bool shouldRecomputeRootView = false, bool replaying = false, }) { - final pathChange = state.currentPath != path; + final curState = state.requireValue; + final pathChange = curState.currentPath != path; final (currentNode, opening) = _nodeOpeningAt(_root, path); // always show variation if the user plays a move @@ -437,9 +453,9 @@ class AnalysisController extends _$AnalysisController // or a variation is hidden/shown final rootView = shouldForceShowVariation || shouldRecomputeRootView ? _root.view - : state.root; + : curState.root; - final isForward = path.size > state.currentPath.size; + final isForward = path.size > curState.currentPath.size; if (currentNode is Branch) { if (!replaying) { if (isForward) { @@ -462,65 +478,72 @@ class AnalysisController extends _$AnalysisController } if (currentNode.opening == null && currentNode.position.ply <= 30) { - _fetchOpening(_root, path); + _fetchOpening(_root, path).then((opening) { + if (opening != null) { + _root.updateAt(path, (node) => node.opening = opening); + + final curState = state.requireValue; + if (curState.currentPath == path) { + state = AsyncData( + curState.copyWith( + currentNode: AnalysisCurrentNode.fromNode(_root.nodeAt(path)), + ), + ); + } + } + }); } - state = state.copyWith( - currentPath: path, - isOnMainline: _root.isOnMainline(path), - currentNode: AnalysisCurrentNode.fromNode(currentNode), - currentBranchOpening: opening, - lastMove: currentNode.sanMove.move, - promotionMove: null, - root: rootView, + state = AsyncData( + curState.copyWith( + currentPath: path, + isOnMainline: _root.isOnMainline(path), + currentNode: AnalysisCurrentNode.fromNode(currentNode), + currentBranchOpening: opening, + lastMove: currentNode.sanMove.move, + promotionMove: null, + root: rootView, + ), ); } else { - state = state.copyWith( - currentPath: path, - isOnMainline: _root.isOnMainline(path), - currentNode: AnalysisCurrentNode.fromNode(currentNode), - currentBranchOpening: opening, - lastMove: null, - promotionMove: null, - root: rootView, + state = AsyncData( + curState.copyWith( + currentPath: path, + isOnMainline: _root.isOnMainline(path), + currentNode: AnalysisCurrentNode.fromNode(currentNode), + currentBranchOpening: opening, + lastMove: null, + promotionMove: null, + root: rootView, + ), ); } - if (pathChange && state.isEngineAvailable) { + if (pathChange && curState.isEngineAvailable) { _debouncedStartEngineEval(); } } - Future _fetchOpening(Node fromNode, UciPath path) async { - if (!kOpeningAllowedVariants.contains(options.variant)) return; + Future _fetchOpening(Node fromNode, UciPath path) async { + if (!kOpeningAllowedVariants.contains(_variant)) return null; final moves = fromNode.branchesOn(path).map((node) => node.sanMove.move); - if (moves.isEmpty) return; - if (moves.length > 40) return; - - final opening = - await ref.read(openingServiceProvider).fetchFromMoves(moves); + if (moves.isEmpty) return null; + if (moves.length > 40) return null; - if (opening != null) { - fromNode.updateAt(path, (node) => node.opening = opening); - - if (state.currentPath == path) { - state = state.copyWith( - currentNode: AnalysisCurrentNode.fromNode(fromNode.nodeAt(path)), - ); - } - } + return ref.read(openingServiceProvider).fetchFromMoves(moves); } void _startEngineEval() { - if (!state.isEngineAvailable) return; + final curState = state.requireValue; + if (!curState.isEngineAvailable) return; ref .read(evaluationServiceProvider) .start( - state.currentPath, - _root.branchesOn(state.currentPath).map(Step.fromNode), + curState.currentPath, + _root.branchesOn(curState.currentPath).map(Step.fromNode), initialPositionEval: _root.eval, - shouldEmit: (work) => work.path == state.currentPath, + shouldEmit: (work) => work.path == curState.currentPath, ) ?.forEach( (t) => _root.updateAt(t.$1.path, (node) => node.eval = t.$2), @@ -536,23 +559,31 @@ class AnalysisController extends _$AnalysisController void _stopEngineEval() { ref.read(evaluationServiceProvider).stop(); // update the current node with last cached eval - state = state.copyWith( - currentNode: - AnalysisCurrentNode.fromNode(_root.nodeAt(state.currentPath)), + final curState = state.requireValue; + state = AsyncData( + curState.copyWith( + currentNode: + AnalysisCurrentNode.fromNode(_root.nodeAt(curState.currentPath)), + ), ); } void _listenToServerAnalysisEvents() { final event = ref.read(serverAnalysisServiceProvider).lastAnalysisEvent.value; - if (event != null && event.$1 == state.id) { + if (event != null && event.$1 == state.requireValue.gameId) { _mergeOngoingAnalysis(_root, event.$2.tree); - state = state.copyWith( - acplChartData: _makeAcplChartData(), - playersAnalysis: event.$2.analysis != null - ? (white: event.$2.analysis!.white, black: event.$2.analysis!.black) - : null, - root: _root.view, + state = AsyncData( + state.requireValue.copyWith( + acplChartData: _makeAcplChartData(), + playersAnalysis: event.$2.analysis != null + ? ( + white: event.$2.analysis!.white, + black: event.$2.analysis!.black + ) + : null, + root: _root.view, + ), ); } } @@ -651,8 +682,8 @@ class AnalysisState with _$AnalysisState { const AnalysisState._(); const factory AnalysisState({ - /// Analysis ID - required StringId id, + /// The ID of the game if it's a lichess game. + required GameId? gameId, /// The variant of the analysis. required Variant variant, @@ -697,6 +728,9 @@ class AnalysisState with _$AnalysisState { /// Optional server analysis to display player stats. ({PlayerAnalysis white, PlayerAnalysis black})? playersAnalysis, + /// Optional game division data, given by server analysis. + Division? division, + /// Optional ACPL chart data of the game, coming from lichess server analysis. IList? acplChartData, @@ -709,12 +743,8 @@ class AnalysisState with _$AnalysisState { IList? pgnRootComments, }) = _AnalysisState; - /// The game ID of the analysis, if it's a lichess game. - GameAnyId? get gameAnyId => - _isStandaloneAnalysis(id) ? null : GameAnyId(id.value); - /// Whether the analysis is for a lichess game. - bool get isLichessGameAnalysis => gameAnyId != null; + bool get isLichessGameAnalysis => gameId != null; IMap> get validMoves => makeLegalMoves( currentNode.position, @@ -725,10 +755,7 @@ class AnalysisState with _$AnalysisState { /// /// It must be a lichess game, which is finished and not already analyzed. bool get canRequestServerAnalysis => - gameAnyId != null && - (id.length == 8 || id.length == 12) && - !hasServerAnalysis && - pgnHeaders['Result'] != '*'; + gameId != null && !hasServerAnalysis && pgnHeaders['Result'] != '*'; bool get hasServerAnalysis => playersAnalysis != null; diff --git a/lib/src/model/analysis/server_analysis_service.dart b/lib/src/model/analysis/server_analysis_service.dart index 94665dfdac..ed73593547 100644 --- a/lib/src/model/analysis/server_analysis_service.dart +++ b/lib/src/model/analysis/server_analysis_service.dart @@ -40,11 +40,9 @@ class ServerAnalysisService { /// /// This will return a future that completes when the server analysis is /// launched (but not when it is finished). - Future requestAnalysis(GameAnyId id, [Side? side]) async { + Future requestAnalysis(GameId id, [Side? side]) async { final socketPool = ref.read(socketPoolProvider); - final uri = id.isFullId - ? Uri(path: '/play/$id/v6') - : Uri(path: '/watch/$id/${side?.name ?? Side.white}/v6'); + final uri = Uri(path: '/watch/$id/${side?.name ?? Side.white}/v6'); final socketClient = socketPool.open(uri); _socketSubscription?.$2.cancel(); diff --git a/lib/src/model/game/game_controller.dart b/lib/src/model/game/game_controller.dart index 60a7efd0b7..e663d77b04 100644 --- a/lib/src/model/game/game_controller.dart +++ b/lib/src/model/game/game_controller.dart @@ -12,7 +12,6 @@ import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:lichess_mobile/src/model/account/account_preferences.dart'; import 'package:lichess_mobile/src/model/account/account_repository.dart'; import 'package:lichess_mobile/src/model/analysis/analysis_controller.dart'; -import 'package:lichess_mobile/src/model/analysis/server_analysis_service.dart'; import 'package:lichess_mobile/src/model/clock/chess_clock.dart'; import 'package:lichess_mobile/src/model/common/chess.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; @@ -424,21 +423,6 @@ class GameController extends _$GameController { _socketClient.send('rematch-no', null); } - Future requestServerAnalysis() { - return state.mapOrNull( - data: (d) { - if (!d.value.game.finished) { - return Future.error( - 'Cannot request server analysis on a non finished game', - ); - } - final service = ref.read(serverAnalysisServiceProvider); - return service.requestAnalysis(gameFullId); - }, - ) ?? - Future.value(); - } - /// Gets the live game clock if available. LiveGameClock? get _liveClock => _clock != null ? ( @@ -1183,14 +1167,7 @@ class GameState with _$GameState { String get analysisPgn => game.makePgn(); AnalysisOptions get analysisOptions => AnalysisOptions( - pgn: analysisPgn, - isLocalEvaluationAllowed: true, - variant: game.meta.variant, initialMoveCursor: stepCursor, - orientation: game.youAre ?? Side.white, - id: gameFullId, - opening: game.meta.opening, - serverAnalysis: game.serverAnalysis, - division: game.meta.division, + gameId: gameFullId.gameId, ); } diff --git a/lib/src/view/analysis/analysis_board.dart b/lib/src/view/analysis/analysis_board.dart index 6e60630a7e..c6e70e8557 100644 --- a/lib/src/view/analysis/analysis_board.dart +++ b/lib/src/view/analysis/analysis_board.dart @@ -19,6 +19,7 @@ class AnalysisBoard extends ConsumerStatefulWidget { this.boardSize, { this.borderRadius, this.enableDrawingShapes = true, + this.shouldReplaceChildOnUserMove = false, }); final AnalysisOptions options; @@ -26,6 +27,7 @@ class AnalysisBoard extends ConsumerStatefulWidget { final BorderRadiusGeometry? borderRadius; final bool enableDrawingShapes; + final bool shouldReplaceChildOnUserMove; @override ConsumerState createState() => AnalysisBoardState(); @@ -37,7 +39,7 @@ class AnalysisBoardState extends ConsumerState { @override Widget build(BuildContext context) { final ctrlProvider = analysisControllerProvider(widget.options); - final analysisState = ref.watch(ctrlProvider); + final analysisState = ref.watch(ctrlProvider).requireValue; final boardPrefs = ref.watch(boardPreferencesProvider); final showBestMoveArrow = ref.watch( analysisPreferencesProvider.select( @@ -85,7 +87,10 @@ class AnalysisBoardState extends ConsumerState { validMoves: analysisState.validMoves, promotionMove: analysisState.promotionMove, onMove: (move, {isDrop, captured}) => - ref.read(ctrlProvider.notifier).onUserMove(move), + ref.read(ctrlProvider.notifier).onUserMove( + move, + shouldReplace: widget.shouldReplaceChildOnUserMove, + ), onPromotionSelection: (role) => ref.read(ctrlProvider.notifier).onPromotionSelection(role), ), diff --git a/lib/src/view/analysis/analysis_screen.dart b/lib/src/view/analysis/analysis_screen.dart index 2276f60d94..cbe72e7d3a 100644 --- a/lib/src/view/analysis/analysis_screen.dart +++ b/lib/src/view/analysis/analysis_screen.dart @@ -4,8 +4,6 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/model/analysis/analysis_controller.dart'; import 'package:lichess_mobile/src/model/analysis/analysis_preferences.dart'; import 'package:lichess_mobile/src/model/common/chess.dart'; -import 'package:lichess_mobile/src/model/common/id.dart'; -import 'package:lichess_mobile/src/model/game/game_repository_providers.dart'; import 'package:lichess_mobile/src/model/game/game_share_service.dart'; import 'package:lichess_mobile/src/network/http.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; @@ -25,103 +23,30 @@ import 'package:lichess_mobile/src/widgets/bottom_bar_button.dart'; import 'package:lichess_mobile/src/widgets/buttons.dart'; import 'package:lichess_mobile/src/widgets/feedback.dart'; import 'package:lichess_mobile/src/widgets/platform_scaffold.dart'; +import 'package:logging/logging.dart'; import '../../utils/share.dart'; import 'analysis_board.dart'; import 'analysis_settings.dart'; import 'tree_view.dart'; -class AnalysisScreen extends StatelessWidget { +final _logger = Logger('AnalysisScreen'); + +class AnalysisScreen extends ConsumerStatefulWidget { const AnalysisScreen({ required this.options, - required this.pgnOrId, this.enableDrawingShapes = true, }); - /// The analysis options. final AnalysisOptions options; - /// The PGN or game ID to load. - final String pgnOrId; - final bool enableDrawingShapes; @override - Widget build(BuildContext context) { - return pgnOrId.length == 8 && GameId(pgnOrId).isValid - ? _LoadGame( - GameId(pgnOrId), - options, - enableDrawingShapes: enableDrawingShapes, - ) - : _LoadedAnalysisScreen( - options: options.copyWith( - pgn: pgnOrId, - ), - enableDrawingShapes: enableDrawingShapes, - ); - } + ConsumerState createState() => _AnalysisScreenState(); } -class _LoadGame extends ConsumerWidget { - const _LoadGame( - this.gameId, - this.options, { - required this.enableDrawingShapes, - }); - - final AnalysisOptions options; - final GameId gameId; - - final bool enableDrawingShapes; - - @override - Widget build(BuildContext context, WidgetRef ref) { - final gameAsync = ref.watch(archivedGameProvider(id: gameId)); - - return gameAsync.when( - data: (game) { - final serverAnalysis = - game.white.analysis != null && game.black.analysis != null - ? (white: game.white.analysis!, black: game.black.analysis!) - : null; - return _LoadedAnalysisScreen( - options: options.copyWith( - id: game.id, - opening: game.meta.opening, - division: game.meta.division, - serverAnalysis: serverAnalysis, - pgn: game.makePgn(), - ), - enableDrawingShapes: enableDrawingShapes, - ); - }, - loading: () => const Center(child: CircularProgressIndicator.adaptive()), - error: (error, _) { - return Center( - child: Text('Cannot load game analysis: $error'), - ); - }, - ); - } -} - -class _LoadedAnalysisScreen extends ConsumerStatefulWidget { - const _LoadedAnalysisScreen({ - required this.options, - required this.enableDrawingShapes, - }); - - final AnalysisOptions options; - - final bool enableDrawingShapes; - - @override - ConsumerState<_LoadedAnalysisScreen> createState() => - _LoadedAnalysisScreenState(); -} - -class _LoadedAnalysisScreenState extends ConsumerState<_LoadedAnalysisScreen> +class _AnalysisScreenState extends ConsumerState with SingleTickerProviderStateMixin { late final TabController _tabController; late final List tabs; @@ -132,7 +57,7 @@ class _LoadedAnalysisScreenState extends ConsumerState<_LoadedAnalysisScreen> tabs = [ AnalysisTab.opening, AnalysisTab.moves, - if (widget.options.canShowGameSummary) AnalysisTab.summary, + if (widget.options.isLichessGameAnalysis) AnalysisTab.summary, ]; _tabController = TabController( @@ -151,44 +76,64 @@ class _LoadedAnalysisScreenState extends ConsumerState<_LoadedAnalysisScreen> @override Widget build(BuildContext context) { final ctrlProvider = analysisControllerProvider(widget.options); - final currentNodeEval = - ref.watch(ctrlProvider.select((value) => value.currentNode.eval)); + final asyncState = ref.watch(ctrlProvider); + + final appBarActions = [ + EngineDepth(defaultEval: asyncState.valueOrNull?.currentNode.eval), + AppBarAnalysisTabIndicator( + tabs: tabs, + controller: _tabController, + ), + AppBarIconButton( + onPressed: () => showAdaptiveBottomSheet( + context: context, + isScrollControlled: true, + showDragHandle: true, + isDismissible: true, + builder: (_) => AnalysisSettings(widget.options), + ), + semanticsLabel: context.l10n.settingsSettings, + icon: const Icon(Icons.settings), + ), + ]; - return PlatformScaffold( - resizeToAvoidBottomInset: false, - appBar: PlatformAppBar( - title: _Title(options: widget.options), - actions: [ - EngineDepth(defaultEval: currentNodeEval), - AppBarAnalysisTabIndicator( - tabs: tabs, + switch (asyncState) { + case AsyncData(:final value): + return PlatformScaffold( + resizeToAvoidBottomInset: false, + appBar: PlatformAppBar( + title: _Title(variant: value.variant), + actions: appBarActions, + ), + body: _Body( controller: _tabController, + options: widget.options, + enableDrawingShapes: widget.enableDrawingShapes, ), - AppBarIconButton( - onPressed: () => showAdaptiveBottomSheet( - context: context, - isScrollControlled: true, - showDragHandle: true, - isDismissible: true, - builder: (_) => AnalysisSettings(widget.options), - ), - semanticsLabel: context.l10n.settingsSettings, - icon: const Icon(Icons.settings), + ); + case AsyncError(:final error, :final stackTrace): + _logger.severe('Cannot load study: $error', stackTrace); + return FullScreenRetryRequest( + onRetry: () { + ref.invalidate(ctrlProvider); + }, + ); + case _: + return PlatformScaffold( + resizeToAvoidBottomInset: false, + appBar: PlatformAppBar( + title: const _Title(variant: Variant.standard), + actions: appBarActions, ), - ], - ), - body: _Body( - controller: _tabController, - options: widget.options, - enableDrawingShapes: widget.enableDrawingShapes, - ), - ); + body: const Center(child: CircularProgressIndicator()), + ); + } } } class _Title extends StatelessWidget { - const _Title({required this.options}); - final AnalysisOptions options; + const _Title({required this.variant}); + final Variant variant; static const excludedIcons = [Variant.standard, Variant.fromPosition]; @@ -197,8 +142,8 @@ class _Title extends StatelessWidget { return Row( mainAxisSize: MainAxisSize.min, children: [ - if (!excludedIcons.contains(options.variant)) ...[ - Icon(options.variant.icon), + if (!excludedIcons.contains(variant)) ...[ + Icon(variant.icon), const SizedBox(width: 5.0), ], Text(context.l10n.analysis), @@ -225,7 +170,7 @@ class _Body extends ConsumerWidget { ); final ctrlProvider = analysisControllerProvider(options); - final analysisState = ref.watch(ctrlProvider); + final analysisState = ref.watch(ctrlProvider).requireValue; final isEngineAvailable = analysisState.isEngineAvailable; final hasEval = analysisState.hasAvailableEval; @@ -269,7 +214,7 @@ class _Body extends ConsumerWidget { children: [ OpeningExplorerView(options: options), AnalysisTreeView(options), - if (options.canShowGameSummary) ServerAnalysisSummary(options), + if (options.isLichessGameAnalysis) ServerAnalysisSummary(options), ], ); } @@ -283,7 +228,7 @@ class _BottomBar extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final ctrlProvider = analysisControllerProvider(options); - final analysisState = ref.watch(ctrlProvider); + final analysisState = ref.watch(ctrlProvider).requireValue; return BottomBar( children: [ @@ -344,7 +289,8 @@ class _BottomBar extends ConsumerWidget { BottomSheetAction( makeLabel: (context) => Text(context.l10n.boardEditor), onPressed: (context) { - final analysisState = ref.read(analysisControllerProvider(options)); + final analysisState = + ref.read(analysisControllerProvider(options)).requireValue; final boardFen = analysisState.position.fen; pushPlatformRoute( context, @@ -368,26 +314,27 @@ class _BottomBar extends ConsumerWidget { BottomSheetAction( makeLabel: (context) => Text(context.l10n.mobileSharePositionAsFEN), onPressed: (_) { - final analysisState = ref.read(analysisControllerProvider(options)); + final analysisState = + ref.read(analysisControllerProvider(options)).requireValue; launchShareDialog( context, text: analysisState.position.fen, ); }, ), - if (options.gameAnyId != null) + if (options.gameId != null) BottomSheetAction( makeLabel: (context) => Text(context.l10n.screenshotCurrentPosition), onPressed: (_) async { - final gameId = options.gameAnyId!.gameId; + final gameId = options.gameId!; final analysisState = - ref.read(analysisControllerProvider(options)); + ref.read(analysisControllerProvider(options)).requireValue; try { final image = await ref.read(gameShareServiceProvider).screenshotPosition( gameId, - options.orientation, + analysisState.pov, analysisState.position.fen, analysisState.lastMove, ); diff --git a/lib/src/view/analysis/analysis_settings.dart b/lib/src/view/analysis/analysis_settings.dart index 380687363a..f7101bd8c4 100644 --- a/lib/src/view/analysis/analysis_settings.dart +++ b/lib/src/view/analysis/analysis_settings.dart @@ -6,6 +6,7 @@ import 'package:lichess_mobile/src/model/engine/evaluation_service.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/view/opening_explorer/opening_explorer_settings.dart'; import 'package:lichess_mobile/src/widgets/adaptive_bottom_sheet.dart'; +import 'package:lichess_mobile/src/widgets/feedback.dart'; import 'package:lichess_mobile/src/widgets/list.dart'; import 'package:lichess_mobile/src/widgets/non_linear_slider.dart'; import 'package:lichess_mobile/src/widgets/settings.dart'; @@ -18,123 +19,127 @@ class AnalysisSettings extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final ctrlProvider = analysisControllerProvider(options); - final isLocalEvaluationAllowed = - ref.watch(ctrlProvider.select((s) => s.isLocalEvaluationAllowed)); - final isEngineAvailable = ref.watch( - ctrlProvider.select((s) => s.isEngineAvailable), - ); final prefs = ref.watch(analysisPreferencesProvider); + final asyncState = ref.watch(ctrlProvider); - return BottomSheetScrollableContainer( - children: [ - PlatformListTile( - title: Text(context.l10n.openingExplorer), - onTap: () => showAdaptiveBottomSheet( - context: context, - isScrollControlled: true, - showDragHandle: true, - isDismissible: true, - builder: (_) => OpeningExplorerSettings(options), - ), - trailing: const Icon(CupertinoIcons.chevron_right), - ), - SwitchSettingTile( - title: Text(context.l10n.toggleLocalEvaluation), - value: prefs.enableLocalEvaluation, - onChanged: isLocalEvaluationAllowed - ? (_) { - ref.read(ctrlProvider.notifier).toggleLocalEvaluation(); - } - : null, - ), - PlatformListTile( - title: Text.rich( - TextSpan( - text: '${context.l10n.multipleLines}: ', - style: const TextStyle( - fontWeight: FontWeight.normal, + switch (asyncState) { + case AsyncData(:final value): + return BottomSheetScrollableContainer( + children: [ + PlatformListTile( + title: Text(context.l10n.openingExplorer), + onTap: () => showAdaptiveBottomSheet( + context: context, + isScrollControlled: true, + showDragHandle: true, + isDismissible: true, + builder: (_) => OpeningExplorerSettings(options), ), - children: [ + trailing: const Icon(CupertinoIcons.chevron_right), + ), + SwitchSettingTile( + title: Text(context.l10n.toggleLocalEvaluation), + value: prefs.enableLocalEvaluation, + onChanged: value.isLocalEvaluationAllowed + ? (_) { + ref.read(ctrlProvider.notifier).toggleLocalEvaluation(); + } + : null, + ), + PlatformListTile( + title: Text.rich( TextSpan( + text: '${context.l10n.multipleLines}: ', style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 18, + fontWeight: FontWeight.normal, ), - text: prefs.numEvalLines.toString(), + children: [ + TextSpan( + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 18, + ), + text: prefs.numEvalLines.toString(), + ), + ], ), - ], + ), + subtitle: NonLinearSlider( + value: prefs.numEvalLines, + values: const [0, 1, 2, 3], + onChangeEnd: value.isEngineAvailable + ? (value) => ref + .read(ctrlProvider.notifier) + .setNumEvalLines(value.toInt()) + : null, + ), ), - ), - subtitle: NonLinearSlider( - value: prefs.numEvalLines, - values: const [0, 1, 2, 3], - onChangeEnd: isEngineAvailable - ? (value) => ref - .read(ctrlProvider.notifier) - .setNumEvalLines(value.toInt()) - : null, - ), - ), - if (maxEngineCores > 1) - PlatformListTile( - title: Text.rich( - TextSpan( - text: '${context.l10n.cpus}: ', - style: const TextStyle( - fontWeight: FontWeight.normal, - ), - children: [ + if (maxEngineCores > 1) + PlatformListTile( + title: Text.rich( TextSpan( + text: '${context.l10n.cpus}: ', style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 18, + fontWeight: FontWeight.normal, ), - text: prefs.numEngineCores.toString(), + children: [ + TextSpan( + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 18, + ), + text: prefs.numEngineCores.toString(), + ), + ], ), - ], + ), + subtitle: NonLinearSlider( + value: prefs.numEngineCores, + values: List.generate(maxEngineCores, (index) => index + 1), + onChangeEnd: value.isEngineAvailable + ? (value) => ref + .read(ctrlProvider.notifier) + .setEngineCores(value.toInt()) + : null, + ), ), - ), - subtitle: NonLinearSlider( - value: prefs.numEngineCores, - values: List.generate(maxEngineCores, (index) => index + 1), - onChangeEnd: isEngineAvailable + SwitchSettingTile( + title: Text(context.l10n.bestMoveArrow), + value: prefs.showBestMoveArrow, + onChanged: value.isEngineAvailable ? (value) => ref - .read(ctrlProvider.notifier) - .setEngineCores(value.toInt()) + .read(analysisPreferencesProvider.notifier) + .toggleShowBestMoveArrow() : null, ), - ), - SwitchSettingTile( - title: Text(context.l10n.bestMoveArrow), - value: prefs.showBestMoveArrow, - onChanged: isEngineAvailable - ? (value) => ref + SwitchSettingTile( + title: Text(context.l10n.evaluationGauge), + value: prefs.showEvaluationGauge, + onChanged: (value) => ref + .read(analysisPreferencesProvider.notifier) + .toggleShowEvaluationGauge(), + ), + SwitchSettingTile( + title: Text(context.l10n.toggleGlyphAnnotations), + value: prefs.showAnnotations, + onChanged: (_) => ref .read(analysisPreferencesProvider.notifier) - .toggleShowBestMoveArrow() - : null, - ), - SwitchSettingTile( - title: Text(context.l10n.evaluationGauge), - value: prefs.showEvaluationGauge, - onChanged: (value) => ref - .read(analysisPreferencesProvider.notifier) - .toggleShowEvaluationGauge(), - ), - SwitchSettingTile( - title: Text(context.l10n.toggleGlyphAnnotations), - value: prefs.showAnnotations, - onChanged: (_) => ref - .read(analysisPreferencesProvider.notifier) - .toggleAnnotations(), - ), - SwitchSettingTile( - title: Text(context.l10n.mobileShowComments), - value: prefs.showPgnComments, - onChanged: (_) => ref - .read(analysisPreferencesProvider.notifier) - .togglePgnComments(), - ), - ], - ); + .toggleAnnotations(), + ), + SwitchSettingTile( + title: Text(context.l10n.mobileShowComments), + value: prefs.showPgnComments, + onChanged: (_) => ref + .read(analysisPreferencesProvider.notifier) + .togglePgnComments(), + ), + ], + ); + case AsyncError(:final error): + debugPrint('Error loading analysis: $error'); + return const SizedBox.shrink(); + case _: + return const CenterLoadingIndicator(); + } } } diff --git a/lib/src/view/analysis/analysis_share_screen.dart b/lib/src/view/analysis/analysis_share_screen.dart index 785e8b0cf7..63552ffe2e 100644 --- a/lib/src/view/analysis/analysis_share_screen.dart +++ b/lib/src/view/analysis/analysis_share_screen.dart @@ -56,7 +56,7 @@ class _EditPgnTagsFormState extends ConsumerState<_EditPgnTagsForm> { void initState() { super.initState(); final ctrlProvider = analysisControllerProvider(widget.options); - final pgnHeaders = ref.read(ctrlProvider).pgnHeaders; + final pgnHeaders = ref.read(ctrlProvider).requireValue.pgnHeaders; for (final entry in pgnHeaders.entries) { _controllers[entry.key] = TextEditingController(text: entry.value); @@ -86,7 +86,8 @@ class _EditPgnTagsFormState extends ConsumerState<_EditPgnTagsForm> { @override Widget build(BuildContext context) { final ctrlProvider = analysisControllerProvider(widget.options); - final pgnHeaders = ref.watch(ctrlProvider.select((c) => c.pgnHeaders)); + final pgnHeaders = + ref.watch(ctrlProvider.select((c) => c.requireValue.pgnHeaders)); final showRatingAsync = ref.watch(showRatingsPrefProvider); void focusAndSelectNextField(int index, IMap pgnHeaders) { diff --git a/lib/src/view/analysis/opening_explorer_view.dart b/lib/src/view/analysis/opening_explorer_view.dart index b06f1efc34..5f82d006dc 100644 --- a/lib/src/view/analysis/opening_explorer_view.dart +++ b/lib/src/view/analysis/opening_explorer_view.dart @@ -35,7 +35,8 @@ class _OpeningExplorerState extends ConsumerState { @override Widget build(BuildContext context) { - final analysisState = ref.watch(analysisControllerProvider(widget.options)); + final analysisState = + ref.watch(analysisControllerProvider(widget.options)).requireValue; if (analysisState.position.ply >= 50) { return _OpeningExplorerView( diff --git a/lib/src/view/analysis/server_analysis.dart b/lib/src/view/analysis/server_analysis.dart index ba0ac8f359..d943bddb25 100644 --- a/lib/src/view/analysis/server_analysis.dart +++ b/lib/src/view/analysis/server_analysis.dart @@ -23,16 +23,17 @@ class ServerAnalysisSummary extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final ctrlProvider = analysisControllerProvider(options); - final playersAnalysis = - ref.watch(ctrlProvider.select((value) => value.playersAnalysis)); - final pgnHeaders = - ref.watch(ctrlProvider.select((value) => value.pgnHeaders)); + final playersAnalysis = ref.watch( + ctrlProvider.select((value) => value.requireValue.playersAnalysis), + ); + final pgnHeaders = ref + .watch(ctrlProvider.select((value) => value.requireValue.pgnHeaders)); final currentGameAnalysis = ref.watch(currentAnalysisProvider); return playersAnalysis != null ? ListView( children: [ - if (currentGameAnalysis == options.gameAnyId?.gameId) + if (currentGameAnalysis == options.gameId) const Padding( padding: EdgeInsets.only(top: 16.0), child: WaitingForServerAnalysis(), @@ -164,7 +165,7 @@ class ServerAnalysisSummary extends ConsumerWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ const Spacer(), - if (currentGameAnalysis == options.gameAnyId?.gameId) + if (currentGameAnalysis == options.gameId) const Center( child: Padding( padding: EdgeInsets.symmetric(vertical: 16.0), @@ -356,23 +357,11 @@ class AcplChart extends ConsumerWidget { ), ); - final data = ref.watch( - analysisControllerProvider(options) - .select((value) => value.acplChartData), - ); - - final rootPly = ref.watch( - analysisControllerProvider(options) - .select((value) => value.root.position.ply), - ); - - final currentNode = ref.watch( - analysisControllerProvider(options).select((value) => value.currentNode), - ); - - final isOnMainline = ref.watch( - analysisControllerProvider(options).select((value) => value.isOnMainline), - ); + final state = ref.watch(analysisControllerProvider(options)).requireValue; + final data = state.acplChartData; + final rootPly = state.root.position.ply; + final currentNode = state.currentNode; + final isOnMainline = state.isOnMainline; if (data == null) { return const SizedBox.shrink(); @@ -386,12 +375,12 @@ class AcplChart extends ConsumerWidget { final divisionLines = []; - if (options.division?.middlegame != null) { - if (options.division!.middlegame! > 0) { + if (state.division?.middlegame != null) { + if (state.division!.middlegame! > 0) { divisionLines.add(phaseVerticalBar(0.0, context.l10n.opening)); divisionLines.add( phaseVerticalBar( - options.division!.middlegame! - 1, + state.division!.middlegame! - 1, context.l10n.middlegame, ), ); @@ -400,11 +389,11 @@ class AcplChart extends ConsumerWidget { } } - if (options.division?.endgame != null) { - if (options.division!.endgame! > 0) { + if (state.division?.endgame != null) { + if (state.division!.endgame! > 0) { divisionLines.add( phaseVerticalBar( - options.division!.endgame! - 1, + state.division!.endgame! - 1, context.l10n.endgame, ), ); diff --git a/lib/src/view/analysis/tree_view.dart b/lib/src/view/analysis/tree_view.dart index de648427d7..54c55d7bce 100644 --- a/lib/src/view/analysis/tree_view.dart +++ b/lib/src/view/analysis/tree_view.dart @@ -17,15 +17,20 @@ class AnalysisTreeView extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final ctrlProvider = analysisControllerProvider(options); - final root = ref.watch(ctrlProvider.select((value) => value.root)); - final currentPath = - ref.watch(ctrlProvider.select((value) => value.currentPath)); - final pgnRootComments = - ref.watch(ctrlProvider.select((value) => value.pgnRootComments)); + final variant = ref.watch( + ctrlProvider.select((value) => value.requireValue.variant), + ); + final root = + ref.watch(ctrlProvider.select((value) => value.requireValue.root)); + final currentPath = ref + .watch(ctrlProvider.select((value) => value.requireValue.currentPath)); + final pgnRootComments = ref.watch( + ctrlProvider.select((value) => value.requireValue.pgnRootComments), + ); return CustomScrollView( slivers: [ - if (kOpeningAllowedVariants.contains(options.variant)) + if (kOpeningAllowedVariants.contains(variant)) SliverPersistentHeader( delegate: _OpeningHeaderDelegate(ctrlProvider), ), @@ -76,14 +81,14 @@ class _Opening extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final isRootNode = ref.watch( - ctrlProvider.select((s) => s.currentNode.isRoot), + ctrlProvider.select((s) => s.requireValue.currentNode.isRoot), ); - final nodeOpening = - ref.watch(ctrlProvider.select((s) => s.currentNode.opening)); - final branchOpening = - ref.watch(ctrlProvider.select((s) => s.currentBranchOpening)); + final nodeOpening = ref + .watch(ctrlProvider.select((s) => s.requireValue.currentNode.opening)); + final branchOpening = ref + .watch(ctrlProvider.select((s) => s.requireValue.currentBranchOpening)); final contextOpening = - ref.watch(ctrlProvider.select((s) => s.contextOpening)); + ref.watch(ctrlProvider.select((s) => s.requireValue.contextOpening)); final opening = isRootNode ? LightOpening( eco: '', diff --git a/lib/src/view/board_editor/board_editor_screen.dart b/lib/src/view/board_editor/board_editor_screen.dart index 7b4006a4ba..a4269e7873 100644 --- a/lib/src/view/board_editor/board_editor_screen.dart +++ b/lib/src/view/board_editor/board_editor_screen.dart @@ -335,13 +335,13 @@ class _BottomBar extends ConsumerWidget { context, rootNavigator: true, builder: (context) => AnalysisScreen( - pgnOrId: editorState.pgn!, options: AnalysisOptions( - pgn: '', - isLocalEvaluationAllowed: true, - variant: Variant.fromPosition, - orientation: editorState.orientation, - id: standaloneAnalysisId, + standalone: ( + pgn: editorState.pgn!, + isLocalEvaluationAllowed: true, + variant: Variant.fromPosition, + orientation: editorState.orientation, + ), ), ), ); diff --git a/lib/src/view/correspondence/offline_correspondence_game_screen.dart b/lib/src/view/correspondence/offline_correspondence_game_screen.dart index c816be5561..45d3ccd843 100644 --- a/lib/src/view/correspondence/offline_correspondence_game_screen.dart +++ b/lib/src/view/correspondence/offline_correspondence_game_screen.dart @@ -259,15 +259,14 @@ class _BodyState extends ConsumerState<_Body> { pushPlatformRoute( context, builder: (_) => AnalysisScreen( - pgnOrId: game.makePgn(), options: AnalysisOptions( - pgn: '', - isLocalEvaluationAllowed: false, - variant: game.variant, + standalone: ( + pgn: game.makePgn(), + isLocalEvaluationAllowed: false, + variant: game.variant, + orientation: game.youAre, + ), initialMoveCursor: stepCursor, - orientation: game.youAre, - id: game.id, - division: game.meta.division, ), ), ); diff --git a/lib/src/view/game/archived_game_screen.dart b/lib/src/view/game/archived_game_screen.dart index a8b0193202..b6abcc72da 100644 --- a/lib/src/view/game/archived_game_screen.dart +++ b/lib/src/view/game/archived_game_screen.dart @@ -375,27 +375,10 @@ class _BottomBar extends ConsumerWidget { label: context.l10n.gameAnalysis, onTap: ref.read(gameCursorProvider(gameData.id)).hasValue ? () { - final (game, cursor) = ref - .read( - gameCursorProvider(gameData.id), - ) - .requireValue; - pushPlatformRoute( context, builder: (context) => AnalysisScreen( - pgnOrId: game.makePgn(), - options: AnalysisOptions( - pgn: '', - isLocalEvaluationAllowed: true, - variant: gameData.variant, - initialMoveCursor: cursor, - orientation: orientation, - id: gameData.id, - opening: gameData.opening, - serverAnalysis: game.serverAnalysis, - division: game.meta.division, - ), + options: AnalysisOptions(gameId: gameData.id), ), ); } diff --git a/lib/src/view/game/game_body.dart b/lib/src/view/game/game_body.dart index 77a5a209d8..e6b6a6b341 100644 --- a/lib/src/view/game/game_body.dart +++ b/lib/src/view/game/game_body.dart @@ -569,7 +569,6 @@ class _GameBottomBar extends ConsumerWidget { pushPlatformRoute( context, builder: (_) => AnalysisScreen( - pgnOrId: gameState.analysisPgn, options: gameState.analysisOptions, ), ); @@ -702,9 +701,8 @@ class _GameBottomBar extends ConsumerWidget { pushPlatformRoute( context, builder: (_) => AnalysisScreen( - pgnOrId: gameState.analysisPgn, options: gameState.analysisOptions.copyWith( - isLocalEvaluationAllowed: false, + gameId: gameState.game.id, ), ), ); diff --git a/lib/src/view/game/game_list_tile.dart b/lib/src/view/game/game_list_tile.dart index c54d6c5106..1f7417d3db 100644 --- a/lib/src/view/game/game_list_tile.dart +++ b/lib/src/view/game/game_list_tile.dart @@ -226,14 +226,7 @@ class _ContextMenu extends ConsumerWidget { pushPlatformRoute( context, builder: (context) => AnalysisScreen( - pgnOrId: game.id.value, - options: AnalysisOptions( - pgn: '', - isLocalEvaluationAllowed: true, - variant: game.variant, - orientation: orientation, - id: game.id, - ), + options: AnalysisOptions(gameId: game.id), ), ); } diff --git a/lib/src/view/game/game_result_dialog.dart b/lib/src/view/game/game_result_dialog.dart index 20eeb61b7b..4e09a5cf9d 100644 --- a/lib/src/view/game/game_result_dialog.dart +++ b/lib/src/view/game/game_result_dialog.dart @@ -191,7 +191,6 @@ class _GameEndDialogState extends ConsumerState { pushPlatformRoute( context, builder: (_) => AnalysisScreen( - pgnOrId: gameState.analysisPgn, options: gameState.analysisOptions, ), ); @@ -261,13 +260,13 @@ class OverTheBoardGameResultDialog extends StatelessWidget { pushPlatformRoute( context, builder: (_) => AnalysisScreen( - pgnOrId: game.makePgn(), options: AnalysisOptions( - pgn: '', - isLocalEvaluationAllowed: true, - variant: game.meta.variant, - orientation: Side.white, - id: standaloneAnalysisId, + standalone: ( + pgn: game.makePgn(), + isLocalEvaluationAllowed: true, + variant: game.meta.variant, + orientation: Side.white, + ), ), ), ); diff --git a/lib/src/view/opening_explorer/opening_explorer_screen.dart b/lib/src/view/opening_explorer/opening_explorer_screen.dart index 6823005fed..063a134d3b 100644 --- a/lib/src/view/opening_explorer/opening_explorer_screen.dart +++ b/lib/src/view/opening_explorer/opening_explorer_screen.dart @@ -17,6 +17,7 @@ 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/bottom_bar_button.dart'; import 'package:lichess_mobile/src/widgets/buttons.dart'; +import 'package:lichess_mobile/src/widgets/feedback.dart'; import 'package:lichess_mobile/src/widgets/move_list.dart'; import 'package:lichess_mobile/src/widgets/platform.dart'; import 'package:lichess_mobile/src/widgets/shimmer.dart'; @@ -50,121 +51,204 @@ class _OpeningExplorerState extends ConsumerState { @override Widget build(BuildContext context) { - final analysisState = ref.watch(analysisControllerProvider(widget.options)); - - final opening = analysisState.currentNode.isRoot - ? LightOpening( - eco: '', - name: context.l10n.startPosition, - ) - : analysisState.currentNode.opening ?? - analysisState.currentBranchOpening ?? - analysisState.contextOpening; - - final Widget openingHeader = Container( - padding: _kTableRowPadding, - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.secondaryContainer, - ), - child: opening != null - ? GestureDetector( - onTap: opening.name == context.l10n.startPosition - ? null - : () => launchUrl( - Uri.parse( - 'https://lichess.org/opening/${opening.name}', - ), - ), - child: Row( - children: [ - Icon( - Icons.open_in_browser_outlined, - color: Theme.of(context).colorScheme.onSecondaryContainer, - ), - const SizedBox(width: 6.0), - Expanded( - child: Text( - opening.name, - style: TextStyle( + switch (ref.watch(analysisControllerProvider(widget.options))) { + case AsyncData(value: final analysisState): + final opening = analysisState.currentNode.isRoot + ? LightOpening( + eco: '', + name: context.l10n.startPosition, + ) + : analysisState.currentNode.opening ?? + analysisState.currentBranchOpening ?? + analysisState.contextOpening; + + final Widget openingHeader = Container( + padding: _kTableRowPadding, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.secondaryContainer, + ), + child: opening != null + ? GestureDetector( + onTap: opening.name == context.l10n.startPosition + ? null + : () => launchUrl( + Uri.parse( + 'https://lichess.org/opening/${opening.name}', + ), + ), + child: Row( + children: [ + Icon( + Icons.open_in_browser_outlined, color: Theme.of(context).colorScheme.onSecondaryContainer, - fontWeight: FontWeight.bold, ), - ), + const SizedBox(width: 6.0), + Expanded( + child: Text( + opening.name, + style: TextStyle( + color: Theme.of(context) + .colorScheme + .onSecondaryContainer, + fontWeight: FontWeight.bold, + ), + ), + ), + ], ), - ], - ), - ) - : const SizedBox.shrink(), - ); + ) + : const SizedBox.shrink(), + ); - if (analysisState.position.ply >= 50) { - return _OpeningExplorerView( - options: widget.options, - isLoading: false, - isIndexing: false, - children: [ - openingHeader, - OpeningExplorerMoveTable.maxDepth( + if (analysisState.position.ply >= 50) { + return _OpeningExplorerView( options: widget.options, - ), - ], - ); - } + isLoading: false, + isIndexing: false, + children: [ + openingHeader, + OpeningExplorerMoveTable.maxDepth( + options: widget.options, + ), + ], + ); + } - final prefs = ref.watch(openingExplorerPreferencesProvider); + final prefs = ref.watch(openingExplorerPreferencesProvider); - if (prefs.db == OpeningDatabase.player && prefs.playerDb.username == null) { - return _OpeningExplorerView( - options: widget.options, - isLoading: false, - isIndexing: false, - children: [ - openingHeader, - const Padding( - padding: _kTableRowPadding, - child: Center( - // TODO: l10n - child: Text('Select a Lichess player in the settings.'), - ), - ), - ], - ); - } + if (prefs.db == OpeningDatabase.player && + prefs.playerDb.username == null) { + return _OpeningExplorerView( + options: widget.options, + isLoading: false, + isIndexing: false, + children: [ + openingHeader, + const Padding( + padding: _kTableRowPadding, + child: Center( + // TODO: l10n + child: Text('Select a Lichess player in the settings.'), + ), + ), + ], + ); + } + + final cacheKey = OpeningExplorerCacheKey( + fen: analysisState.position.fen, + prefs: prefs, + ); + final cacheOpeningExplorer = cache[cacheKey]; + final openingExplorerAsync = cacheOpeningExplorer != null + ? AsyncValue.data( + (entry: cacheOpeningExplorer, isIndexing: false), + ) + : ref.watch( + openingExplorerProvider(fen: analysisState.position.fen), + ); + + if (cacheOpeningExplorer == null) { + ref.listen(openingExplorerProvider(fen: analysisState.position.fen), + (_, curAsync) { + curAsync.whenData((cur) { + if (cur != null && !cur.isIndexing) { + cache[cacheKey] = cur.entry; + } + }); + }); + } + + final isLoading = openingExplorerAsync.isLoading || + openingExplorerAsync.value == null; + + return _OpeningExplorerView( + options: widget.options, + isLoading: isLoading, + isIndexing: openingExplorerAsync.value?.isIndexing ?? false, + children: openingExplorerAsync.when( + data: (openingExplorer) { + if (openingExplorer == null) { + return lastExplorerWidgets ?? + [ + Shimmer( + child: ShimmerLoading( + isLoading: true, + child: OpeningExplorerMoveTable.loading( + options: widget.options, + ), + ), + ), + ]; + } + + final topGames = openingExplorer.entry.topGames; + final recentGames = openingExplorer.entry.recentGames; + + final ply = analysisState.position.ply; + + final children = [ + openingHeader, + OpeningExplorerMoveTable( + moves: openingExplorer.entry.moves, + whiteWins: openingExplorer.entry.white, + draws: openingExplorer.entry.draws, + blackWins: openingExplorer.entry.black, + options: widget.options, + ), + if (topGames != null && topGames.isNotEmpty) ...[ + OpeningExplorerHeaderTile( + key: const Key('topGamesHeader'), + child: Text(context.l10n.topGames), + ), + ...List.generate( + topGames.length, + (int index) { + return OpeningExplorerGameTile( + key: Key('top-game-${topGames.get(index).id}'), + game: topGames.get(index), + color: index.isEven + ? Theme.of(context).colorScheme.surfaceContainerLow + : Theme.of(context) + .colorScheme + .surfaceContainerHigh, + ply: ply, + ); + }, + growable: false, + ), + ], + if (recentGames != null && recentGames.isNotEmpty) ...[ + OpeningExplorerHeaderTile( + key: const Key('recentGamesHeader'), + child: Text(context.l10n.recentGames), + ), + ...List.generate( + recentGames.length, + (int index) { + return OpeningExplorerGameTile( + key: Key('recent-game-${recentGames.get(index).id}'), + game: recentGames.get(index), + color: index.isEven + ? Theme.of(context).colorScheme.surfaceContainerLow + : Theme.of(context) + .colorScheme + .surfaceContainerHigh, + ply: ply, + ); + }, + growable: false, + ), + ], + ]; - final cacheKey = OpeningExplorerCacheKey( - fen: analysisState.position.fen, - prefs: prefs, - ); - final cacheOpeningExplorer = cache[cacheKey]; - final openingExplorerAsync = cacheOpeningExplorer != null - ? AsyncValue.data( - (entry: cacheOpeningExplorer, isIndexing: false), - ) - : ref.watch(openingExplorerProvider(fen: analysisState.position.fen)); - - if (cacheOpeningExplorer == null) { - ref.listen(openingExplorerProvider(fen: analysisState.position.fen), - (_, curAsync) { - curAsync.whenData((cur) { - if (cur != null && !cur.isIndexing) { - cache[cacheKey] = cur.entry; - } - }); - }); - } + lastExplorerWidgets = children; - final isLoading = - openingExplorerAsync.isLoading || openingExplorerAsync.value == null; - - return _OpeningExplorerView( - options: widget.options, - isLoading: isLoading, - isIndexing: openingExplorerAsync.value?.isIndexing ?? false, - children: openingExplorerAsync.when( - data: (openingExplorer) { - if (openingExplorer == null) { - return lastExplorerWidgets ?? + return children; + }, + loading: () => + lastExplorerWidgets ?? [ Shimmer( child: ShimmerLoading( @@ -174,96 +258,35 @@ class _OpeningExplorerState extends ConsumerState { ), ), ), - ]; - } - - final topGames = openingExplorer.entry.topGames; - final recentGames = openingExplorer.entry.recentGames; - - final ply = analysisState.position.ply; - - final children = [ - openingHeader, - OpeningExplorerMoveTable( - moves: openingExplorer.entry.moves, - whiteWins: openingExplorer.entry.white, - draws: openingExplorer.entry.draws, - blackWins: openingExplorer.entry.black, - options: widget.options, - ), - if (topGames != null && topGames.isNotEmpty) ...[ - OpeningExplorerHeaderTile( - key: const Key('topGamesHeader'), - child: Text(context.l10n.topGames), - ), - ...List.generate( - topGames.length, - (int index) { - return OpeningExplorerGameTile( - key: Key('top-game-${topGames.get(index).id}'), - game: topGames.get(index), - color: index.isEven - ? Theme.of(context).colorScheme.surfaceContainerLow - : Theme.of(context).colorScheme.surfaceContainerHigh, - ply: ply, - ); - }, - growable: false, - ), - ], - if (recentGames != null && recentGames.isNotEmpty) ...[ - OpeningExplorerHeaderTile( - key: const Key('recentGamesHeader'), - child: Text(context.l10n.recentGames), - ), - ...List.generate( - recentGames.length, - (int index) { - return OpeningExplorerGameTile( - key: Key('recent-game-${recentGames.get(index).id}'), - game: recentGames.get(index), - color: index.isEven - ? Theme.of(context).colorScheme.surfaceContainerLow - : Theme.of(context).colorScheme.surfaceContainerHigh, - ply: ply, - ); - }, - growable: false, - ), - ], - ]; - - lastExplorerWidgets = children; - - return children; - }, - loading: () => - lastExplorerWidgets ?? - [ - Shimmer( - child: ShimmerLoading( - isLoading: true, - child: OpeningExplorerMoveTable.loading( - options: widget.options, + ], + error: (e, s) { + debugPrint( + 'SEVERE: [OpeningExplorerScreen] could not load opening explorer data; $e\n$s', + ); + return [ + Center( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Text(e.toString()), ), ), - ), - ], - error: (e, s) { - debugPrint( - 'SEVERE: [OpeningExplorerScreen] could not load opening explorer data; $e\n$s', - ); - return [ - Center( - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Text(e.toString()), - ), - ), - ]; - }, - ), - ); + ]; + }, + ), + ); + case AsyncError(:final error): + debugPrint( + 'SEVERE: [OpeningExplorerScreen] could not load analysis data; $error', + ); + return Center( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Text(error.toString()), + ), + ); + case _: + return const CenterLoadingIndicator(); + } } } @@ -330,6 +353,7 @@ class _OpeningExplorerView extends StatelessWidget { options, boardSize, borderRadius: isTablet ? _kTabletBoardRadius : null, + shouldReplaceChildOnUserMove: true, ), ), Flexible( @@ -379,6 +403,7 @@ class _OpeningExplorerView extends StatelessWidget { child: AnalysisBoard( options, boardSize, + shouldReplaceChildOnUserMove: true, ), ), ...children, @@ -477,7 +502,7 @@ class _MoveList extends ConsumerWidget implements PreferredSizeWidget { @override Widget build(BuildContext context, WidgetRef ref) { final ctrlProvider = analysisControllerProvider(options); - final state = ref.watch(ctrlProvider); + final state = ref.watch(ctrlProvider).requireValue; final slicedMoves = state.root.mainline .map((e) => e.sanMove.san) .toList() @@ -519,9 +544,9 @@ class _BottomBar extends ConsumerWidget { .watch(openingExplorerPreferencesProvider.select((value) => value.db)); final ctrlProvider = analysisControllerProvider(options); final canGoBack = - ref.watch(ctrlProvider.select((value) => value.canGoBack)); + ref.watch(ctrlProvider.select((value) => value.requireValue.canGoBack)); final canGoNext = - ref.watch(ctrlProvider.select((value) => value.canGoNext)); + ref.watch(ctrlProvider.select((value) => value.requireValue.canGoNext)); final dbLabel = switch (db) { OpeningDatabase.master => 'Masters', diff --git a/lib/src/view/puzzle/puzzle_screen.dart b/lib/src/view/puzzle/puzzle_screen.dart index b79254f87c..3f7aadeb45 100644 --- a/lib/src/view/puzzle/puzzle_screen.dart +++ b/lib/src/view/puzzle/puzzle_screen.dart @@ -484,13 +484,13 @@ class _BottomBar extends ConsumerWidget { pushPlatformRoute( context, builder: (context) => AnalysisScreen( - pgnOrId: ref.read(ctrlProvider.notifier).makePgn(), options: AnalysisOptions( - pgn: '', - isLocalEvaluationAllowed: true, - variant: Variant.standard, - orientation: puzzleState.pov, - id: standaloneAnalysisId, + standalone: ( + pgn: ref.read(ctrlProvider.notifier).makePgn(), + isLocalEvaluationAllowed: true, + variant: Variant.standard, + orientation: puzzleState.pov, + ), initialMoveCursor: 0, ), ), diff --git a/lib/src/view/puzzle/streak_screen.dart b/lib/src/view/puzzle/streak_screen.dart index 6be35275dc..06fc77e366 100644 --- a/lib/src/view/puzzle/streak_screen.dart +++ b/lib/src/view/puzzle/streak_screen.dart @@ -291,13 +291,13 @@ class _BottomBar extends ConsumerWidget { pushPlatformRoute( context, builder: (context) => AnalysisScreen( - pgnOrId: ref.read(ctrlProvider.notifier).makePgn(), options: AnalysisOptions( - pgn: '', - isLocalEvaluationAllowed: true, - variant: Variant.standard, - orientation: puzzleState.pov, - id: standaloneAnalysisId, + standalone: ( + pgn: ref.read(ctrlProvider.notifier).makePgn(), + isLocalEvaluationAllowed: true, + variant: Variant.standard, + orientation: puzzleState.pov, + ), initialMoveCursor: 0, ), ), diff --git a/lib/src/view/study/study_bottom_bar.dart b/lib/src/view/study/study_bottom_bar.dart index d94d6e4d57..17f19f2f3d 100644 --- a/lib/src/view/study/study_bottom_bar.dart +++ b/lib/src/view/study/study_bottom_bar.dart @@ -165,13 +165,13 @@ class _GamebookBottomBar extends ConsumerWidget { context, rootNavigator: true, builder: (context) => AnalysisScreen( - pgnOrId: state.pgn, options: AnalysisOptions( - pgn: state.pgn, - isLocalEvaluationAllowed: true, - variant: state.variant, - orientation: state.pov, - id: standaloneAnalysisId, + standalone: ( + pgn: state.pgn, + isLocalEvaluationAllowed: true, + variant: state.variant, + orientation: state.pov, + ), ), ), ), diff --git a/lib/src/view/tools/load_position_screen.dart b/lib/src/view/tools/load_position_screen.dart index 27040a0d70..6f92ea14f4 100644 --- a/lib/src/view/tools/load_position_screen.dart +++ b/lib/src/view/tools/load_position_screen.dart @@ -86,7 +86,6 @@ class _BodyState extends State<_Body> { context, rootNavigator: true, builder: (context) => AnalysisScreen( - pgnOrId: parsedInput!.pgn, options: parsedInput!.options, ), ) @@ -121,7 +120,7 @@ class _BodyState extends State<_Body> { } } - ({String pgn, String fen, AnalysisOptions options})? get parsedInput { + ({String fen, AnalysisOptions options})? get parsedInput { if (textInput == null || textInput!.trim().isEmpty) { return null; } @@ -130,14 +129,14 @@ class _BodyState extends State<_Body> { try { final pos = Chess.fromSetup(Setup.parseFen(textInput!.trim())); return ( - pgn: '[FEN "${pos.fen}"]', fen: pos.fen, options: AnalysisOptions( - pgn: '[FEN "${pos.fen}"]', - isLocalEvaluationAllowed: true, - variant: Variant.standard, - orientation: Side.white, - id: standaloneAnalysisId, + standalone: ( + pgn: '[FEN "${pos.fen}"]', + isLocalEvaluationAllowed: true, + variant: Variant.standard, + orientation: Side.white, + ), ) ); } catch (_, __) {} @@ -162,15 +161,15 @@ class _BodyState extends State<_Body> { ); return ( - pgn: textInput!, fen: lastPosition.fen, options: AnalysisOptions( - pgn: textInput!, - isLocalEvaluationAllowed: true, - variant: rule != null ? Variant.fromRule(rule) : Variant.standard, + standalone: ( + pgn: textInput!, + isLocalEvaluationAllowed: true, + variant: rule != null ? Variant.fromRule(rule) : Variant.standard, + orientation: Side.white, + ), initialMoveCursor: mainlineMoves.isEmpty ? 0 : 1, - orientation: Side.white, - id: standaloneAnalysisId, ) ); } catch (_, __) {} diff --git a/lib/src/view/tools/tools_tab_screen.dart b/lib/src/view/tools/tools_tab_screen.dart index b2bba9d51c..b602c80f26 100644 --- a/lib/src/view/tools/tools_tab_screen.dart +++ b/lib/src/view/tools/tools_tab_screen.dart @@ -148,13 +148,13 @@ class _Body extends ConsumerWidget { context, rootNavigator: true, builder: (context) => const AnalysisScreen( - pgnOrId: '', options: AnalysisOptions( - pgn: '', - isLocalEvaluationAllowed: true, - variant: Variant.standard, - orientation: Side.white, - id: standaloneAnalysisId, + standalone: ( + pgn: '', + isLocalEvaluationAllowed: true, + variant: Variant.standard, + orientation: Side.white, + ), ), ), ), @@ -168,11 +168,12 @@ class _Body extends ConsumerWidget { rootNavigator: true, builder: (context) => const OpeningExplorerScreen( options: AnalysisOptions( - pgn: '', - isLocalEvaluationAllowed: false, - variant: Variant.standard, - orientation: Side.white, - id: standaloneOpeningExplorerId, + standalone: ( + pgn: '', + isLocalEvaluationAllowed: false, + variant: Variant.standard, + orientation: Side.white, + ), ), ), ) diff --git a/lib/src/widgets/feedback.dart b/lib/src/widgets/feedback.dart index c637d2b7c8..5c64cdb703 100644 --- a/lib/src/widgets/feedback.dart +++ b/lib/src/widgets/feedback.dart @@ -169,7 +169,6 @@ class FullScreenRetryRequest extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center, children: [ - // TODO translate Text( context.l10n.mobileSomethingWentWrong, style: Styles.sectionTitle, diff --git a/test/view/analysis/analysis_screen_test.dart b/test/view/analysis/analysis_screen_test.dart index 337fdc7785..fcc4f46738 100644 --- a/test/view/analysis/analysis_screen_test.dart +++ b/test/view/analysis/analysis_screen_test.dart @@ -7,14 +7,7 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:lichess_mobile/src/model/analysis/analysis_controller.dart'; import 'package:lichess_mobile/src/model/analysis/analysis_preferences.dart'; import 'package:lichess_mobile/src/model/common/chess.dart'; -import 'package:lichess_mobile/src/model/common/id.dart'; -import 'package:lichess_mobile/src/model/common/perf.dart'; -import 'package:lichess_mobile/src/model/common/speed.dart'; -import 'package:lichess_mobile/src/model/game/archived_game.dart'; -import 'package:lichess_mobile/src/model/game/game_status.dart'; -import 'package:lichess_mobile/src/model/game/player.dart'; import 'package:lichess_mobile/src/model/settings/preferences_storage.dart'; -import 'package:lichess_mobile/src/model/user/user.dart'; import 'package:lichess_mobile/src/view/analysis/analysis_screen.dart'; import 'package:lichess_mobile/src/widgets/bottom_bar_button.dart'; import 'package:lichess_mobile/src/widgets/pgn.dart'; @@ -24,29 +17,26 @@ import '../../test_provider_scope.dart'; void main() { // ignore: avoid_dynamic_calls final sanMoves = jsonDecode(gameResponse)['moves'] as String; - const opening = LightOpening( - eco: 'C20', - name: "King's Pawn Game: Wayward Queen Attack, Kiddie Countergambit", - ); group('Analysis Screen', () { testWidgets('displays correct move and position', (tester) async { final app = await makeTestProviderScopeApp( tester, home: AnalysisScreen( - pgnOrId: sanMoves, options: AnalysisOptions( - pgn: '', - isLocalEvaluationAllowed: false, - variant: Variant.standard, - opening: opening, - orientation: Side.white, - id: gameData.id, + standalone: ( + pgn: sanMoves, + isLocalEvaluationAllowed: false, + orientation: Side.white, + variant: Variant.standard, + ), ), ), ); await tester.pumpWidget(app); + expect(find.byType(CircularProgressIndicator), findsOneWidget); + await tester.pump(const Duration(milliseconds: 1)); expect(find.byType(Chessboard), findsOneWidget); expect(find.byType(PieceWidget), findsNWidgets(25)); @@ -69,19 +59,20 @@ void main() { final app = await makeTestProviderScopeApp( tester, home: AnalysisScreen( - pgnOrId: sanMoves, options: AnalysisOptions( - pgn: '', - isLocalEvaluationAllowed: false, - variant: Variant.standard, - opening: opening, - orientation: Side.white, - id: gameData.id, + standalone: ( + pgn: sanMoves, + isLocalEvaluationAllowed: false, + variant: Variant.standard, + orientation: Side.white, + ), ), ), ); await tester.pumpWidget(app); + expect(find.byType(CircularProgressIndicator), findsOneWidget); + await tester.pump(const Duration(milliseconds: 1)); // cannot go forward expect( @@ -135,20 +126,20 @@ void main() { ), }, home: AnalysisScreen( - pgnOrId: pgn, - options: const AnalysisOptions( - pgn: '', - isLocalEvaluationAllowed: false, - variant: Variant.standard, - orientation: Side.white, - opening: opening, - id: standaloneAnalysisId, + options: AnalysisOptions( + standalone: ( + pgn: pgn, + isLocalEvaluationAllowed: false, + variant: Variant.standard, + orientation: Side.white, + ), ), enableDrawingShapes: false, ), ); await tester.pumpWidget(app); + await tester.pump(const Duration(milliseconds: 1)); } Text parentText(WidgetTester tester, String move) { @@ -464,27 +455,27 @@ void main() { }); } -final gameData = LightArchivedGame( - id: const GameId('qVChCOTc'), - rated: false, - speed: Speed.blitz, - perf: Perf.blitz, - createdAt: DateTime.parse('2023-01-11 14:30:22.389'), - lastMoveAt: DateTime.parse('2023-01-11 14:33:56.416'), - status: GameStatus.mate, - white: const Player(aiLevel: 1), - black: const Player( - user: LightUser( - id: UserId('veloce'), - name: 'veloce', - isPatron: true, - ), - rating: 1435, - ), - variant: Variant.standard, - lastFen: '1r3rk1/p1pb1ppp/3p4/8/1nBN1P2/1P6/PBPP1nPP/R1K1q3 w - - 4 1', - winner: Side.black, -); +// final gameData = LightArchivedGame( +// id: const GameId('qVChCOTc'), +// rated: false, +// speed: Speed.blitz, +// perf: Perf.blitz, +// createdAt: DateTime.parse('2023-01-11 14:30:22.389'), +// lastMoveAt: DateTime.parse('2023-01-11 14:33:56.416'), +// status: GameStatus.mate, +// white: const Player(aiLevel: 1), +// black: const Player( +// user: LightUser( +// id: UserId('veloce'), +// name: 'veloce', +// isPatron: true, +// ), +// rating: 1435, +// ), +// variant: Variant.standard, +// lastFen: '1r3rk1/p1pb1ppp/3p4/8/1nBN1P2/1P6/PBPP1nPP/R1K1q3 w - - 4 1', +// winner: Side.black, +// ); const gameResponse = ''' {"id":"qVChCOTc","rated":false,"variant":"standard","speed":"blitz","perf":"blitz","createdAt":1673443822389,"lastMoveAt":1673444036416,"status":"mate","players":{"white":{"aiLevel":1},"black":{"user":{"name":"veloce","patron":true,"id":"veloce"},"rating":1435,"provisional":true}},"winner":"black","opening":{"eco":"C20","name":"King's Pawn Game: Wayward Queen Attack, Kiddie Countergambit","ply":4},"moves":"e4 e5 Qh5 Nf6 Qxe5+ Be7 b3 d6 Qb5+ Bd7 Qxb7 Nc6 Ba3 Rb8 Qa6 Nxe4 Bb2 O-O Nc3 Nb4 Nf3 Nxa6 Nd5 Nb4 Nxe7+ Qxe7 Nd4 Qf6 f4 Qe7 Ke2 Ng3+ Kd1 Nxh1 Bc4 Nf2+ Kc1 Qe1#","clocks":[18003,18003,17915,17627,17771,16691,17667,16243,17475,15459,17355,14779,17155,13795,16915,13267,14771,11955,14451,10995,14339,10203,13899,9099,12427,8379,12003,7547,11787,6691,11355,6091,11147,5763,10851,5099,10635,4657],"clock":{"initial":180,"increment":0,"totalTime":180}} diff --git a/test/view/opening_explorer/opening_explorer_screen_test.dart b/test/view/opening_explorer/opening_explorer_screen_test.dart index 5811d32f6f..be17a9ce9d 100644 --- a/test/view/opening_explorer/opening_explorer_screen_test.dart +++ b/test/view/opening_explorer/opening_explorer_screen_test.dart @@ -40,11 +40,12 @@ void main() { }); const options = AnalysisOptions( - pgn: '', - id: standaloneOpeningExplorerId, - isLocalEvaluationAllowed: false, - orientation: Side.white, - variant: Variant.standard, + standalone: ( + pgn: '', + isLocalEvaluationAllowed: false, + orientation: Side.white, + variant: Variant.standard, + ), ); const name = 'John'; @@ -65,14 +66,14 @@ void main() { (WidgetTester tester) async { final app = await makeTestProviderScopeApp( tester, - home: const OpeningExplorerScreen( - options: options, - ), + home: const OpeningExplorerScreen(options: options), overrides: [ defaultClientProvider.overrideWithValue(mockClient), ], ); await tester.pumpWidget(app); + expect(find.byType(CircularProgressIndicator), findsOneWidget); + await tester.pump(const Duration(milliseconds: 1)); // wait for opening explorer data to load (taking debounce delay into account) await tester.pump(const Duration(milliseconds: 350)); @@ -111,9 +112,7 @@ void main() { (WidgetTester tester) async { final app = await makeTestProviderScopeApp( tester, - home: const OpeningExplorerScreen( - options: options, - ), + home: const OpeningExplorerScreen(options: options), overrides: [ defaultClientProvider.overrideWithValue(mockClient), ], @@ -129,6 +128,8 @@ void main() { }, ); await tester.pumpWidget(app); + expect(find.byType(CircularProgressIndicator), findsOneWidget); + await tester.pump(const Duration(milliseconds: 1)); // wait for opening explorer data to load (taking debounce delay into account) await tester.pump(const Duration(milliseconds: 350)); @@ -163,9 +164,7 @@ void main() { (WidgetTester tester) async { final app = await makeTestProviderScopeApp( tester, - home: const OpeningExplorerScreen( - options: options, - ), + home: const OpeningExplorerScreen(options: options), overrides: [ defaultClientProvider.overrideWithValue(mockClient), ], @@ -182,6 +181,8 @@ void main() { }, ); await tester.pumpWidget(app); + expect(find.byType(CircularProgressIndicator), findsOneWidget); + await tester.pump(const Duration(milliseconds: 1)); // wait for opening explorer data to load (taking debounce delay into account) await tester.pump(const Duration(milliseconds: 350)); From c9da3f1d4ebc9dabb47df31824c71a017b17d0e6 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Thu, 21 Nov 2024 11:56:04 +0100 Subject: [PATCH 08/23] Don't show explorer if offline --- .../view/analysis/opening_explorer_view.dart | 316 ++++++++++-------- lib/src/view/engine/engine_gauge.dart | 2 +- lib/src/view/game/archived_game_screen.dart | 6 +- 3 files changed, 175 insertions(+), 149 deletions(-) diff --git a/lib/src/view/analysis/opening_explorer_view.dart b/lib/src/view/analysis/opening_explorer_view.dart index 5f82d006dc..c631778fda 100644 --- a/lib/src/view/analysis/opening_explorer_view.dart +++ b/lib/src/view/analysis/opening_explorer_view.dart @@ -5,9 +5,11 @@ import 'package:lichess_mobile/src/model/analysis/analysis_controller.dart'; import 'package:lichess_mobile/src/model/opening_explorer/opening_explorer.dart'; import 'package:lichess_mobile/src/model/opening_explorer/opening_explorer_preferences.dart'; import 'package:lichess_mobile/src/model/opening_explorer/opening_explorer_repository.dart'; +import 'package:lichess_mobile/src/network/connectivity.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/screen.dart'; import 'package:lichess_mobile/src/view/opening_explorer/opening_explorer_widgets.dart'; +import 'package:lichess_mobile/src/widgets/feedback.dart'; import 'package:lichess_mobile/src/widgets/shimmer.dart'; const _kTableRowVerticalPadding = 12.0; @@ -35,68 +37,159 @@ class _OpeningExplorerState extends ConsumerState { @override Widget build(BuildContext context) { - final analysisState = - ref.watch(analysisControllerProvider(widget.options)).requireValue; - - if (analysisState.position.ply >= 50) { - return _OpeningExplorerView( - isLoading: false, - children: [ - OpeningExplorerMoveTable.maxDepth( - options: widget.options, - ), - ], - ); - } - - final prefs = ref.watch(openingExplorerPreferencesProvider); - - if (prefs.db == OpeningDatabase.player && prefs.playerDb.username == null) { - return const _OpeningExplorerView( - isLoading: false, - children: [ - Padding( - padding: _kTableRowPadding, - child: Center( - // TODO: l10n - child: Text('Select a Lichess player in the settings.'), - ), - ), - ], - ); - } + final connectivity = ref.watch(connectivityChangesProvider); + return connectivity.whenIsLoading( + loading: () => const CenterLoadingIndicator(), + offline: () => const Center( + child: Padding( + padding: EdgeInsets.all(16.0), + // TODO l10n + child: Text('Opening explorer is not available offline.'), + ), + ), + online: () { + final analysisState = + ref.watch(analysisControllerProvider(widget.options)).requireValue; + + if (analysisState.position.ply >= 50) { + return _OpeningExplorerView( + isLoading: false, + children: [ + OpeningExplorerMoveTable.maxDepth( + options: widget.options, + ), + ], + ); + } + + final prefs = ref.watch(openingExplorerPreferencesProvider); + + if (prefs.db == OpeningDatabase.player && + prefs.playerDb.username == null) { + return const _OpeningExplorerView( + isLoading: false, + children: [ + Padding( + padding: _kTableRowPadding, + child: Center( + // TODO: l10n + child: Text('Select a Lichess player in the settings.'), + ), + ), + ], + ); + } + + final cacheKey = OpeningExplorerCacheKey( + fen: analysisState.position.fen, + prefs: prefs, + ); + final cacheOpeningExplorer = cache[cacheKey]; + final openingExplorerAsync = cacheOpeningExplorer != null + ? AsyncValue.data( + (entry: cacheOpeningExplorer, isIndexing: false), + ) + : ref.watch( + openingExplorerProvider(fen: analysisState.position.fen), + ); + + if (cacheOpeningExplorer == null) { + ref.listen(openingExplorerProvider(fen: analysisState.position.fen), + (_, curAsync) { + curAsync.whenData((cur) { + if (cur != null && !cur.isIndexing) { + cache[cacheKey] = cur.entry; + } + }); + }); + } + + final isLoading = openingExplorerAsync.isLoading || + openingExplorerAsync.value == null; + + return _OpeningExplorerView( + isLoading: isLoading, + children: openingExplorerAsync.when( + data: (openingExplorer) { + if (openingExplorer == null) { + return lastExplorerWidgets ?? + [ + Shimmer( + child: ShimmerLoading( + isLoading: true, + child: OpeningExplorerMoveTable.loading( + options: widget.options, + ), + ), + ), + ]; + } - final cacheKey = OpeningExplorerCacheKey( - fen: analysisState.position.fen, - prefs: prefs, - ); - final cacheOpeningExplorer = cache[cacheKey]; - final openingExplorerAsync = cacheOpeningExplorer != null - ? AsyncValue.data( - (entry: cacheOpeningExplorer, isIndexing: false), - ) - : ref.watch(openingExplorerProvider(fen: analysisState.position.fen)); - - if (cacheOpeningExplorer == null) { - ref.listen(openingExplorerProvider(fen: analysisState.position.fen), - (_, curAsync) { - curAsync.whenData((cur) { - if (cur != null && !cur.isIndexing) { - cache[cacheKey] = cur.entry; - } - }); - }); - } - - final isLoading = - openingExplorerAsync.isLoading || openingExplorerAsync.value == null; - - return _OpeningExplorerView( - isLoading: isLoading, - children: openingExplorerAsync.when( - data: (openingExplorer) { - if (openingExplorer == null) { - return lastExplorerWidgets ?? + final topGames = openingExplorer.entry.topGames; + final recentGames = openingExplorer.entry.recentGames; + + final ply = analysisState.position.ply; + + final children = [ + OpeningExplorerMoveTable( + moves: openingExplorer.entry.moves, + whiteWins: openingExplorer.entry.white, + draws: openingExplorer.entry.draws, + blackWins: openingExplorer.entry.black, + options: widget.options, + ), + if (topGames != null && topGames.isNotEmpty) ...[ + OpeningExplorerHeaderTile( + key: const Key('topGamesHeader'), + child: Text(context.l10n.topGames), + ), + ...List.generate( + topGames.length, + (int index) { + return OpeningExplorerGameTile( + key: Key('top-game-${topGames.get(index).id}'), + game: topGames.get(index), + color: index.isEven + ? Theme.of(context).colorScheme.surfaceContainerLow + : Theme.of(context) + .colorScheme + .surfaceContainerHigh, + ply: ply, + ); + }, + growable: false, + ), + ], + if (recentGames != null && recentGames.isNotEmpty) ...[ + OpeningExplorerHeaderTile( + key: const Key('recentGamesHeader'), + child: Text(context.l10n.recentGames), + ), + ...List.generate( + recentGames.length, + (int index) { + return OpeningExplorerGameTile( + key: Key('recent-game-${recentGames.get(index).id}'), + game: recentGames.get(index), + color: index.isEven + ? Theme.of(context).colorScheme.surfaceContainerLow + : Theme.of(context) + .colorScheme + .surfaceContainerHigh, + ply: ply, + ); + }, + growable: false, + ), + ], + ]; + + lastExplorerWidgets = children; + + return children; + }, + loading: () => + lastExplorerWidgets ?? [ Shimmer( child: ShimmerLoading( @@ -106,94 +199,23 @@ class _OpeningExplorerState extends ConsumerState { ), ), ), - ]; - } - - final topGames = openingExplorer.entry.topGames; - final recentGames = openingExplorer.entry.recentGames; - - final ply = analysisState.position.ply; - - final children = [ - OpeningExplorerMoveTable( - moves: openingExplorer.entry.moves, - whiteWins: openingExplorer.entry.white, - draws: openingExplorer.entry.draws, - blackWins: openingExplorer.entry.black, - options: widget.options, - ), - if (topGames != null && topGames.isNotEmpty) ...[ - OpeningExplorerHeaderTile( - key: const Key('topGamesHeader'), - child: Text(context.l10n.topGames), - ), - ...List.generate( - topGames.length, - (int index) { - return OpeningExplorerGameTile( - key: Key('top-game-${topGames.get(index).id}'), - game: topGames.get(index), - color: index.isEven - ? Theme.of(context).colorScheme.surfaceContainerLow - : Theme.of(context).colorScheme.surfaceContainerHigh, - ply: ply, - ); - }, - growable: false, - ), - ], - if (recentGames != null && recentGames.isNotEmpty) ...[ - OpeningExplorerHeaderTile( - key: const Key('recentGamesHeader'), - child: Text(context.l10n.recentGames), - ), - ...List.generate( - recentGames.length, - (int index) { - return OpeningExplorerGameTile( - key: Key('recent-game-${recentGames.get(index).id}'), - game: recentGames.get(index), - color: index.isEven - ? Theme.of(context).colorScheme.surfaceContainerLow - : Theme.of(context).colorScheme.surfaceContainerHigh, - ply: ply, - ); - }, - growable: false, - ), - ], - ]; - - lastExplorerWidgets = children; - - return children; - }, - loading: () => - lastExplorerWidgets ?? - [ - Shimmer( - child: ShimmerLoading( - isLoading: true, - child: OpeningExplorerMoveTable.loading( - options: widget.options, + ], + error: (e, s) { + debugPrint( + 'SEVERE: [OpeningExplorerView] could not load opening explorer data; $e\n$s', + ); + return [ + Center( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Text(e.toString()), ), ), - ), - ], - error: (e, s) { - debugPrint( - 'SEVERE: [OpeningExplorerView] could not load opening explorer data; $e\n$s', - ); - return [ - Center( - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Text(e.toString()), - ), - ), - ]; - }, - ), + ]; + }, + ), + ); + }, ); } } diff --git a/lib/src/view/engine/engine_gauge.dart b/lib/src/view/engine/engine_gauge.dart index d1ef7ae79b..0da958228c 100644 --- a/lib/src/view/engine/engine_gauge.dart +++ b/lib/src/view/engine/engine_gauge.dart @@ -9,7 +9,7 @@ import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; const double kEvalGaugeSize = 24.0; -const double kEvalGaugeFontSize = 10.0; +const double kEvalGaugeFontSize = 11.0; const Color _kEvalGaugeBackgroundColor = Color(0xFF444444); const Color _kEvalGaugeValueColorDarkBg = Color(0xEEEEEEEE); const Color _kEvalGaugeValueColorLightBg = Color(0xFFFFFFFF); diff --git a/lib/src/view/game/archived_game_screen.dart b/lib/src/view/game/archived_game_screen.dart index b6abcc72da..ec5226fe83 100644 --- a/lib/src/view/game/archived_game_screen.dart +++ b/lib/src/view/game/archived_game_screen.dart @@ -375,10 +375,14 @@ class _BottomBar extends ConsumerWidget { label: context.l10n.gameAnalysis, onTap: ref.read(gameCursorProvider(gameData.id)).hasValue ? () { + final cursor = gameCursor.requireValue.$2; pushPlatformRoute( context, builder: (context) => AnalysisScreen( - options: AnalysisOptions(gameId: gameData.id), + options: AnalysisOptions( + gameId: gameData.id, + initialMoveCursor: cursor, + ), ), ); } From cee722276ffed3f678d5e921685c9d5c1f2e0ee1 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Thu, 21 Nov 2024 14:59:49 +0100 Subject: [PATCH 09/23] Only show game summary tab when necessary --- .../model/analysis/analysis_controller.dart | 2 + lib/src/view/analysis/analysis_screen.dart | 53 ++++++++++++++----- 2 files changed, 43 insertions(+), 12 deletions(-) diff --git a/lib/src/model/analysis/analysis_controller.dart b/lib/src/model/analysis/analysis_controller.dart index 645a922ef9..fef06e292b 100644 --- a/lib/src/model/analysis/analysis_controller.dart +++ b/lib/src/model/analysis/analysis_controller.dart @@ -759,6 +759,8 @@ class AnalysisState with _$AnalysisState { bool get hasServerAnalysis => playersAnalysis != null; + bool get canShowGameSummary => hasServerAnalysis || canRequestServerAnalysis; + /// Whether an evaluation can be available bool get hasAvailableEval => isEngineAvailable || diff --git a/lib/src/view/analysis/analysis_screen.dart b/lib/src/view/analysis/analysis_screen.dart index cbe72e7d3a..f87067227e 100644 --- a/lib/src/view/analysis/analysis_screen.dart +++ b/lib/src/view/analysis/analysis_screen.dart @@ -47,9 +47,9 @@ class AnalysisScreen extends ConsumerStatefulWidget { } class _AnalysisScreenState extends ConsumerState - with SingleTickerProviderStateMixin { - late final TabController _tabController; - late final List tabs; + with TickerProviderStateMixin { + late List tabs; + late TabController _tabController; @override void initState() { @@ -57,7 +57,6 @@ class _AnalysisScreenState extends ConsumerState tabs = [ AnalysisTab.opening, AnalysisTab.moves, - if (widget.options.isLichessGameAnalysis) AnalysisTab.summary, ]; _tabController = TabController( @@ -65,6 +64,28 @@ class _AnalysisScreenState extends ConsumerState initialIndex: 1, length: tabs.length, ); + + ref.listenManual>( + analysisControllerProvider(widget.options), + (_, state) { + if (state.valueOrNull?.canShowGameSummary == true) { + setState(() { + tabs = [ + AnalysisTab.opening, + AnalysisTab.moves, + AnalysisTab.summary, + ]; + final index = _tabController.index; + _tabController.dispose(); + _tabController = TabController( + vsync: this, + initialIndex: index, + length: tabs.length, + ); + }); + } + }, + ); } @override @@ -76,8 +97,8 @@ class _AnalysisScreenState extends ConsumerState @override Widget build(BuildContext context) { final ctrlProvider = analysisControllerProvider(widget.options); - final asyncState = ref.watch(ctrlProvider); + final asyncState = ref.watch(ctrlProvider); final appBarActions = [ EngineDepth(defaultEval: asyncState.valueOrNull?.currentNode.eval), AppBarAnalysisTabIndicator( @@ -106,8 +127,9 @@ class _AnalysisScreenState extends ConsumerState actions: appBarActions, ), body: _Body( - controller: _tabController, options: widget.options, + controller: _tabController, + tabs: tabs, enableDrawingShapes: widget.enableDrawingShapes, ), ); @@ -154,12 +176,14 @@ class _Title extends StatelessWidget { class _Body extends ConsumerWidget { const _Body({ - required this.controller, required this.options, + required this.controller, + required this.tabs, required this.enableDrawingShapes, }); final TabController controller; + final List tabs; final AnalysisOptions options; final bool enableDrawingShapes; @@ -211,11 +235,16 @@ class _Body extends ConsumerWidget { ) : null, bottomBar: _BottomBar(options: options), - children: [ - OpeningExplorerView(options: options), - AnalysisTreeView(options), - if (options.isLichessGameAnalysis) ServerAnalysisSummary(options), - ], + children: tabs.map((tab) { + switch (tab) { + case AnalysisTab.opening: + return OpeningExplorerView(options: options); + case AnalysisTab.moves: + return AnalysisTreeView(options); + case AnalysisTab.summary: + return ServerAnalysisSummary(options); + } + }).toList(), ); } } From 1b593536eb2a95576d9ead309b22ba9b03b5408a Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Thu, 21 Nov 2024 15:09:58 +0100 Subject: [PATCH 10/23] Hide settings if evaluation is not allowed --- lib/src/view/analysis/analysis_screen.dart | 3 + lib/src/view/analysis/analysis_settings.dart | 144 +++++++++---------- 2 files changed, 75 insertions(+), 72 deletions(-) diff --git a/lib/src/view/analysis/analysis_screen.dart b/lib/src/view/analysis/analysis_screen.dart index f87067227e..476645fca7 100644 --- a/lib/src/view/analysis/analysis_screen.dart +++ b/lib/src/view/analysis/analysis_screen.dart @@ -111,6 +111,9 @@ class _AnalysisScreenState extends ConsumerState isScrollControlled: true, showDragHandle: true, isDismissible: true, + constraints: BoxConstraints( + minHeight: MediaQuery.sizeOf(context).height * 0.5, + ), builder: (_) => AnalysisSettings(widget.options), ), semanticsLabel: context.l10n.settingsSettings, diff --git a/lib/src/view/analysis/analysis_settings.dart b/lib/src/view/analysis/analysis_settings.dart index f7101bd8c4..0de707426f 100644 --- a/lib/src/view/analysis/analysis_settings.dart +++ b/lib/src/view/analysis/analysis_settings.dart @@ -37,48 +37,18 @@ class AnalysisSettings extends ConsumerWidget { ), trailing: const Icon(CupertinoIcons.chevron_right), ), - SwitchSettingTile( - title: Text(context.l10n.toggleLocalEvaluation), - value: prefs.enableLocalEvaluation, - onChanged: value.isLocalEvaluationAllowed - ? (_) { - ref.read(ctrlProvider.notifier).toggleLocalEvaluation(); - } - : null, - ), - PlatformListTile( - title: Text.rich( - TextSpan( - text: '${context.l10n.multipleLines}: ', - style: const TextStyle( - fontWeight: FontWeight.normal, - ), - children: [ - TextSpan( - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 18, - ), - text: prefs.numEvalLines.toString(), - ), - ], - ), + if (value.isLocalEvaluationAllowed) ...[ + SwitchSettingTile( + title: Text(context.l10n.toggleLocalEvaluation), + value: prefs.enableLocalEvaluation, + onChanged: (_) { + ref.read(ctrlProvider.notifier).toggleLocalEvaluation(); + }, ), - subtitle: NonLinearSlider( - value: prefs.numEvalLines, - values: const [0, 1, 2, 3], - onChangeEnd: value.isEngineAvailable - ? (value) => ref - .read(ctrlProvider.notifier) - .setNumEvalLines(value.toInt()) - : null, - ), - ), - if (maxEngineCores > 1) PlatformListTile( title: Text.rich( TextSpan( - text: '${context.l10n.cpus}: ', + text: '${context.l10n.multipleLines}: ', style: const TextStyle( fontWeight: FontWeight.normal, ), @@ -88,51 +58,81 @@ class AnalysisSettings extends ConsumerWidget { fontWeight: FontWeight.bold, fontSize: 18, ), - text: prefs.numEngineCores.toString(), + text: prefs.numEvalLines.toString(), ), ], ), ), subtitle: NonLinearSlider( - value: prefs.numEngineCores, - values: List.generate(maxEngineCores, (index) => index + 1), + value: prefs.numEvalLines, + values: const [0, 1, 2, 3], onChangeEnd: value.isEngineAvailable ? (value) => ref .read(ctrlProvider.notifier) - .setEngineCores(value.toInt()) + .setNumEvalLines(value.toInt()) : null, ), ), - SwitchSettingTile( - title: Text(context.l10n.bestMoveArrow), - value: prefs.showBestMoveArrow, - onChanged: value.isEngineAvailable - ? (value) => ref - .read(analysisPreferencesProvider.notifier) - .toggleShowBestMoveArrow() - : null, - ), - SwitchSettingTile( - title: Text(context.l10n.evaluationGauge), - value: prefs.showEvaluationGauge, - onChanged: (value) => ref - .read(analysisPreferencesProvider.notifier) - .toggleShowEvaluationGauge(), - ), - SwitchSettingTile( - title: Text(context.l10n.toggleGlyphAnnotations), - value: prefs.showAnnotations, - onChanged: (_) => ref - .read(analysisPreferencesProvider.notifier) - .toggleAnnotations(), - ), - SwitchSettingTile( - title: Text(context.l10n.mobileShowComments), - value: prefs.showPgnComments, - onChanged: (_) => ref - .read(analysisPreferencesProvider.notifier) - .togglePgnComments(), - ), + if (maxEngineCores > 1) + PlatformListTile( + title: Text.rich( + TextSpan( + text: '${context.l10n.cpus}: ', + style: const TextStyle( + fontWeight: FontWeight.normal, + ), + children: [ + TextSpan( + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 18, + ), + text: prefs.numEngineCores.toString(), + ), + ], + ), + ), + subtitle: NonLinearSlider( + value: prefs.numEngineCores, + values: List.generate(maxEngineCores, (index) => index + 1), + onChangeEnd: value.isEngineAvailable + ? (value) => ref + .read(ctrlProvider.notifier) + .setEngineCores(value.toInt()) + : null, + ), + ), + SwitchSettingTile( + title: Text(context.l10n.bestMoveArrow), + value: prefs.showBestMoveArrow, + onChanged: value.isEngineAvailable + ? (value) => ref + .read(analysisPreferencesProvider.notifier) + .toggleShowBestMoveArrow() + : null, + ), + SwitchSettingTile( + title: Text(context.l10n.evaluationGauge), + value: prefs.showEvaluationGauge, + onChanged: (value) => ref + .read(analysisPreferencesProvider.notifier) + .toggleShowEvaluationGauge(), + ), + SwitchSettingTile( + title: Text(context.l10n.toggleGlyphAnnotations), + value: prefs.showAnnotations, + onChanged: (_) => ref + .read(analysisPreferencesProvider.notifier) + .toggleAnnotations(), + ), + SwitchSettingTile( + title: Text(context.l10n.mobileShowComments), + value: prefs.showPgnComments, + onChanged: (_) => ref + .read(analysisPreferencesProvider.notifier) + .togglePgnComments(), + ), + ], ], ); case AsyncError(:final error): From 2a6ff82f1953acbd33d20cdd4e90b60470c73fd1 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Thu, 21 Nov 2024 16:08:04 +0100 Subject: [PATCH 11/23] WIP on computer analysis toggle --- .../model/analysis/analysis_controller.dart | 52 ++++-- .../model/analysis/analysis_preferences.dart | 10 + lib/src/model/study/study_controller.dart | 8 +- lib/src/view/analysis/analysis_settings.dart | 173 ++++++++++-------- .../board_editor/board_editor_screen.dart | 2 +- .../offline_correspondence_game_screen.dart | 2 +- lib/src/view/game/game_result_dialog.dart | 2 +- lib/src/view/puzzle/puzzle_screen.dart | 2 +- lib/src/view/puzzle/streak_screen.dart | 2 +- lib/src/view/study/study_bottom_bar.dart | 2 +- lib/src/view/study/study_settings.dart | 6 +- lib/src/view/tools/load_position_screen.dart | 4 +- lib/src/view/tools/tools_tab_screen.dart | 4 +- lib/src/widgets/pgn.dart | 8 +- test/view/analysis/analysis_screen_test.dart | 6 +- .../opening_explorer_screen_test.dart | 2 +- 16 files changed, 173 insertions(+), 112 deletions(-) diff --git a/lib/src/model/analysis/analysis_controller.dart b/lib/src/model/analysis/analysis_controller.dart index fef06e292b..10463ea55e 100644 --- a/lib/src/model/analysis/analysis_controller.dart +++ b/lib/src/model/analysis/analysis_controller.dart @@ -33,7 +33,7 @@ typedef StandaloneAnalysis = ({ String pgn, Variant variant, Side orientation, - bool isLocalEvaluationAllowed, + bool isComputerAnalysisAllowed, }); @freezed @@ -112,9 +112,9 @@ class AnalysisController extends _$AnalysisController final pgnHeaders = IMap(game.headers); final rootComments = IList(game.comments.map((c) => PgnComment.fromPgn(c))); - final isLocalEvaluationAllowed = options.isLichessGameAnalysis + final isComputerAnalysisAllowed = options.isLichessGameAnalysis ? pgnHeaders['Result'] != '*' - : options.standalone!.isLocalEvaluationAllowed; + : options.standalone!.isComputerAnalysisAllowed; _root = Root.fromPgnGame( game, @@ -166,7 +166,8 @@ class AnalysisController extends _$AnalysisController lastMove: lastMove, pov: _orientation, contextOpening: opening, - isLocalEvaluationAllowed: isLocalEvaluationAllowed, + isComputerAnalysisAllowed: isComputerAnalysisAllowed, + isComputerAnalysisEnabled: prefs.enableComputerAnalysis, isLocalEvaluationEnabled: prefs.enableLocalEvaluation, playersAnalysis: serverAnalysis, acplChartData: serverAnalysis != null ? _makeAcplChartData() : null, @@ -325,15 +326,28 @@ class AnalysisController extends _$AnalysisController _setPath(path.penultimate, shouldRecomputeRootView: true); } + /// Toggles the computer analysis on/off. + /// + /// Acts both on local evaluation and server analysis. + Future toggleComputerAnalysis() async { + await ref + .read(analysisPreferencesProvider.notifier) + .toggleEnableComputerAnalysis(); + + await toggleLocalEvaluation(); + } + + /// Toggles the local evaluation on/off. Future toggleLocalEvaluation() async { - ref + await ref .read(analysisPreferencesProvider.notifier) .toggleEnableLocalEvaluation(); - final curState = state.requireValue; + final prefs = ref.read(analysisPreferencesProvider); state = AsyncData( - curState.copyWith( - isLocalEvaluationEnabled: !curState.isLocalEvaluationEnabled, + state.requireValue.copyWith( + isLocalEvaluationEnabled: prefs.enableLocalEvaluation, + isComputerAnalysisEnabled: prefs.enableComputerAnalysis, ), ); @@ -707,10 +721,19 @@ class AnalysisState with _$AnalysisState { /// The side to display the board from. required Side pov, - /// Whether local evaluation is allowed for this analysis. - required bool isLocalEvaluationAllowed, + /// Whether computer evaluation is allowed for this analysis. + /// + /// Acts on both local and server analysis. + required bool isComputerAnalysisAllowed, + + /// Whether the user has enabled computer analysis. + /// + /// This is a user preference and acts both on local and server analysis. + required bool isComputerAnalysisEnabled, /// Whether the user has enabled local evaluation. + /// + /// This is a user preference and acts only on local analysis. required bool isLocalEvaluationEnabled, /// The last move played. @@ -757,6 +780,7 @@ class AnalysisState with _$AnalysisState { bool get canRequestServerAnalysis => gameId != null && !hasServerAnalysis && pgnHeaders['Result'] != '*'; + /// Whether the server analysis is available. bool get hasServerAnalysis => playersAnalysis != null; bool get canShowGameSummary => hasServerAnalysis || canRequestServerAnalysis; @@ -764,13 +788,17 @@ class AnalysisState with _$AnalysisState { /// Whether an evaluation can be available bool get hasAvailableEval => isEngineAvailable || - (isLocalEvaluationAllowed && + (isComputerAnalysisEnabledAndAllowed && acplChartData != null && acplChartData!.isNotEmpty); + bool get isComputerAnalysisEnabledAndAllowed => + isComputerAnalysisEnabled && isComputerAnalysisAllowed; + /// Whether the engine is allowed for this analysis and variant. bool get isEngineAllowed => - isLocalEvaluationAllowed && engineSupportedVariants.contains(variant); + isComputerAnalysisEnabledAndAllowed && + engineSupportedVariants.contains(variant); /// Whether the engine is available for evaluation bool get isEngineAvailable => isEngineAllowed && isLocalEvaluationEnabled; diff --git a/lib/src/model/analysis/analysis_preferences.dart b/lib/src/model/analysis/analysis_preferences.dart index 5633e3db30..30615646d3 100644 --- a/lib/src/model/analysis/analysis_preferences.dart +++ b/lib/src/model/analysis/analysis_preferences.dart @@ -26,6 +26,14 @@ class AnalysisPreferences extends _$AnalysisPreferences return fetch(); } + Future toggleEnableComputerAnalysis() { + return save( + state.copyWith( + enableComputerAnalysis: !state.enableComputerAnalysis, + ), + ); + } + Future toggleEnableLocalEvaluation() { return save( state.copyWith( @@ -90,6 +98,7 @@ class AnalysisPrefs with _$AnalysisPrefs implements Serializable { const AnalysisPrefs._(); const factory AnalysisPrefs({ + @JsonKey(defaultValue: true) required bool enableComputerAnalysis, required bool enableLocalEvaluation, required bool showEvaluationGauge, required bool showBestMoveArrow, @@ -101,6 +110,7 @@ class AnalysisPrefs with _$AnalysisPrefs implements Serializable { }) = _AnalysisPrefs; static const defaults = AnalysisPrefs( + enableComputerAnalysis: true, enableLocalEvaluation: true, showEvaluationGauge: true, showBestMoveArrow: true, diff --git a/lib/src/model/study/study_controller.dart b/lib/src/model/study/study_controller.dart index 1337dff19c..459751d81b 100644 --- a/lib/src/model/study/study_controller.dart +++ b/lib/src/model/study/study_controller.dart @@ -97,7 +97,7 @@ class StudyController extends _$StudyController implements PgnTreeNotifier { currentNode: StudyCurrentNode.illegalPosition(), pgnRootComments: rootComments, pov: orientation, - isLocalEvaluationAllowed: false, + isComputerAnalysisAllowed: false, isLocalEvaluationEnabled: false, gamebookActive: false, pgn: pgn, @@ -121,7 +121,7 @@ class StudyController extends _$StudyController implements PgnTreeNotifier { pgnRootComments: rootComments, lastMove: lastMove, pov: orientation, - isLocalEvaluationAllowed: + isComputerAnalysisAllowed: study.chapter.features.computer && !study.chapter.gamebook, isLocalEvaluationEnabled: prefs.enableLocalEvaluation, gamebookActive: study.chapter.gamebook, @@ -559,7 +559,7 @@ class StudyState with _$StudyState { required Side pov, /// Whether local evaluation is allowed for this study. - required bool isLocalEvaluationAllowed, + required bool isComputerAnalysisAllowed, /// Whether we're currently in gamebook mode, where the user has to find the right moves. required bool gamebookActive, @@ -583,7 +583,7 @@ class StudyState with _$StudyState { /// Whether the engine is available for evaluation bool get isEngineAvailable => - isLocalEvaluationAllowed && + isComputerAnalysisAllowed && engineSupportedVariants.contains(variant) && isLocalEvaluationEnabled; diff --git a/lib/src/view/analysis/analysis_settings.dart b/lib/src/view/analysis/analysis_settings.dart index 0de707426f..9b03d4c943 100644 --- a/lib/src/view/analysis/analysis_settings.dart +++ b/lib/src/view/analysis/analysis_settings.dart @@ -37,102 +37,121 @@ class AnalysisSettings extends ConsumerWidget { ), trailing: const Icon(CupertinoIcons.chevron_right), ), - if (value.isLocalEvaluationAllowed) ...[ + if (value.isComputerAnalysisAllowed) SwitchSettingTile( - title: Text(context.l10n.toggleLocalEvaluation), - value: prefs.enableLocalEvaluation, + title: Text(context.l10n.computerAnalysis), + value: prefs.enableComputerAnalysis, onChanged: (_) { - ref.read(ctrlProvider.notifier).toggleLocalEvaluation(); + ref.read(ctrlProvider.notifier).toggleComputerAnalysis(); }, ), - PlatformListTile( - title: Text.rich( - TextSpan( - text: '${context.l10n.multipleLines}: ', - style: const TextStyle( - fontWeight: FontWeight.normal, - ), - children: [ + AnimatedCrossFade( + duration: const Duration(milliseconds: 300), + crossFadeState: value.isComputerAnalysisEnabledAndAllowed + ? CrossFadeState.showSecond + : CrossFadeState.showFirst, + firstChild: const SizedBox.shrink(), + secondChild: Column( + mainAxisSize: MainAxisSize.min, + children: [ + SwitchSettingTile( + title: Text(context.l10n.toggleLocalEvaluation), + value: prefs.enableLocalEvaluation, + onChanged: (_) { + ref.read(ctrlProvider.notifier).toggleLocalEvaluation(); + }, + ), + PlatformListTile( + title: Text.rich( TextSpan( + text: '${context.l10n.multipleLines}: ', style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 18, + fontWeight: FontWeight.normal, ), - text: prefs.numEvalLines.toString(), + children: [ + TextSpan( + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 18, + ), + text: prefs.numEvalLines.toString(), + ), + ], ), - ], + ), + subtitle: NonLinearSlider( + value: prefs.numEvalLines, + values: const [0, 1, 2, 3], + onChangeEnd: value.isEngineAvailable + ? (value) => ref + .read(ctrlProvider.notifier) + .setNumEvalLines(value.toInt()) + : null, + ), ), - ), - subtitle: NonLinearSlider( - value: prefs.numEvalLines, - values: const [0, 1, 2, 3], - onChangeEnd: value.isEngineAvailable - ? (value) => ref - .read(ctrlProvider.notifier) - .setNumEvalLines(value.toInt()) - : null, - ), - ), - if (maxEngineCores > 1) - PlatformListTile( - title: Text.rich( - TextSpan( - text: '${context.l10n.cpus}: ', - style: const TextStyle( - fontWeight: FontWeight.normal, - ), - children: [ + if (maxEngineCores > 1) + PlatformListTile( + title: Text.rich( TextSpan( + text: '${context.l10n.cpus}: ', style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 18, + fontWeight: FontWeight.normal, ), - text: prefs.numEngineCores.toString(), + children: [ + TextSpan( + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 18, + ), + text: prefs.numEngineCores.toString(), + ), + ], ), - ], + ), + subtitle: NonLinearSlider( + value: prefs.numEngineCores, + values: + List.generate(maxEngineCores, (index) => index + 1), + onChangeEnd: value.isEngineAvailable + ? (value) => ref + .read(ctrlProvider.notifier) + .setEngineCores(value.toInt()) + : null, + ), ), - ), - subtitle: NonLinearSlider( - value: prefs.numEngineCores, - values: List.generate(maxEngineCores, (index) => index + 1), - onChangeEnd: value.isEngineAvailable + SwitchSettingTile( + title: Text(context.l10n.bestMoveArrow), + value: prefs.showBestMoveArrow, + onChanged: value.isEngineAvailable ? (value) => ref - .read(ctrlProvider.notifier) - .setEngineCores(value.toInt()) + .read(analysisPreferencesProvider.notifier) + .toggleShowBestMoveArrow() : null, ), - ), - SwitchSettingTile( - title: Text(context.l10n.bestMoveArrow), - value: prefs.showBestMoveArrow, - onChanged: value.isEngineAvailable - ? (value) => ref + SwitchSettingTile( + title: Text(context.l10n.evaluationGauge), + value: prefs.showEvaluationGauge, + onChanged: (value) => ref .read(analysisPreferencesProvider.notifier) - .toggleShowBestMoveArrow() - : null, - ), - SwitchSettingTile( - title: Text(context.l10n.evaluationGauge), - value: prefs.showEvaluationGauge, - onChanged: (value) => ref - .read(analysisPreferencesProvider.notifier) - .toggleShowEvaluationGauge(), - ), - SwitchSettingTile( - title: Text(context.l10n.toggleGlyphAnnotations), - value: prefs.showAnnotations, - onChanged: (_) => ref - .read(analysisPreferencesProvider.notifier) - .toggleAnnotations(), - ), - SwitchSettingTile( - title: Text(context.l10n.mobileShowComments), - value: prefs.showPgnComments, - onChanged: (_) => ref - .read(analysisPreferencesProvider.notifier) - .togglePgnComments(), + .toggleShowEvaluationGauge(), + ), + SwitchSettingTile( + title: Text(context.l10n.toggleGlyphAnnotations), + value: prefs.showAnnotations, + onChanged: (_) => ref + .read(analysisPreferencesProvider.notifier) + .toggleAnnotations(), + ), + SwitchSettingTile( + title: Text(context.l10n.mobileShowComments), + value: prefs.showPgnComments, + onChanged: (_) => ref + .read(analysisPreferencesProvider.notifier) + .togglePgnComments(), + ), + ], ), - ], + ), ], ); case AsyncError(:final error): diff --git a/lib/src/view/board_editor/board_editor_screen.dart b/lib/src/view/board_editor/board_editor_screen.dart index a4269e7873..45ee49e2b1 100644 --- a/lib/src/view/board_editor/board_editor_screen.dart +++ b/lib/src/view/board_editor/board_editor_screen.dart @@ -338,7 +338,7 @@ class _BottomBar extends ConsumerWidget { options: AnalysisOptions( standalone: ( pgn: editorState.pgn!, - isLocalEvaluationAllowed: true, + isComputerAnalysisAllowed: true, variant: Variant.fromPosition, orientation: editorState.orientation, ), diff --git a/lib/src/view/correspondence/offline_correspondence_game_screen.dart b/lib/src/view/correspondence/offline_correspondence_game_screen.dart index 45d3ccd843..931ad9e5d5 100644 --- a/lib/src/view/correspondence/offline_correspondence_game_screen.dart +++ b/lib/src/view/correspondence/offline_correspondence_game_screen.dart @@ -262,7 +262,7 @@ class _BodyState extends ConsumerState<_Body> { options: AnalysisOptions( standalone: ( pgn: game.makePgn(), - isLocalEvaluationAllowed: false, + isComputerAnalysisAllowed: false, variant: game.variant, orientation: game.youAre, ), diff --git a/lib/src/view/game/game_result_dialog.dart b/lib/src/view/game/game_result_dialog.dart index 4e09a5cf9d..e4ebf84b08 100644 --- a/lib/src/view/game/game_result_dialog.dart +++ b/lib/src/view/game/game_result_dialog.dart @@ -263,7 +263,7 @@ class OverTheBoardGameResultDialog extends StatelessWidget { options: AnalysisOptions( standalone: ( pgn: game.makePgn(), - isLocalEvaluationAllowed: true, + isComputerAnalysisAllowed: true, variant: game.meta.variant, orientation: Side.white, ), diff --git a/lib/src/view/puzzle/puzzle_screen.dart b/lib/src/view/puzzle/puzzle_screen.dart index 3f7aadeb45..0885219d38 100644 --- a/lib/src/view/puzzle/puzzle_screen.dart +++ b/lib/src/view/puzzle/puzzle_screen.dart @@ -487,7 +487,7 @@ class _BottomBar extends ConsumerWidget { options: AnalysisOptions( standalone: ( pgn: ref.read(ctrlProvider.notifier).makePgn(), - isLocalEvaluationAllowed: true, + isComputerAnalysisAllowed: true, variant: Variant.standard, orientation: puzzleState.pov, ), diff --git a/lib/src/view/puzzle/streak_screen.dart b/lib/src/view/puzzle/streak_screen.dart index 06fc77e366..cd38aa97fb 100644 --- a/lib/src/view/puzzle/streak_screen.dart +++ b/lib/src/view/puzzle/streak_screen.dart @@ -294,7 +294,7 @@ class _BottomBar extends ConsumerWidget { options: AnalysisOptions( standalone: ( pgn: ref.read(ctrlProvider.notifier).makePgn(), - isLocalEvaluationAllowed: true, + isComputerAnalysisAllowed: true, variant: Variant.standard, orientation: puzzleState.pov, ), diff --git a/lib/src/view/study/study_bottom_bar.dart b/lib/src/view/study/study_bottom_bar.dart index 17f19f2f3d..104f5c29fd 100644 --- a/lib/src/view/study/study_bottom_bar.dart +++ b/lib/src/view/study/study_bottom_bar.dart @@ -168,7 +168,7 @@ class _GamebookBottomBar extends ConsumerWidget { options: AnalysisOptions( standalone: ( pgn: state.pgn, - isLocalEvaluationAllowed: true, + isComputerAnalysisAllowed: true, variant: state.variant, orientation: state.pov, ), diff --git a/lib/src/view/study/study_settings.dart b/lib/src/view/study/study_settings.dart index 2b1527de6a..ea8393242c 100644 --- a/lib/src/view/study/study_settings.dart +++ b/lib/src/view/study/study_settings.dart @@ -22,8 +22,8 @@ class StudySettings extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final studyController = studyControllerProvider(id); - final isLocalEvaluationAllowed = ref.watch( - studyController.select((s) => s.requireValue.isLocalEvaluationAllowed), + final isComputerAnalysisAllowed = ref.watch( + studyController.select((s) => s.requireValue.isComputerAnalysisAllowed), ); final isEngineAvailable = ref.watch( studyController.select((s) => s.requireValue.isEngineAvailable), @@ -40,7 +40,7 @@ class StudySettings extends ConsumerWidget { SwitchSettingTile( title: Text(context.l10n.toggleLocalEvaluation), value: analysisPrefs.enableLocalEvaluation, - onChanged: isLocalEvaluationAllowed + onChanged: isComputerAnalysisAllowed ? (_) { ref.read(studyController.notifier).toggleLocalEvaluation(); } diff --git a/lib/src/view/tools/load_position_screen.dart b/lib/src/view/tools/load_position_screen.dart index 6f92ea14f4..e7655899c7 100644 --- a/lib/src/view/tools/load_position_screen.dart +++ b/lib/src/view/tools/load_position_screen.dart @@ -133,7 +133,7 @@ class _BodyState extends State<_Body> { options: AnalysisOptions( standalone: ( pgn: '[FEN "${pos.fen}"]', - isLocalEvaluationAllowed: true, + isComputerAnalysisAllowed: true, variant: Variant.standard, orientation: Side.white, ), @@ -165,7 +165,7 @@ class _BodyState extends State<_Body> { options: AnalysisOptions( standalone: ( pgn: textInput!, - isLocalEvaluationAllowed: true, + isComputerAnalysisAllowed: true, variant: rule != null ? Variant.fromRule(rule) : Variant.standard, orientation: Side.white, ), diff --git a/lib/src/view/tools/tools_tab_screen.dart b/lib/src/view/tools/tools_tab_screen.dart index b602c80f26..3834d28098 100644 --- a/lib/src/view/tools/tools_tab_screen.dart +++ b/lib/src/view/tools/tools_tab_screen.dart @@ -151,7 +151,7 @@ class _Body extends ConsumerWidget { options: AnalysisOptions( standalone: ( pgn: '', - isLocalEvaluationAllowed: true, + isComputerAnalysisAllowed: true, variant: Variant.standard, orientation: Side.white, ), @@ -170,7 +170,7 @@ class _Body extends ConsumerWidget { options: AnalysisOptions( standalone: ( pgn: '', - isLocalEvaluationAllowed: false, + isComputerAnalysisAllowed: false, variant: Variant.standard, orientation: Side.white, ), diff --git a/lib/src/widgets/pgn.dart b/lib/src/widgets/pgn.dart index f480db8883..fb2a2a6c85 100644 --- a/lib/src/widgets/pgn.dart +++ b/lib/src/widgets/pgn.dart @@ -196,11 +196,15 @@ class _DebouncedPgnTreeViewState extends ConsumerState { @override Widget build(BuildContext context) { final shouldShowComments = ref.watch( - analysisPreferencesProvider.select((value) => value.showPgnComments), + analysisPreferencesProvider.select( + (value) => value.enableComputerAnalysis && value.showPgnComments, + ), ); final shouldShowAnnotations = ref.watch( - analysisPreferencesProvider.select((value) => value.showAnnotations), + analysisPreferencesProvider.select( + (value) => value.enableComputerAnalysis && value.showAnnotations, + ), ); return _PgnTreeView( diff --git a/test/view/analysis/analysis_screen_test.dart b/test/view/analysis/analysis_screen_test.dart index fcc4f46738..44bf19bc87 100644 --- a/test/view/analysis/analysis_screen_test.dart +++ b/test/view/analysis/analysis_screen_test.dart @@ -26,7 +26,7 @@ void main() { options: AnalysisOptions( standalone: ( pgn: sanMoves, - isLocalEvaluationAllowed: false, + isComputerAnalysisAllowed: false, orientation: Side.white, variant: Variant.standard, ), @@ -62,7 +62,7 @@ void main() { options: AnalysisOptions( standalone: ( pgn: sanMoves, - isLocalEvaluationAllowed: false, + isComputerAnalysisAllowed: false, variant: Variant.standard, orientation: Side.white, ), @@ -129,7 +129,7 @@ void main() { options: AnalysisOptions( standalone: ( pgn: pgn, - isLocalEvaluationAllowed: false, + isComputerAnalysisAllowed: false, variant: Variant.standard, orientation: Side.white, ), diff --git a/test/view/opening_explorer/opening_explorer_screen_test.dart b/test/view/opening_explorer/opening_explorer_screen_test.dart index be17a9ce9d..10a3b08a72 100644 --- a/test/view/opening_explorer/opening_explorer_screen_test.dart +++ b/test/view/opening_explorer/opening_explorer_screen_test.dart @@ -42,7 +42,7 @@ void main() { const options = AnalysisOptions( standalone: ( pgn: '', - isLocalEvaluationAllowed: false, + isComputerAnalysisAllowed: false, orientation: Side.white, variant: Variant.standard, ), From 36958f0dd31852486ab1d32ed6add6745f44c0b3 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Thu, 21 Nov 2024 19:34:39 +0100 Subject: [PATCH 12/23] Don't show computer variations if computer disabled --- .../model/analysis/analysis_controller.dart | 30 +++-- lib/src/model/common/node.dart | 19 +++- lib/src/model/study/study_controller.dart | 10 +- lib/src/view/analysis/analysis_screen.dart | 8 +- lib/src/widgets/pgn.dart | 104 +++++++++++++----- 5 files changed, 119 insertions(+), 52 deletions(-) diff --git a/lib/src/model/analysis/analysis_controller.dart b/lib/src/model/analysis/analysis_controller.dart index 10463ea55e..12600de25d 100644 --- a/lib/src/model/analysis/analysis_controller.dart +++ b/lib/src/model/analysis/analysis_controller.dart @@ -289,9 +289,9 @@ class AnalysisController extends _$AnalysisController _root.isOnMainline(path) ? node.children.skip(1) : node.children; for (final child in childrenToShow) { - child.isHidden = false; + child.isCollapsed = false; for (final grandChild in child.children) { - grandChild.isHidden = false; + grandChild.isCollapsed = false; } } state = AsyncData(state.requireValue.copyWith(root: _root.view)); @@ -302,7 +302,7 @@ class AnalysisController extends _$AnalysisController final node = _root.nodeAt(path); for (final child in node.children) { - child.isHidden = true; + child.isCollapsed = true; } state = AsyncData(state.requireValue.copyWith(root: _root.view)); @@ -334,7 +334,19 @@ class AnalysisController extends _$AnalysisController .read(analysisPreferencesProvider.notifier) .toggleEnableComputerAnalysis(); - await toggleLocalEvaluation(); + final curState = state.requireValue; + final engineWasAvailable = curState.isEngineAvailable; + + state = AsyncData( + curState.copyWith( + isComputerAnalysisEnabled: !curState.isComputerAnalysisEnabled, + ), + ); + + final computerAllowed = state.requireValue.isComputerAnalysisEnabled; + if (!computerAllowed && engineWasAvailable) { + toggleLocalEvaluation(); + } } /// Toggles the local evaluation on/off. @@ -343,11 +355,9 @@ class AnalysisController extends _$AnalysisController .read(analysisPreferencesProvider.notifier) .toggleEnableLocalEvaluation(); - final prefs = ref.read(analysisPreferencesProvider); state = AsyncData( state.requireValue.copyWith( - isLocalEvaluationEnabled: prefs.enableLocalEvaluation, - isComputerAnalysisEnabled: prefs.enableComputerAnalysis, + isLocalEvaluationEnabled: !state.requireValue.isLocalEvaluationEnabled, ), ); @@ -456,9 +466,9 @@ class AnalysisController extends _$AnalysisController // always show variation if the user plays a move if (shouldForceShowVariation && currentNode is Branch && - currentNode.isHidden) { + currentNode.isCollapsed) { _root.updateAt(path, (node) { - if (node is Branch) node.isHidden = false; + if (node is Branch) node.isCollapsed = false; }); } @@ -646,7 +656,7 @@ class AnalysisController extends _$AnalysisController Branch( position: n1.position.playUnchecked(move), sanMove: SanMove(san, move), - isHidden: children.length > 1, + isCollapsed: children.length > 1, ), ); } diff --git a/lib/src/model/common/node.dart b/lib/src/model/common/node.dart index ea9bd6c638..fefd59fdcc 100644 --- a/lib/src/model/common/node.dart +++ b/lib/src/model/common/node.dart @@ -130,7 +130,7 @@ abstract class Node { return null; } - /// Updates all nodes. + /// Recursively applies [update] to all nodes of the tree. void updateAll(void Function(Node node) update) { update(this); for (final child in children) { @@ -360,7 +360,8 @@ class Branch extends Node { super.eval, super.opening, required this.sanMove, - this.isHidden = false, + this.isComputerVariation = false, + this.isCollapsed = false, this.lichessAnalysisComments, // below are fields from dartchess [PgnNodeData] this.startingComments, @@ -368,8 +369,11 @@ class Branch extends Node { this.nags, }); + /// Whether this branch is from a variation generated by lichess computer analysis. + final bool isComputerVariation; + /// Whether the branch should be hidden in the tree view. - bool isHidden; + bool isCollapsed; /// The id of the branch, using a concise notation of associated move. UciCharPair get id => UciCharPair.fromMove(sanMove.move); @@ -398,7 +402,8 @@ class Branch extends Node { eval: eval, opening: opening, children: IList(children.map((child) => child.view)), - isHidden: isHidden, + isComputerVariation: isComputerVariation, + isCollapsed: isCollapsed, lichessAnalysisComments: lichessAnalysisComments?.lock, startingComments: startingComments?.lock, comments: comments?.lock, @@ -487,7 +492,8 @@ class Root extends Node { final branch = Branch( sanMove: SanMove(childFrom.data.san, move), position: newPos, - isHidden: frame.nesting > 2 || hideVariations && childIdx > 0, + isCollapsed: frame.nesting > 2 || hideVariations && childIdx > 0, + isComputerVariation: isLichessAnalysis && childIdx > 0, lichessAnalysisComments: isLichessAnalysis ? comments?.toList() : null, startingComments: isLichessAnalysis @@ -587,7 +593,8 @@ class ViewBranch extends ViewNode with _$ViewBranch { required Position position, Opening? opening, required IList children, - @Default(false) bool isHidden, + @Default(false) bool isCollapsed, + required bool isComputerVariation, ClientEval? eval, IList? lichessAnalysisComments, IList? startingComments, diff --git a/lib/src/model/study/study_controller.dart b/lib/src/model/study/study_controller.dart index 459751d81b..5648833799 100644 --- a/lib/src/model/study/study_controller.dart +++ b/lib/src/model/study/study_controller.dart @@ -289,9 +289,9 @@ class StudyController extends _$StudyController implements PgnTreeNotifier { _root.isOnMainline(path) ? node.children.skip(1) : node.children; for (final child in childrenToShow) { - child.isHidden = false; + child.isCollapsed = false; for (final grandChild in child.children) { - grandChild.isHidden = false; + grandChild.isCollapsed = false; } } state = AsyncValue.data(state.requireValue.copyWith(root: _root.view)); @@ -304,7 +304,7 @@ class StudyController extends _$StudyController implements PgnTreeNotifier { final node = _root.nodeAt(path); for (final child in node.children) { - child.isHidden = true; + child.isCollapsed = true; } state = AsyncValue.data(state.requireValue.copyWith(root: _root.view)); @@ -417,9 +417,9 @@ class StudyController extends _$StudyController implements PgnTreeNotifier { // always show variation if the user plays a move if (shouldForceShowVariation && currentNode is Branch && - currentNode.isHidden) { + currentNode.isCollapsed) { _root.updateAt(path, (node) { - if (node is Branch) node.isHidden = false; + if (node is Branch) node.isCollapsed = false; }); } diff --git a/lib/src/view/analysis/analysis_screen.dart b/lib/src/view/analysis/analysis_screen.dart index 476645fca7..2a701e57e2 100644 --- a/lib/src/view/analysis/analysis_screen.dart +++ b/lib/src/view/analysis/analysis_screen.dart @@ -97,10 +97,12 @@ class _AnalysisScreenState extends ConsumerState @override Widget build(BuildContext context) { final ctrlProvider = analysisControllerProvider(widget.options); - final asyncState = ref.watch(ctrlProvider); + final prefs = ref.watch(analysisPreferencesProvider); + final appBarActions = [ - EngineDepth(defaultEval: asyncState.valueOrNull?.currentNode.eval), + if (prefs.enableComputerAnalysis) + EngineDepth(defaultEval: asyncState.valueOrNull?.currentNode.eval), AppBarAnalysisTabIndicator( tabs: tabs, controller: _tabController, @@ -137,7 +139,7 @@ class _AnalysisScreenState extends ConsumerState ), ); case AsyncError(:final error, :final stackTrace): - _logger.severe('Cannot load study: $error', stackTrace); + _logger.severe('Cannot load analysis: $error', stackTrace); return FullScreenRetryRequest( onRetry: () { ref.invalidate(ctrlProvider); diff --git a/lib/src/widgets/pgn.dart b/lib/src/widgets/pgn.dart index fb2a2a6c85..8781cf9bc0 100644 --- a/lib/src/widgets/pgn.dart +++ b/lib/src/widgets/pgn.dart @@ -195,22 +195,30 @@ class _DebouncedPgnTreeViewState extends ConsumerState { // using the fast replay buttons. @override Widget build(BuildContext context) { - final shouldShowComments = ref.watch( - analysisPreferencesProvider.select( - (value) => value.enableComputerAnalysis && value.showPgnComments, - ), + final withComputerAnalysis = ref.watch( + analysisPreferencesProvider + .select((value) => value.enableComputerAnalysis), ); - final shouldShowAnnotations = ref.watch( - analysisPreferencesProvider.select( - (value) => value.enableComputerAnalysis && value.showAnnotations, - ), - ); + final shouldShowComments = withComputerAnalysis && + ref.watch( + analysisPreferencesProvider.select( + (value) => value.showPgnComments, + ), + ); + + final shouldShowAnnotations = withComputerAnalysis && + ref.watch( + analysisPreferencesProvider.select( + (value) => value.showAnnotations, + ), + ); return _PgnTreeView( root: widget.root, rootComments: widget.pgnRootComments, params: ( + withComputerAnalysis: withComputerAnalysis, shouldShowAnnotations: shouldShowAnnotations, shouldShowComments: shouldShowComments, currentMoveKey: currentMoveKey, @@ -229,6 +237,11 @@ typedef _PgnTreeViewParams = ({ /// Path to the currently selected move in the tree. UciPath pathToCurrentMove, + /// Whether to show NAG, comments, and analysis variations. + /// + /// Takes precedence over [shouldShowAnnotations], and [shouldShowComments], + bool withComputerAnalysis, + /// Whether to show NAG annotations like '!' and '??'. bool shouldShowAnnotations, @@ -243,6 +256,15 @@ typedef _PgnTreeViewParams = ({ PgnTreeNotifier notifier, }); +IList _computerPrefAwareChildren( + ViewNode node, + bool withComputerAnalysis, +) { + return node.children + .where((c) => withComputerAnalysis || !c.isComputerVariation) + .toIList(); +} + /// Whether to display the sideline inline. /// /// Sidelines are usually rendered on a new line and indented. @@ -255,16 +277,22 @@ bool _displaySideLineAsInline(ViewBranch node, [int depth = 0]) { } /// Returns whether this node has a sideline that should not be displayed inline. -bool _hasNonInlineSideLine(ViewNode node) => - node.children.length > 2 || - (node.children.length == 2 && !_displaySideLineAsInline(node.children[1])); +bool _hasNonInlineSideLine(ViewNode node, _PgnTreeViewParams params) { + final children = + _computerPrefAwareChildren(node, params.withComputerAnalysis); + return children.length > 2 || + (children.length == 2 && !_displaySideLineAsInline(children[1])); +} /// Splits the mainline into parts, where each part is a sequence of moves that are displayed on the same line. /// /// A part ends when a mainline node has a sideline that should not be displayed inline. -Iterable> _mainlineParts(ViewRoot root) => +Iterable> _mainlineParts( + ViewRoot root, + _PgnTreeViewParams params, +) => [root, ...root.mainline] - .splitAfter(_hasNonInlineSideLine) + .splitAfter((n) => _hasNonInlineSideLine(n, params)) .takeWhile((nodes) => nodes.firstOrNull?.children.isNotEmpty == true); class _PgnTreeView extends StatefulWidget { @@ -382,7 +410,8 @@ class _PgnTreeViewState extends State<_PgnTreeView> { void _updateLines({required bool fullRebuild}) { setState(() { if (fullRebuild) { - mainlineParts = _mainlineParts(widget.root).toList(growable: false); + mainlineParts = + _mainlineParts(widget.root, widget.params).toList(growable: false); } subtrees = _buildChangedSubtrees(fullRebuild: fullRebuild); @@ -400,6 +429,8 @@ class _PgnTreeViewState extends State<_PgnTreeView> { super.didUpdateWidget(oldWidget); _updateLines( fullRebuild: oldWidget.root != widget.root || + oldWidget.params.withComputerAnalysis != + widget.params.withComputerAnalysis || oldWidget.params.shouldShowComments != widget.params.shouldShowComments || oldWidget.params.shouldShowAnnotations != @@ -632,6 +663,12 @@ class _SideLinePart extends ConsumerWidget { /// A widget that renders part of the mainline. /// /// A part of the mainline is rendered on a single line. See [_mainlineParts]. +/// +/// For example: +/// 1. e4 e5 <-- mainline part +/// |- 1... d5 <-- sideline part +/// |- 1... Nc6 <-- sideline part +/// 2. Nf3 Nc6 (2... a5) 3. Bc4 <-- mainline part class _MainLinePart extends ConsumerWidget { const _MainLinePart({ required this.initialPath, @@ -655,27 +692,37 @@ class _MainLinePart extends ConsumerWidget { return Text.rich( TextSpan( children: nodes - .takeWhile((node) => node.children.isNotEmpty) + .takeWhile( + (node) => + _computerPrefAwareChildren(node, params.withComputerAnalysis) + .isNotEmpty, + ) .mapIndexed( (i, node) { - final mainlineNode = node.children.first; + final children = _computerPrefAwareChildren( + node, + params.withComputerAnalysis, + ); + final mainlineNode = children.first; final moves = [ _moveWithComment( mainlineNode, lineInfo: ( type: _LineType.mainline, - startLine: i == 0 || (node as ViewBranch).hasTextComment, + startLine: i == 0 || + (params.shouldShowComments && + (node as ViewBranch).hasTextComment), pathToLine: initialPath, ), pathToNode: path, textStyle: textStyle, params: params, ), - if (node.children.length == 2 && - _displaySideLineAsInline(node.children[1])) ...[ + if (children.length == 2 && + _displaySideLineAsInline(children[1])) ...[ _buildInlineSideLine( followsComment: mainlineNode.hasTextComment, - firstNode: node.children[1], + firstNode: children[1], parent: node, initialPath: path, textStyle: textStyle, @@ -719,7 +766,7 @@ class _SideLine extends StatelessWidget { List _getSidelinePartNodes() { final sidelineNodes = [firstNode]; while (sidelineNodes.last.children.isNotEmpty && - !_hasNonInlineSideLine(sidelineNodes.last)) { + !_hasNonInlineSideLine(sidelineNodes.last, params)) { sidelineNodes.add(sidelineNodes.last.children.first); } return sidelineNodes.toList(growable: false); @@ -848,7 +895,7 @@ class _IndentedSideLinesState extends State<_IndentedSideLines> { /// calculate the position of the indents in a post-frame callback. void _redrawIndents() { _sideLinesStartKeys = List.generate( - _visibleSideLines.length + (_hasHiddenLines ? 1 : 0), + _expandedSidelines.length + (_hasCollapsedLines ? 1 : 0), (_) => GlobalKey(), ); WidgetsBinding.instance.addPostFrameCallback((_) { @@ -871,10 +918,11 @@ class _IndentedSideLinesState extends State<_IndentedSideLines> { }); } - bool get _hasHiddenLines => widget.sideLines.any((node) => node.isHidden); + bool get _hasCollapsedLines => + widget.sideLines.any((node) => node.isCollapsed); - Iterable get _visibleSideLines => - widget.sideLines.whereNot((node) => node.isHidden); + Iterable get _expandedSidelines => + widget.sideLines.whereNot((node) => node.isCollapsed); @override void initState() { @@ -892,7 +940,7 @@ class _IndentedSideLinesState extends State<_IndentedSideLines> { @override Widget build(BuildContext context) { - final sideLineWidgets = _visibleSideLines + final sideLineWidgets = _expandedSidelines .mapIndexed( (i, firstSidelineNode) => _SideLine( firstNode: firstSidelineNode, @@ -920,7 +968,7 @@ class _IndentedSideLinesState extends State<_IndentedSideLines> { crossAxisAlignment: CrossAxisAlignment.start, children: [ ...sideLineWidgets, - if (_hasHiddenLines) + if (_hasCollapsedLines) GestureDetector( child: Icon( Icons.add_box, From fce8378b99fc260f84b6ea08432ca7cad60e5d18 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Thu, 21 Nov 2024 21:01:44 +0100 Subject: [PATCH 13/23] More work on analysis computer toggle --- .../model/analysis/analysis_controller.dart | 4 ++- lib/src/view/analysis/analysis_board.dart | 30 ++++++++++--------- lib/src/view/analysis/analysis_screen.dart | 22 ++++++++++++-- 3 files changed, 39 insertions(+), 17 deletions(-) diff --git a/lib/src/model/analysis/analysis_controller.dart b/lib/src/model/analysis/analysis_controller.dart index 12600de25d..c9db2303ae 100644 --- a/lib/src/model/analysis/analysis_controller.dart +++ b/lib/src/model/analysis/analysis_controller.dart @@ -793,7 +793,9 @@ class AnalysisState with _$AnalysisState { /// Whether the server analysis is available. bool get hasServerAnalysis => playersAnalysis != null; - bool get canShowGameSummary => hasServerAnalysis || canRequestServerAnalysis; + bool get canShowGameSummary => + isComputerAnalysisEnabledAndAllowed && + (hasServerAnalysis || canRequestServerAnalysis); /// Whether an evaluation can be available bool get hasAvailableEval => diff --git a/lib/src/view/analysis/analysis_board.dart b/lib/src/view/analysis/analysis_board.dart index c6e70e8557..8f37f0fe54 100644 --- a/lib/src/view/analysis/analysis_board.dart +++ b/lib/src/view/analysis/analysis_board.dart @@ -41,23 +41,25 @@ class AnalysisBoardState extends ConsumerState { final ctrlProvider = analysisControllerProvider(widget.options); final analysisState = ref.watch(ctrlProvider).requireValue; final boardPrefs = ref.watch(boardPreferencesProvider); - final showBestMoveArrow = ref.watch( - analysisPreferencesProvider.select( - (value) => value.showBestMoveArrow, - ), - ); - final showAnnotationsOnBoard = ref.watch( - analysisPreferencesProvider.select((value) => value.showAnnotations), - ); - - final evalBestMoves = ref.watch( - engineEvaluationProvider.select((s) => s.eval?.bestMoves), - ); + final analysisPrefs = ref.watch(analysisPreferencesProvider); + final enableComputerAnalysis = analysisPrefs.enableComputerAnalysis; + final showBestMoveArrow = + enableComputerAnalysis && analysisPrefs.showBestMoveArrow; + final showAnnotationsOnBoard = + enableComputerAnalysis && analysisPrefs.showAnnotations; + final evalBestMoves = enableComputerAnalysis + ? ref.watch( + engineEvaluationProvider.select((s) => s.eval?.bestMoves), + ) + : null; final currentNode = analysisState.currentNode; - final annotation = makeAnnotation(currentNode.nags); + final annotation = + showAnnotationsOnBoard ? makeAnnotation(currentNode.nags) : null; - final bestMoves = evalBestMoves ?? currentNode.eval?.bestMoves; + final bestMoves = enableComputerAnalysis + ? evalBestMoves ?? currentNode.eval?.bestMoves + : null; final sanMove = currentNode.sanMove; diff --git a/lib/src/view/analysis/analysis_screen.dart b/lib/src/view/analysis/analysis_screen.dart index 2a701e57e2..129f27620c 100644 --- a/lib/src/view/analysis/analysis_screen.dart +++ b/lib/src/view/analysis/analysis_screen.dart @@ -67,8 +67,12 @@ class _AnalysisScreenState extends ConsumerState ref.listenManual>( analysisControllerProvider(widget.options), - (_, state) { - if (state.valueOrNull?.canShowGameSummary == true) { + (prev, state) { + final canPrevShowGameSummary = + prev?.valueOrNull?.canShowGameSummary == true; + final canShowGameSummary = + state.valueOrNull?.canShowGameSummary == true; + if (!canPrevShowGameSummary && canShowGameSummary) { setState(() { tabs = [ AnalysisTab.opening, @@ -83,6 +87,20 @@ class _AnalysisScreenState extends ConsumerState length: tabs.length, ); }); + } else if (canPrevShowGameSummary && !canShowGameSummary) { + setState(() { + tabs = [ + AnalysisTab.opening, + AnalysisTab.moves, + ]; + final index = _tabController.index; + _tabController.dispose(); + _tabController = TabController( + vsync: this, + initialIndex: index == 2 ? 1 : index, + length: tabs.length, + ); + }); } }, ); From f93d164eaa3cc7212bd0decf2b0d6bf6ae1a168b Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Fri, 22 Nov 2024 09:26:03 +0100 Subject: [PATCH 14/23] Fix analysis orientation --- lib/src/model/analysis/analysis_controller.dart | 9 +++------ lib/src/model/game/game_controller.dart | 1 + lib/src/view/board_editor/board_editor_screen.dart | 2 +- .../offline_correspondence_game_screen.dart | 2 +- lib/src/view/game/archived_game_screen.dart | 1 + lib/src/view/game/game_list_tile.dart | 5 ++++- lib/src/view/game/game_result_dialog.dart | 2 +- lib/src/view/puzzle/puzzle_screen.dart | 2 +- lib/src/view/puzzle/streak_screen.dart | 2 +- lib/src/view/study/study_bottom_bar.dart | 2 +- lib/src/view/tools/load_position_screen.dart | 4 ++-- lib/src/view/tools/tools_tab_screen.dart | 4 ++-- test/view/analysis/analysis_screen_test.dart | 6 +++--- .../opening_explorer/opening_explorer_screen_test.dart | 2 +- 14 files changed, 23 insertions(+), 21 deletions(-) diff --git a/lib/src/model/analysis/analysis_controller.dart b/lib/src/model/analysis/analysis_controller.dart index c9db2303ae..4dd4dd4cc5 100644 --- a/lib/src/model/analysis/analysis_controller.dart +++ b/lib/src/model/analysis/analysis_controller.dart @@ -32,7 +32,6 @@ final _dateFormat = DateFormat('yyyy.MM.dd'); typedef StandaloneAnalysis = ({ String pgn, Variant variant, - Side orientation, bool isComputerAnalysisAllowed, }); @@ -42,6 +41,7 @@ class AnalysisOptions with _$AnalysisOptions { @Assert('standalone != null || gameId != null') const factory AnalysisOptions({ + required Side orientation, StandaloneAnalysis? standalone, GameId? gameId, int? initialMoveCursor, @@ -55,7 +55,6 @@ class AnalysisController extends _$AnalysisController implements PgnTreeNotifier { late final Root _root; late final Variant _variant; - late final Side _orientation; final _engineEvalDebounce = Debouncer(const Duration(milliseconds: 150)); @@ -75,14 +74,12 @@ class AnalysisController extends _$AnalysisController final game = await ref.watch(archivedGameProvider(id: options.gameId!).future); _variant = game.meta.variant; - _orientation = game.youAre ?? Side.white; pgn = game.makePgn(); opening = game.data.opening; serverAnalysis = game.serverAnalysis; division = game.meta.division; } else { _variant = options.standalone!.variant; - _orientation = options.standalone!.orientation; pgn = options.standalone!.pgn; opening = null; serverAnalysis = null; @@ -164,7 +161,7 @@ class AnalysisController extends _$AnalysisController pgnHeaders: pgnHeaders, pgnRootComments: rootComments, lastMove: lastMove, - pov: _orientation, + pov: options.orientation, contextOpening: opening, isComputerAnalysisAllowed: isComputerAnalysisAllowed, isComputerAnalysisEnabled: prefs.enableComputerAnalysis, @@ -425,7 +422,7 @@ class AnalysisController extends _$AnalysisController Future requestServerAnalysis() { if (state.requireValue.canRequestServerAnalysis) { final service = ref.read(serverAnalysisServiceProvider); - return service.requestAnalysis(options.gameId!, _orientation); + return service.requestAnalysis(options.gameId!, options.orientation); } return Future.error('Cannot request server analysis'); } diff --git a/lib/src/model/game/game_controller.dart b/lib/src/model/game/game_controller.dart index e663d77b04..568782422f 100644 --- a/lib/src/model/game/game_controller.dart +++ b/lib/src/model/game/game_controller.dart @@ -1167,6 +1167,7 @@ class GameState with _$GameState { String get analysisPgn => game.makePgn(); AnalysisOptions get analysisOptions => AnalysisOptions( + orientation: game.youAre ?? Side.white, initialMoveCursor: stepCursor, gameId: gameFullId.gameId, ); diff --git a/lib/src/view/board_editor/board_editor_screen.dart b/lib/src/view/board_editor/board_editor_screen.dart index 45ee49e2b1..e5a66bd187 100644 --- a/lib/src/view/board_editor/board_editor_screen.dart +++ b/lib/src/view/board_editor/board_editor_screen.dart @@ -336,11 +336,11 @@ class _BottomBar extends ConsumerWidget { rootNavigator: true, builder: (context) => AnalysisScreen( options: AnalysisOptions( + orientation: editorState.orientation, standalone: ( pgn: editorState.pgn!, isComputerAnalysisAllowed: true, variant: Variant.fromPosition, - orientation: editorState.orientation, ), ), ), diff --git a/lib/src/view/correspondence/offline_correspondence_game_screen.dart b/lib/src/view/correspondence/offline_correspondence_game_screen.dart index 931ad9e5d5..f8a6b18697 100644 --- a/lib/src/view/correspondence/offline_correspondence_game_screen.dart +++ b/lib/src/view/correspondence/offline_correspondence_game_screen.dart @@ -260,11 +260,11 @@ class _BodyState extends ConsumerState<_Body> { context, builder: (_) => AnalysisScreen( options: AnalysisOptions( + orientation: game.youAre, standalone: ( pgn: game.makePgn(), isComputerAnalysisAllowed: false, variant: game.variant, - orientation: game.youAre, ), initialMoveCursor: stepCursor, ), diff --git a/lib/src/view/game/archived_game_screen.dart b/lib/src/view/game/archived_game_screen.dart index ec5226fe83..c7ef8b358d 100644 --- a/lib/src/view/game/archived_game_screen.dart +++ b/lib/src/view/game/archived_game_screen.dart @@ -380,6 +380,7 @@ class _BottomBar extends ConsumerWidget { context, builder: (context) => AnalysisScreen( options: AnalysisOptions( + orientation: orientation, gameId: gameData.id, initialMoveCursor: cursor, ), diff --git a/lib/src/view/game/game_list_tile.dart b/lib/src/view/game/game_list_tile.dart index 1f7417d3db..3901f67c2b 100644 --- a/lib/src/view/game/game_list_tile.dart +++ b/lib/src/view/game/game_list_tile.dart @@ -226,7 +226,10 @@ class _ContextMenu extends ConsumerWidget { pushPlatformRoute( context, builder: (context) => AnalysisScreen( - options: AnalysisOptions(gameId: game.id), + options: AnalysisOptions( + orientation: orientation, + gameId: game.id, + ), ), ); } diff --git a/lib/src/view/game/game_result_dialog.dart b/lib/src/view/game/game_result_dialog.dart index e4ebf84b08..a4e6522d53 100644 --- a/lib/src/view/game/game_result_dialog.dart +++ b/lib/src/view/game/game_result_dialog.dart @@ -261,11 +261,11 @@ class OverTheBoardGameResultDialog extends StatelessWidget { context, builder: (_) => AnalysisScreen( options: AnalysisOptions( + orientation: Side.white, standalone: ( pgn: game.makePgn(), isComputerAnalysisAllowed: true, variant: game.meta.variant, - orientation: Side.white, ), ), ), diff --git a/lib/src/view/puzzle/puzzle_screen.dart b/lib/src/view/puzzle/puzzle_screen.dart index 0885219d38..63bae946cd 100644 --- a/lib/src/view/puzzle/puzzle_screen.dart +++ b/lib/src/view/puzzle/puzzle_screen.dart @@ -485,11 +485,11 @@ class _BottomBar extends ConsumerWidget { context, builder: (context) => AnalysisScreen( options: AnalysisOptions( + orientation: puzzleState.pov, standalone: ( pgn: ref.read(ctrlProvider.notifier).makePgn(), isComputerAnalysisAllowed: true, variant: Variant.standard, - orientation: puzzleState.pov, ), initialMoveCursor: 0, ), diff --git a/lib/src/view/puzzle/streak_screen.dart b/lib/src/view/puzzle/streak_screen.dart index cd38aa97fb..7382bee36c 100644 --- a/lib/src/view/puzzle/streak_screen.dart +++ b/lib/src/view/puzzle/streak_screen.dart @@ -292,11 +292,11 @@ class _BottomBar extends ConsumerWidget { context, builder: (context) => AnalysisScreen( options: AnalysisOptions( + orientation: puzzleState.pov, standalone: ( pgn: ref.read(ctrlProvider.notifier).makePgn(), isComputerAnalysisAllowed: true, variant: Variant.standard, - orientation: puzzleState.pov, ), initialMoveCursor: 0, ), diff --git a/lib/src/view/study/study_bottom_bar.dart b/lib/src/view/study/study_bottom_bar.dart index 104f5c29fd..2c8d4c7a12 100644 --- a/lib/src/view/study/study_bottom_bar.dart +++ b/lib/src/view/study/study_bottom_bar.dart @@ -166,11 +166,11 @@ class _GamebookBottomBar extends ConsumerWidget { rootNavigator: true, builder: (context) => AnalysisScreen( options: AnalysisOptions( + orientation: state.pov, standalone: ( pgn: state.pgn, isComputerAnalysisAllowed: true, variant: state.variant, - orientation: state.pov, ), ), ), diff --git a/lib/src/view/tools/load_position_screen.dart b/lib/src/view/tools/load_position_screen.dart index e7655899c7..3c48bf6ad7 100644 --- a/lib/src/view/tools/load_position_screen.dart +++ b/lib/src/view/tools/load_position_screen.dart @@ -131,11 +131,11 @@ class _BodyState extends State<_Body> { return ( fen: pos.fen, options: AnalysisOptions( + orientation: Side.white, standalone: ( pgn: '[FEN "${pos.fen}"]', isComputerAnalysisAllowed: true, variant: Variant.standard, - orientation: Side.white, ), ) ); @@ -163,11 +163,11 @@ class _BodyState extends State<_Body> { return ( fen: lastPosition.fen, options: AnalysisOptions( + orientation: Side.white, standalone: ( pgn: textInput!, isComputerAnalysisAllowed: true, variant: rule != null ? Variant.fromRule(rule) : Variant.standard, - orientation: Side.white, ), initialMoveCursor: mainlineMoves.isEmpty ? 0 : 1, ) diff --git a/lib/src/view/tools/tools_tab_screen.dart b/lib/src/view/tools/tools_tab_screen.dart index 3834d28098..f00a896a22 100644 --- a/lib/src/view/tools/tools_tab_screen.dart +++ b/lib/src/view/tools/tools_tab_screen.dart @@ -149,11 +149,11 @@ class _Body extends ConsumerWidget { rootNavigator: true, builder: (context) => const AnalysisScreen( options: AnalysisOptions( + orientation: Side.white, standalone: ( pgn: '', isComputerAnalysisAllowed: true, variant: Variant.standard, - orientation: Side.white, ), ), ), @@ -168,11 +168,11 @@ class _Body extends ConsumerWidget { rootNavigator: true, builder: (context) => const OpeningExplorerScreen( options: AnalysisOptions( + orientation: Side.white, standalone: ( pgn: '', isComputerAnalysisAllowed: false, variant: Variant.standard, - orientation: Side.white, ), ), ), diff --git a/test/view/analysis/analysis_screen_test.dart b/test/view/analysis/analysis_screen_test.dart index 44bf19bc87..bf91a6b889 100644 --- a/test/view/analysis/analysis_screen_test.dart +++ b/test/view/analysis/analysis_screen_test.dart @@ -24,10 +24,10 @@ void main() { tester, home: AnalysisScreen( options: AnalysisOptions( + orientation: Side.white, standalone: ( pgn: sanMoves, isComputerAnalysisAllowed: false, - orientation: Side.white, variant: Variant.standard, ), ), @@ -60,11 +60,11 @@ void main() { tester, home: AnalysisScreen( options: AnalysisOptions( + orientation: Side.white, standalone: ( pgn: sanMoves, isComputerAnalysisAllowed: false, variant: Variant.standard, - orientation: Side.white, ), ), ), @@ -127,11 +127,11 @@ void main() { }, home: AnalysisScreen( options: AnalysisOptions( + orientation: Side.white, standalone: ( pgn: pgn, isComputerAnalysisAllowed: false, variant: Variant.standard, - orientation: Side.white, ), ), enableDrawingShapes: false, diff --git a/test/view/opening_explorer/opening_explorer_screen_test.dart b/test/view/opening_explorer/opening_explorer_screen_test.dart index 10a3b08a72..662440da3b 100644 --- a/test/view/opening_explorer/opening_explorer_screen_test.dart +++ b/test/view/opening_explorer/opening_explorer_screen_test.dart @@ -40,10 +40,10 @@ void main() { }); const options = AnalysisOptions( + orientation: Side.white, standalone: ( pgn: '', isComputerAnalysisAllowed: false, - orientation: Side.white, variant: Variant.standard, ), ); From 52f2f6a8b1c99f14815a82cca0422a5d3c4f24d8 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Fri, 22 Nov 2024 10:03:07 +0100 Subject: [PATCH 15/23] Don't make tabs dynamic --- .../model/analysis/analysis_controller.dart | 12 ++-- lib/src/view/analysis/analysis_layout.dart | 6 +- lib/src/view/analysis/analysis_screen.dart | 66 +++---------------- lib/src/view/analysis/analysis_settings.dart | 2 +- lib/src/view/analysis/server_analysis.dart | 29 ++++++++ 5 files changed, 47 insertions(+), 68 deletions(-) diff --git a/lib/src/model/analysis/analysis_controller.dart b/lib/src/model/analysis/analysis_controller.dart index 4dd4dd4cc5..d12a48cae1 100644 --- a/lib/src/model/analysis/analysis_controller.dart +++ b/lib/src/model/analysis/analysis_controller.dart @@ -790,23 +790,21 @@ class AnalysisState with _$AnalysisState { /// Whether the server analysis is available. bool get hasServerAnalysis => playersAnalysis != null; - bool get canShowGameSummary => - isComputerAnalysisEnabledAndAllowed && - (hasServerAnalysis || canRequestServerAnalysis); + bool get canShowGameSummary => hasServerAnalysis || canRequestServerAnalysis; /// Whether an evaluation can be available bool get hasAvailableEval => isEngineAvailable || - (isComputerAnalysisEnabledAndAllowed && + (isComputerAnalysisAllowedAndEnabled && acplChartData != null && acplChartData!.isNotEmpty); - bool get isComputerAnalysisEnabledAndAllowed => - isComputerAnalysisEnabled && isComputerAnalysisAllowed; + bool get isComputerAnalysisAllowedAndEnabled => + isComputerAnalysisAllowed && isComputerAnalysisEnabled; /// Whether the engine is allowed for this analysis and variant. bool get isEngineAllowed => - isComputerAnalysisEnabledAndAllowed && + isComputerAnalysisAllowedAndEnabled && engineSupportedVariants.contains(variant); /// Whether the engine is available for evaluation diff --git a/lib/src/view/analysis/analysis_layout.dart b/lib/src/view/analysis/analysis_layout.dart index 5ca457b409..bad95d9e73 100644 --- a/lib/src/view/analysis/analysis_layout.dart +++ b/lib/src/view/analysis/analysis_layout.dart @@ -33,11 +33,9 @@ enum AnalysisTab { case AnalysisTab.opening: return l10n.openingExplorer; case AnalysisTab.moves: - // TODO: Add l10n - return 'Moves'; + return l10n.movesPlayed; case AnalysisTab.summary: - // TODO: Add l10n - return 'Summary'; + return l10n.computerAnalysis; } } } diff --git a/lib/src/view/analysis/analysis_screen.dart b/lib/src/view/analysis/analysis_screen.dart index 129f27620c..44852fe1a0 100644 --- a/lib/src/view/analysis/analysis_screen.dart +++ b/lib/src/view/analysis/analysis_screen.dart @@ -47,16 +47,18 @@ class AnalysisScreen extends ConsumerStatefulWidget { } class _AnalysisScreenState extends ConsumerState - with TickerProviderStateMixin { - late List tabs; - late TabController _tabController; + with SingleTickerProviderStateMixin { + late final List tabs; + late final TabController _tabController; @override void initState() { super.initState(); + tabs = [ AnalysisTab.opening, AnalysisTab.moves, + if (widget.options.gameId != null) AnalysisTab.summary, ]; _tabController = TabController( @@ -64,46 +66,6 @@ class _AnalysisScreenState extends ConsumerState initialIndex: 1, length: tabs.length, ); - - ref.listenManual>( - analysisControllerProvider(widget.options), - (prev, state) { - final canPrevShowGameSummary = - prev?.valueOrNull?.canShowGameSummary == true; - final canShowGameSummary = - state.valueOrNull?.canShowGameSummary == true; - if (!canPrevShowGameSummary && canShowGameSummary) { - setState(() { - tabs = [ - AnalysisTab.opening, - AnalysisTab.moves, - AnalysisTab.summary, - ]; - final index = _tabController.index; - _tabController.dispose(); - _tabController = TabController( - vsync: this, - initialIndex: index, - length: tabs.length, - ); - }); - } else if (canPrevShowGameSummary && !canShowGameSummary) { - setState(() { - tabs = [ - AnalysisTab.opening, - AnalysisTab.moves, - ]; - final index = _tabController.index; - _tabController.dispose(); - _tabController = TabController( - vsync: this, - initialIndex: index == 2 ? 1 : index, - length: tabs.length, - ); - }); - } - }, - ); } @override @@ -152,7 +114,6 @@ class _AnalysisScreenState extends ConsumerState body: _Body( options: widget.options, controller: _tabController, - tabs: tabs, enableDrawingShapes: widget.enableDrawingShapes, ), ); @@ -201,12 +162,10 @@ class _Body extends ConsumerWidget { const _Body({ required this.options, required this.controller, - required this.tabs, required this.enableDrawingShapes, }); final TabController controller; - final List tabs; final AnalysisOptions options; final bool enableDrawingShapes; @@ -258,16 +217,11 @@ class _Body extends ConsumerWidget { ) : null, bottomBar: _BottomBar(options: options), - children: tabs.map((tab) { - switch (tab) { - case AnalysisTab.opening: - return OpeningExplorerView(options: options); - case AnalysisTab.moves: - return AnalysisTreeView(options); - case AnalysisTab.summary: - return ServerAnalysisSummary(options); - } - }).toList(), + children: [ + OpeningExplorerView(options: options), + AnalysisTreeView(options), + if (options.gameId != null) ServerAnalysisSummary(options), + ], ); } } diff --git a/lib/src/view/analysis/analysis_settings.dart b/lib/src/view/analysis/analysis_settings.dart index 9b03d4c943..02ef27597c 100644 --- a/lib/src/view/analysis/analysis_settings.dart +++ b/lib/src/view/analysis/analysis_settings.dart @@ -47,7 +47,7 @@ class AnalysisSettings extends ConsumerWidget { ), AnimatedCrossFade( duration: const Duration(milliseconds: 300), - crossFadeState: value.isComputerAnalysisEnabledAndAllowed + crossFadeState: value.isComputerAnalysisAllowedAndEnabled ? CrossFadeState.showSecond : CrossFadeState.showFirst, firstChild: const SizedBox.shrink(), diff --git a/lib/src/view/analysis/server_analysis.dart b/lib/src/view/analysis/server_analysis.dart index d943bddb25..ae54ce5de7 100644 --- a/lib/src/view/analysis/server_analysis.dart +++ b/lib/src/view/analysis/server_analysis.dart @@ -8,6 +8,7 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/model/analysis/analysis_controller.dart'; +import 'package:lichess_mobile/src/model/analysis/analysis_preferences.dart'; import 'package:lichess_mobile/src/model/analysis/server_analysis_service.dart'; import 'package:lichess_mobile/src/model/auth/auth_session.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; @@ -22,14 +23,42 @@ class ServerAnalysisSummary extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { + final analysisPrefs = ref.watch(analysisPreferencesProvider); final ctrlProvider = analysisControllerProvider(options); final playersAnalysis = ref.watch( ctrlProvider.select((value) => value.requireValue.playersAnalysis), ); + final canShowGameSummary = ref.watch( + ctrlProvider.select((value) => value.requireValue.canShowGameSummary), + ); final pgnHeaders = ref .watch(ctrlProvider.select((value) => value.requireValue.pgnHeaders)); final currentGameAnalysis = ref.watch(currentAnalysisProvider); + if (analysisPrefs.enableComputerAnalysis == false) { + return Center( + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 16.0), + child: Column( + mainAxisSize: MainAxisSize.max, + children: [ + const Spacer(), + Text(context.l10n.computerAnalysisDisabled), + if (canShowGameSummary) + SecondaryButton( + onPressed: () { + ref.read(ctrlProvider.notifier).toggleComputerAnalysis(); + }, + semanticsLabel: context.l10n.enable, + child: Text(context.l10n.enable), + ), + const Spacer(), + ], + ), + ), + ); + } + return playersAnalysis != null ? ListView( children: [ From aea9b72c884bdb7713771e879ab67d080cd9f6e6 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Fri, 22 Nov 2024 10:15:30 +0100 Subject: [PATCH 16/23] Tweak analysis settings --- lib/src/view/analysis/analysis_settings.dart | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/lib/src/view/analysis/analysis_settings.dart b/lib/src/view/analysis/analysis_settings.dart index 02ef27597c..46372bbe14 100644 --- a/lib/src/view/analysis/analysis_settings.dart +++ b/lib/src/view/analysis/analysis_settings.dart @@ -122,11 +122,9 @@ class AnalysisSettings extends ConsumerWidget { SwitchSettingTile( title: Text(context.l10n.bestMoveArrow), value: prefs.showBestMoveArrow, - onChanged: value.isEngineAvailable - ? (value) => ref - .read(analysisPreferencesProvider.notifier) - .toggleShowBestMoveArrow() - : null, + onChanged: (value) => ref + .read(analysisPreferencesProvider.notifier) + .toggleShowBestMoveArrow(), ), SwitchSettingTile( title: Text(context.l10n.evaluationGauge), From 3382baf7305fb6f745280ba19ebcd9af01879e7f Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Fri, 22 Nov 2024 10:31:11 +0100 Subject: [PATCH 17/23] Tweak study settings --- lib/src/view/analysis/analysis_settings.dart | 13 +++ lib/src/view/study/study_settings.dart | 116 ++++++++++--------- 2 files changed, 72 insertions(+), 57 deletions(-) diff --git a/lib/src/view/analysis/analysis_settings.dart b/lib/src/view/analysis/analysis_settings.dart index 46372bbe14..7571566adc 100644 --- a/lib/src/view/analysis/analysis_settings.dart +++ b/lib/src/view/analysis/analysis_settings.dart @@ -3,6 +3,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/model/analysis/analysis_controller.dart'; import 'package:lichess_mobile/src/model/analysis/analysis_preferences.dart'; import 'package:lichess_mobile/src/model/engine/evaluation_service.dart'; +import 'package:lichess_mobile/src/model/settings/general_preferences.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/view/opening_explorer/opening_explorer_settings.dart'; import 'package:lichess_mobile/src/widgets/adaptive_bottom_sheet.dart'; @@ -21,6 +22,9 @@ class AnalysisSettings extends ConsumerWidget { final ctrlProvider = analysisControllerProvider(options); final prefs = ref.watch(analysisPreferencesProvider); final asyncState = ref.watch(ctrlProvider); + final isSoundEnabled = ref.watch( + generalPreferencesProvider.select((pref) => pref.isSoundEnabled), + ); switch (asyncState) { case AsyncData(:final value): @@ -150,6 +154,15 @@ class AnalysisSettings extends ConsumerWidget { ], ), ), + SwitchSettingTile( + title: Text(context.l10n.sound), + value: isSoundEnabled, + onChanged: (value) { + ref + .read(generalPreferencesProvider.notifier) + .toggleSoundEnabled(); + }, + ), ], ); case AsyncError(:final error): diff --git a/lib/src/view/study/study_settings.dart b/lib/src/view/study/study_settings.dart index ea8393242c..1064c5a962 100644 --- a/lib/src/view/study/study_settings.dart +++ b/lib/src/view/study/study_settings.dart @@ -37,48 +37,20 @@ class StudySettings extends ConsumerWidget { return BottomSheetScrollableContainer( children: [ - SwitchSettingTile( - title: Text(context.l10n.toggleLocalEvaluation), - value: analysisPrefs.enableLocalEvaluation, - onChanged: isComputerAnalysisAllowed - ? (_) { - ref.read(studyController.notifier).toggleLocalEvaluation(); - } - : null, - ), - PlatformListTile( - title: Text.rich( - TextSpan( - text: '${context.l10n.multipleLines}: ', - style: const TextStyle( - fontWeight: FontWeight.normal, - ), - children: [ - TextSpan( - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 18, - ), - text: analysisPrefs.numEvalLines.toString(), - ), - ], - ), - ), - subtitle: NonLinearSlider( - value: analysisPrefs.numEvalLines, - values: const [0, 1, 2, 3], - onChangeEnd: isEngineAvailable - ? (value) => ref - .read(studyController.notifier) - .setNumEvalLines(value.toInt()) + if (isComputerAnalysisAllowed) ...[ + SwitchSettingTile( + title: Text(context.l10n.toggleLocalEvaluation), + value: analysisPrefs.enableLocalEvaluation, + onChanged: isComputerAnalysisAllowed + ? (_) { + ref.read(studyController.notifier).toggleLocalEvaluation(); + } : null, ), - ), - if (maxEngineCores > 1) PlatformListTile( title: Text.rich( TextSpan( - text: '${context.l10n.cpus}: ', + text: '${context.l10n.multipleLines}: ', style: const TextStyle( fontWeight: FontWeight.normal, ), @@ -88,30 +60,67 @@ class StudySettings extends ConsumerWidget { fontWeight: FontWeight.bold, fontSize: 18, ), - text: analysisPrefs.numEngineCores.toString(), + text: analysisPrefs.numEvalLines.toString(), ), ], ), ), subtitle: NonLinearSlider( - value: analysisPrefs.numEngineCores, - values: List.generate(maxEngineCores, (index) => index + 1), + value: analysisPrefs.numEvalLines, + values: const [0, 1, 2, 3], onChangeEnd: isEngineAvailable ? (value) => ref .read(studyController.notifier) - .setEngineCores(value.toInt()) + .setNumEvalLines(value.toInt()) : null, ), ), - SwitchSettingTile( - title: Text(context.l10n.bestMoveArrow), - value: analysisPrefs.showBestMoveArrow, - onChanged: isEngineAvailable - ? (value) => ref - .read(analysisPreferencesProvider.notifier) - .toggleShowBestMoveArrow() - : null, - ), + if (maxEngineCores > 1) + PlatformListTile( + title: Text.rich( + TextSpan( + text: '${context.l10n.cpus}: ', + style: const TextStyle( + fontWeight: FontWeight.normal, + ), + children: [ + TextSpan( + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 18, + ), + text: analysisPrefs.numEngineCores.toString(), + ), + ], + ), + ), + subtitle: NonLinearSlider( + value: analysisPrefs.numEngineCores, + values: List.generate(maxEngineCores, (index) => index + 1), + onChangeEnd: isEngineAvailable + ? (value) => ref + .read(studyController.notifier) + .setEngineCores(value.toInt()) + : null, + ), + ), + SwitchSettingTile( + title: Text(context.l10n.bestMoveArrow), + value: analysisPrefs.showBestMoveArrow, + onChanged: isEngineAvailable + ? (value) => ref + .read(analysisPreferencesProvider.notifier) + .toggleShowBestMoveArrow() + : null, + ), + SwitchSettingTile( + title: Text(context.l10n.evaluationGauge), + value: analysisPrefs.showEvaluationGauge, + onChanged: (value) => ref + .read(analysisPreferencesProvider.notifier) + .toggleShowEvaluationGauge(), + ), + ], SwitchSettingTile( title: Text(context.l10n.showVariationArrows), value: studyPrefs.showVariationArrows, @@ -119,13 +128,6 @@ class StudySettings extends ConsumerWidget { .read(studyPreferencesProvider.notifier) .toggleShowVariationArrows(), ), - SwitchSettingTile( - title: Text(context.l10n.evaluationGauge), - value: analysisPrefs.showEvaluationGauge, - onChanged: (value) => ref - .read(analysisPreferencesProvider.notifier) - .toggleShowEvaluationGauge(), - ), SwitchSettingTile( title: Text(context.l10n.toggleGlyphAnnotations), value: analysisPrefs.showAnnotations, From 76e0aab250b54befe2c2a162aa3df28c51b8a8d7 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Fri, 22 Nov 2024 10:49:22 +0100 Subject: [PATCH 18/23] Use AnalysisLayout in StudyScreen --- lib/src/view/analysis/analysis_layout.dart | 7 +- lib/src/view/study/study_screen.dart | 241 ++++++++------------- 2 files changed, 94 insertions(+), 154 deletions(-) diff --git a/lib/src/view/analysis/analysis_layout.dart b/lib/src/view/analysis/analysis_layout.dart index bad95d9e73..0caf83a0ad 100644 --- a/lib/src/view/analysis/analysis_layout.dart +++ b/lib/src/view/analysis/analysis_layout.dart @@ -103,6 +103,11 @@ class _AppBarAnalysisTabIndicatorState } /// Layout for the analysis and similar screens (study, broadcast, etc.). +/// +/// The layout is responsive and adapts to the screen size and orientation. +/// +/// The length of the [children] list must match the [tabController]'s +/// [TabController.length] and the length of the [AppBarAnalysisTabIndicator.tabs] class AnalysisLayout extends StatelessWidget { const AnalysisLayout({ required this.tabController, @@ -122,7 +127,7 @@ class AnalysisLayout extends StatelessWidget { /// The children of the tab view. /// - /// The length of this list must match the [controller]'s [TabController.length] + /// The length of this list must match the [tabController]'s [TabController.length] /// and the length of the [AppBarAnalysisTabIndicator.tabs] list. final List children; diff --git a/lib/src/view/study/study_screen.dart b/lib/src/view/study/study_screen.dart index 7a1b246d6a..419c974b64 100644 --- a/lib/src/view/study/study_screen.dart +++ b/lib/src/view/study/study_screen.dart @@ -15,7 +15,7 @@ import 'package:lichess_mobile/src/model/settings/board_preferences.dart'; import 'package:lichess_mobile/src/model/study/study_controller.dart'; import 'package:lichess_mobile/src/model/study/study_preferences.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; -import 'package:lichess_mobile/src/utils/screen.dart'; +import 'package:lichess_mobile/src/view/analysis/analysis_layout.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/study/study_bottom_bar.dart'; @@ -26,7 +26,6 @@ import 'package:lichess_mobile/src/widgets/adaptive_bottom_sheet.dart'; import 'package:lichess_mobile/src/widgets/buttons.dart'; import 'package:lichess_mobile/src/widgets/list.dart'; import 'package:lichess_mobile/src/widgets/pgn.dart'; -import 'package:lichess_mobile/src/widgets/platform.dart'; import 'package:lichess_mobile/src/widgets/platform_scaffold.dart'; import 'package:logging/logging.dart'; @@ -168,7 +167,7 @@ class _StudyChaptersMenu extends ConsumerWidget { } } -class _Body extends ConsumerWidget { +class _Body extends ConsumerStatefulWidget { const _Body({ required this.id, }); @@ -176,155 +175,91 @@ class _Body extends ConsumerWidget { final StudyId id; @override - Widget build(BuildContext context, WidgetRef ref) { + ConsumerState<_Body> createState() => _BodyState(); +} + +class _BodyState extends ConsumerState<_Body> + with SingleTickerProviderStateMixin { + late final TabController _tabController; + + @override + void initState() { + super.initState(); + + _tabController = TabController( + vsync: this, + length: 1, + ); + } + + @override + void dispose() { + _tabController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { final gamebookActive = ref.watch( - studyControllerProvider(id) + studyControllerProvider(widget.id) .select((state) => state.requireValue.gamebookActive), ); + final engineGaugeParams = ref.watch( + studyControllerProvider(widget.id) + .select((state) => state.valueOrNull?.engineGaugeParams), + ); - return SafeArea( - child: Column( - children: [ - Expanded( - child: LayoutBuilder( - builder: (context, constraints) { - final defaultBoardSize = constraints.biggest.shortestSide; - final isTablet = isTabletOrLarger(context); - final remainingHeight = - constraints.maxHeight - defaultBoardSize; - final isSmallScreen = - remainingHeight < kSmallRemainingHeightLeftBoardThreshold; - final boardSize = isTablet || isSmallScreen - ? defaultBoardSize - kTabletBoardTableSidePadding * 2 - : defaultBoardSize; - - final landscape = constraints.biggest.aspectRatio > 1; - - final engineGaugeParams = ref.watch( - studyControllerProvider(id) - .select((state) => state.valueOrNull?.engineGaugeParams), - ); - - final currentNode = ref.watch( - studyControllerProvider(id) - .select((state) => state.requireValue.currentNode), - ); - - final engineLines = EngineLines( - clientEval: currentNode.eval, - isGameOver: currentNode.position?.isGameOver ?? false, - onTapMove: ref - .read( - studyControllerProvider(id).notifier, - ) - .onUserMove, - ); - - final showEvaluationGauge = ref.watch( - analysisPreferencesProvider - .select((value) => value.showEvaluationGauge), - ); - - final engineGauge = - showEvaluationGauge && engineGaugeParams != null - ? EngineGauge( - params: engineGaugeParams, - displayMode: landscape - ? EngineGaugeDisplayMode.vertical - : EngineGaugeDisplayMode.horizontal, - ) - : null; - - final bottomChild = - gamebookActive ? StudyGamebook(id) : StudyTreeView(id); - - return landscape - ? Row( - mainAxisSize: MainAxisSize.max, - children: [ - Padding( - padding: EdgeInsets.all( - isTablet ? kTabletBoardTableSidePadding : 0.0, - ), - child: Row( - children: [ - _StudyBoard( - id: id, - boardSize: boardSize, - isTablet: isTablet, - ), - if (engineGauge != null) ...[ - const SizedBox(width: 4.0), - engineGauge, - ], - ], - ), - ), - Flexible( - fit: FlexFit.loose, - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - if (engineGaugeParams != null) engineLines, - Expanded( - child: PlatformCard( - clipBehavior: Clip.hardEdge, - borderRadius: const BorderRadius.all( - Radius.circular(4.0), - ), - margin: const EdgeInsets.all( - kTabletBoardTableSidePadding, - ), - semanticContainer: false, - child: bottomChild, - ), - ), - ], - ), - ), - ], - ) - : Column( - mainAxisAlignment: MainAxisAlignment.center, - mainAxisSize: MainAxisSize.max, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Padding( - padding: EdgeInsets.all( - isTablet ? kTabletBoardTableSidePadding : 0.0, - ), - child: Column( - children: [ - if (engineGauge != null) ...[ - engineGauge, - engineLines, - ], - _StudyBoard( - id: id, - boardSize: boardSize, - isTablet: isTablet, - ), - ], - ), - ), - Expanded( - child: Padding( - padding: isTablet - ? const EdgeInsets.symmetric( - horizontal: kTabletBoardTableSidePadding, - ) - : EdgeInsets.zero, - child: bottomChild, - ), - ), - ], - ); - }, - ), - ), - StudyBottomBar(id: id), - ], + final currentNode = ref.watch( + studyControllerProvider(widget.id) + .select((state) => state.requireValue.currentNode), + ); + + final engineLines = EngineLines( + clientEval: currentNode.eval, + isGameOver: currentNode.position?.isGameOver ?? false, + onTapMove: ref + .read( + studyControllerProvider(widget.id).notifier, + ) + .onUserMove, + ); + + final showEvaluationGauge = ref.watch( + analysisPreferencesProvider.select((value) => value.showEvaluationGauge), + ); + + final bottomChild = + gamebookActive ? StudyGamebook(widget.id) : StudyTreeView(widget.id); + + return AnalysisLayout( + tabController: _tabController, + boardBuilder: (context, boardSize, borderRadius) => _StudyBoard( + id: widget.id, + boardSize: boardSize, + borderRadius: borderRadius, ), + engineGaugeBuilder: showEvaluationGauge && engineGaugeParams != null + ? (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: engineLines, + bottomBar: StudyBottomBar(id: widget.id), + children: [bottomChild], ); } } @@ -351,14 +286,14 @@ class _StudyBoard extends ConsumerStatefulWidget { const _StudyBoard({ required this.id, required this.boardSize, - required this.isTablet, + this.borderRadius, }); final StudyId id; final double boardSize; - final bool isTablet; + final BorderRadiusGeometry? borderRadius; @override ConsumerState<_StudyBoard> createState() => _StudyBoardState(); @@ -440,10 +375,10 @@ class _StudyBoardState extends ConsumerState<_StudyBoard> { return Chessboard( size: widget.boardSize, settings: boardPrefs.toBoardSettings().copyWith( - borderRadius: widget.isTablet - ? const BorderRadius.all(Radius.circular(4.0)) - : BorderRadius.zero, - boxShadow: widget.isTablet ? boardShadows : const [], + borderRadius: widget.borderRadius, + boxShadow: widget.borderRadius != null + ? boardShadows + : const [], drawShape: DrawShapeOptions( enable: true, onCompleteShape: _onCompleteShape, From 25cdd4533cad6a1a6c50a3470e62f4b3bed783ab Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Fri, 22 Nov 2024 11:05:24 +0100 Subject: [PATCH 19/23] Fix tablet analysis layout --- lib/src/view/analysis/analysis_layout.dart | 6 ++-- lib/src/view/analysis/analysis_screen.dart | 8 ++--- .../view/analysis/opening_explorer_view.dart | 10 +----- lib/src/view/engine/engine_lines.dart | 1 + lib/src/view/study/study_screen.dart | 36 +++++++++++-------- 5 files changed, 31 insertions(+), 30 deletions(-) diff --git a/lib/src/view/analysis/analysis_layout.dart b/lib/src/view/analysis/analysis_layout.dart index 0caf83a0ad..c8ead71161 100644 --- a/lib/src/view/analysis/analysis_layout.dart +++ b/lib/src/view/analysis/analysis_layout.dart @@ -193,8 +193,10 @@ class AnalysisLayout extends StatelessWidget { children: [ if (engineLines != null) Padding( - padding: const EdgeInsets.all( - kTabletBoardTableSidePadding, + padding: const EdgeInsets.only( + top: kTabletBoardTableSidePadding, + left: kTabletBoardTableSidePadding, + right: kTabletBoardTableSidePadding, ), child: engineLines, ), diff --git a/lib/src/view/analysis/analysis_screen.dart b/lib/src/view/analysis/analysis_screen.dart index 44852fe1a0..45d3ae1f2f 100644 --- a/lib/src/view/analysis/analysis_screen.dart +++ b/lib/src/view/analysis/analysis_screen.dart @@ -171,9 +171,9 @@ class _Body extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final showEvaluationGauge = ref.watch( - analysisPreferencesProvider.select((value) => value.showEvaluationGauge), - ); + final analysisPrefs = ref.watch(analysisPreferencesProvider); + final showEvaluationGauge = analysisPrefs.showEvaluationGauge; + final numEvalLines = analysisPrefs.numEvalLines; final ctrlProvider = analysisControllerProvider(options); final analysisState = ref.watch(ctrlProvider).requireValue; @@ -209,7 +209,7 @@ class _Body extends ConsumerWidget { ); } : null, - engineLines: isEngineAvailable + engineLines: isEngineAvailable && numEvalLines > 0 ? EngineLines( onTapMove: ref.read(ctrlProvider.notifier).onUserMove, clientEval: currentNode.eval, diff --git a/lib/src/view/analysis/opening_explorer_view.dart b/lib/src/view/analysis/opening_explorer_view.dart index c631778fda..9317fcbff2 100644 --- a/lib/src/view/analysis/opening_explorer_view.dart +++ b/lib/src/view/analysis/opening_explorer_view.dart @@ -231,21 +231,13 @@ class _OpeningExplorerView extends StatelessWidget { @override Widget build(BuildContext context) { - final isTablet = isTabletOrLarger(context); final loadingOverlay = Positioned.fill( child: IgnorePointer(ignoring: !isLoading), ); return Stack( children: [ - ListView( - padding: isTablet - ? const EdgeInsets.symmetric( - horizontal: kTabletBoardTableSidePadding, - ) - : EdgeInsets.zero, - children: children, - ), + ListView(children: children), loadingOverlay, ], ); diff --git a/lib/src/view/engine/engine_lines.dart b/lib/src/view/engine/engine_lines.dart index f20f21cf6b..96ffd7fcc1 100644 --- a/lib/src/view/engine/engine_lines.dart +++ b/lib/src/view/engine/engine_lines.dart @@ -55,6 +55,7 @@ class EngineLines extends ConsumerWidget { } return Column( + mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.start, children: content, diff --git a/lib/src/view/study/study_screen.dart b/lib/src/view/study/study_screen.dart index 419c974b64..15dabbc644 100644 --- a/lib/src/view/study/study_screen.dart +++ b/lib/src/view/study/study_screen.dart @@ -208,25 +208,19 @@ class _BodyState extends ConsumerState<_Body> studyControllerProvider(widget.id) .select((state) => state.valueOrNull?.engineGaugeParams), ); + final isComputerAnalysisAllowed = ref.watch( + studyControllerProvider(widget.id) + .select((s) => s.requireValue.isComputerAnalysisAllowed), + ); final currentNode = ref.watch( studyControllerProvider(widget.id) .select((state) => state.requireValue.currentNode), ); - final engineLines = EngineLines( - clientEval: currentNode.eval, - isGameOver: currentNode.position?.isGameOver ?? false, - onTapMove: ref - .read( - studyControllerProvider(widget.id).notifier, - ) - .onUserMove, - ); - - final showEvaluationGauge = ref.watch( - analysisPreferencesProvider.select((value) => value.showEvaluationGauge), - ); + final analysisPrefs = ref.watch(analysisPreferencesProvider); + final showEvaluationGauge = analysisPrefs.showEvaluationGauge; + final numEvalLines = analysisPrefs.numEvalLines; final bottomChild = gamebookActive ? StudyGamebook(widget.id) : StudyTreeView(widget.id); @@ -238,7 +232,9 @@ class _BodyState extends ConsumerState<_Body> boardSize: boardSize, borderRadius: borderRadius, ), - engineGaugeBuilder: showEvaluationGauge && engineGaugeParams != null + engineGaugeBuilder: isComputerAnalysisAllowed && + showEvaluationGauge && + engineGaugeParams != null ? (context, orientation) { return orientation == Orientation.portrait ? EngineGauge( @@ -257,7 +253,17 @@ class _BodyState extends ConsumerState<_Body> ); } : null, - engineLines: engineLines, + engineLines: isComputerAnalysisAllowed && numEvalLines > 0 + ? EngineLines( + clientEval: currentNode.eval, + isGameOver: currentNode.position?.isGameOver ?? false, + onTapMove: ref + .read( + studyControllerProvider(widget.id).notifier, + ) + .onUserMove, + ) + : null, bottomBar: StudyBottomBar(id: widget.id), children: [bottomChild], ); From 920ad84af648ca98d7e2da5041f79099bb228b7b Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Fri, 22 Nov 2024 11:35:07 +0100 Subject: [PATCH 20/23] Fixes --- lib/src/view/analysis/analysis_layout.dart | 7 +- .../view/analysis/opening_explorer_view.dart | 2 - lib/src/view/study/study_screen.dart | 121 +++++++----------- 3 files changed, 54 insertions(+), 76 deletions(-) diff --git a/lib/src/view/analysis/analysis_layout.dart b/lib/src/view/analysis/analysis_layout.dart index c8ead71161..80f76e7595 100644 --- a/lib/src/view/analysis/analysis_layout.dart +++ b/lib/src/view/analysis/analysis_layout.dart @@ -106,11 +106,14 @@ class _AppBarAnalysisTabIndicatorState /// /// The layout is responsive and adapts to the screen size and orientation. /// +/// It includes a [TabBarView] with the [children] widgets. If a [TabController] +/// is not provided, then there must be a [DefaultTabController] ancestor. +/// /// The length of the [children] list must match the [tabController]'s /// [TabController.length] and the length of the [AppBarAnalysisTabIndicator.tabs] class AnalysisLayout extends StatelessWidget { const AnalysisLayout({ - required this.tabController, + this.tabController, required this.boardBuilder, required this.children, this.engineGaugeBuilder, @@ -120,7 +123,7 @@ class AnalysisLayout extends StatelessWidget { }); /// The tab controller for the tab view. - final TabController tabController; + final TabController? tabController; /// The builder for the board widget. final BoardBuilder boardBuilder; diff --git a/lib/src/view/analysis/opening_explorer_view.dart b/lib/src/view/analysis/opening_explorer_view.dart index 9317fcbff2..df320b632a 100644 --- a/lib/src/view/analysis/opening_explorer_view.dart +++ b/lib/src/view/analysis/opening_explorer_view.dart @@ -1,13 +1,11 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:lichess_mobile/src/constants.dart'; import 'package:lichess_mobile/src/model/analysis/analysis_controller.dart'; import 'package:lichess_mobile/src/model/opening_explorer/opening_explorer.dart'; import 'package:lichess_mobile/src/model/opening_explorer/opening_explorer_preferences.dart'; import 'package:lichess_mobile/src/model/opening_explorer/opening_explorer_repository.dart'; import 'package:lichess_mobile/src/network/connectivity.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; -import 'package:lichess_mobile/src/utils/screen.dart'; import 'package:lichess_mobile/src/view/opening_explorer/opening_explorer_widgets.dart'; import 'package:lichess_mobile/src/widgets/feedback.dart'; import 'package:lichess_mobile/src/widgets/shimmer.dart'; diff --git a/lib/src/view/study/study_screen.dart b/lib/src/view/study/study_screen.dart index 15dabbc644..d7cad0abbf 100644 --- a/lib/src/view/study/study_screen.dart +++ b/lib/src/view/study/study_screen.dart @@ -167,7 +167,7 @@ class _StudyChaptersMenu extends ConsumerWidget { } } -class _Body extends ConsumerStatefulWidget { +class _Body extends ConsumerWidget { const _Body({ required this.id, }); @@ -175,46 +175,22 @@ class _Body extends ConsumerStatefulWidget { final StudyId id; @override - ConsumerState<_Body> createState() => _BodyState(); -} - -class _BodyState extends ConsumerState<_Body> - with SingleTickerProviderStateMixin { - late final TabController _tabController; - - @override - void initState() { - super.initState(); - - _tabController = TabController( - vsync: this, - length: 1, - ); - } - - @override - void dispose() { - _tabController.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { final gamebookActive = ref.watch( - studyControllerProvider(widget.id) + studyControllerProvider(id) .select((state) => state.requireValue.gamebookActive), ); final engineGaugeParams = ref.watch( - studyControllerProvider(widget.id) + studyControllerProvider(id) .select((state) => state.valueOrNull?.engineGaugeParams), ); final isComputerAnalysisAllowed = ref.watch( - studyControllerProvider(widget.id) + studyControllerProvider(id) .select((s) => s.requireValue.isComputerAnalysisAllowed), ); final currentNode = ref.watch( - studyControllerProvider(widget.id) + studyControllerProvider(id) .select((state) => state.requireValue.currentNode), ); @@ -222,50 +198,51 @@ class _BodyState extends ConsumerState<_Body> final showEvaluationGauge = analysisPrefs.showEvaluationGauge; final numEvalLines = analysisPrefs.numEvalLines; - final bottomChild = - gamebookActive ? StudyGamebook(widget.id) : StudyTreeView(widget.id); + final bottomChild = gamebookActive ? StudyGamebook(id) : StudyTreeView(id); - return AnalysisLayout( - tabController: _tabController, - boardBuilder: (context, boardSize, borderRadius) => _StudyBoard( - id: widget.id, - boardSize: boardSize, - borderRadius: borderRadius, - ), - engineGaugeBuilder: isComputerAnalysisAllowed && - showEvaluationGauge && - engineGaugeParams != null - ? (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, + return DefaultTabController( + length: 1, + child: AnalysisLayout( + boardBuilder: (context, boardSize, borderRadius) => _StudyBoard( + id: id, + boardSize: boardSize, + borderRadius: borderRadius, + ), + engineGaugeBuilder: isComputerAnalysisAllowed && + showEvaluationGauge && + engineGaugeParams != null + ? (context, orientation) { + return orientation == Orientation.portrait + ? EngineGauge( + displayMode: EngineGaugeDisplayMode.horizontal, params: engineGaugeParams, - ), - ); - } - : null, - engineLines: isComputerAnalysisAllowed && numEvalLines > 0 - ? EngineLines( - clientEval: currentNode.eval, - isGameOver: currentNode.position?.isGameOver ?? false, - onTapMove: ref - .read( - studyControllerProvider(widget.id).notifier, - ) - .onUserMove, - ) - : null, - bottomBar: StudyBottomBar(id: widget.id), - children: [bottomChild], + ) + : Container( + clipBehavior: Clip.hardEdge, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(4.0), + ), + child: EngineGauge( + displayMode: EngineGaugeDisplayMode.vertical, + params: engineGaugeParams, + ), + ); + } + : null, + engineLines: isComputerAnalysisAllowed && numEvalLines > 0 + ? EngineLines( + clientEval: currentNode.eval, + isGameOver: currentNode.position?.isGameOver ?? false, + onTapMove: ref + .read( + studyControllerProvider(id).notifier, + ) + .onUserMove, + ) + : null, + bottomBar: StudyBottomBar(id: id), + children: [bottomChild], + ), ); } } From 2e5dee402aeee21259129c9120c3b1640429a723 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Fri, 22 Nov 2024 11:38:04 +0100 Subject: [PATCH 21/23] Don't make final --- lib/src/model/analysis/analysis_controller.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/src/model/analysis/analysis_controller.dart b/lib/src/model/analysis/analysis_controller.dart index d12a48cae1..91b36e52de 100644 --- a/lib/src/model/analysis/analysis_controller.dart +++ b/lib/src/model/analysis/analysis_controller.dart @@ -53,8 +53,8 @@ class AnalysisOptions with _$AnalysisOptions { @riverpod class AnalysisController extends _$AnalysisController implements PgnTreeNotifier { - late final Root _root; - late final Variant _variant; + late Root _root; + late Variant _variant; final _engineEvalDebounce = Debouncer(const Duration(milliseconds: 150)); From c3a96fe706f91ba08b1ae4d5d5d7bb1c384b965f Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Fri, 22 Nov 2024 11:52:25 +0100 Subject: [PATCH 22/23] Archived game bottom bar fixes --- lib/src/view/game/archived_game_screen.dart | 32 +++++++++------------ 1 file changed, 14 insertions(+), 18 deletions(-) diff --git a/lib/src/view/game/archived_game_screen.dart b/lib/src/view/game/archived_game_screen.dart index c7ef8b358d..88b720a6f1 100644 --- a/lib/src/view/game/archived_game_screen.dart +++ b/lib/src/view/game/archived_game_screen.dart @@ -354,26 +354,22 @@ class _BottomBar extends ConsumerWidget { onTap: showGameMenu, icon: Icons.menu, ), - gameCursor.when( - data: (data) { - return BottomBarButton( - label: context.l10n.mobileShowResult, - icon: Icons.info_outline, - onTap: () { - showAdaptiveDialog( - context: context, - builder: (context) => ArchivedGameResultDialog(game: data.$1), - barrierDismissible: true, - ); - }, - ); - }, - loading: () => const SizedBox.shrink(), - error: (_, __) => const SizedBox.shrink(), - ), + if (gameCursor.hasValue) + BottomBarButton( + label: context.l10n.mobileShowResult, + icon: Icons.info_outline, + onTap: () { + showAdaptiveDialog( + context: context, + builder: (context) => + ArchivedGameResultDialog(game: gameCursor.requireValue.$1), + barrierDismissible: true, + ); + }, + ), BottomBarButton( label: context.l10n.gameAnalysis, - onTap: ref.read(gameCursorProvider(gameData.id)).hasValue + onTap: gameCursor.hasValue ? () { final cursor = gameCursor.requireValue.$2; pushPlatformRoute( From befa759c73b8fad4ed5289399ff9e59a10699173 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Fri, 22 Nov 2024 11:58:51 +0100 Subject: [PATCH 23/23] Pgn display fixes --- lib/src/widgets/pgn.dart | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/lib/src/widgets/pgn.dart b/lib/src/widgets/pgn.dart index 8781cf9bc0..0dff2207db 100644 --- a/lib/src/widgets/pgn.dart +++ b/lib/src/widgets/pgn.dart @@ -256,7 +256,8 @@ typedef _PgnTreeViewParams = ({ PgnTreeNotifier notifier, }); -IList _computerPrefAwareChildren( +/// Filter node children when computer analysis is disabled +IList _filteredChildren( ViewNode node, bool withComputerAnalysis, ) { @@ -278,8 +279,7 @@ bool _displaySideLineAsInline(ViewBranch node, [int depth = 0]) { /// Returns whether this node has a sideline that should not be displayed inline. bool _hasNonInlineSideLine(ViewNode node, _PgnTreeViewParams params) { - final children = - _computerPrefAwareChildren(node, params.withComputerAnalysis); + final children = _filteredChildren(node, params.withComputerAnalysis); return children.length > 2 || (children.length == 2 && !_displaySideLineAsInline(children[1])); } @@ -507,7 +507,9 @@ List _buildInlineSideLine({ node, lineInfo: ( type: _LineType.inlineSideline, - startLine: i == 0 || sidelineNodes[i - 1].hasTextComment, + startLine: i == 0 || + (params.shouldShowComments && + sidelineNodes[i - 1].hasTextComment), pathToLine: initialPath, ), pathToNode: pathToNode, @@ -628,7 +630,7 @@ class _SideLinePart extends ConsumerWidget { node.children.first, lineInfo: ( type: _LineType.sideline, - startLine: node.hasTextComment, + startLine: params.shouldShowComments && node.hasTextComment, pathToLine: initialPath, ), pathToNode: path, @@ -693,13 +695,12 @@ class _MainLinePart extends ConsumerWidget { TextSpan( children: nodes .takeWhile( - (node) => - _computerPrefAwareChildren(node, params.withComputerAnalysis) - .isNotEmpty, + (node) => _filteredChildren(node, params.withComputerAnalysis) + .isNotEmpty, ) .mapIndexed( (i, node) { - final children = _computerPrefAwareChildren( + final children = _filteredChildren( node, params.withComputerAnalysis, );