diff --git a/lib/src/model/puzzle/puzzle_providers.dart b/lib/src/model/puzzle/puzzle_providers.dart index 9c1efb1643..ad7a5988af 100644 --- a/lib/src/model/puzzle/puzzle_providers.dart +++ b/lib/src/model/puzzle/puzzle_providers.dart @@ -3,6 +3,7 @@ import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart' hide Tuple2; +import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/auth/auth_controller.dart'; import 'package:lichess_mobile/src/model/puzzle/puzzle.dart'; import 'package:lichess_mobile/src/model/puzzle/puzzle_theme.dart'; @@ -32,6 +33,13 @@ Future streak(StreakRef ref) { return Result.release(repo.streak()); } +// TODO when history database is available should first try to fetch from there +@Riverpod(keepAlive: true) +Future puzzle(PuzzleRef ref, PuzzleId id) { + final repo = ref.watch(puzzleRepositoryProvider); + return Result.release(repo.fetch(id)); +} + @Riverpod(keepAlive: true) Future dailyPuzzle(DailyPuzzleRef ref) { final repo = ref.watch(puzzleRepositoryProvider); diff --git a/lib/src/ui/puzzle/puzzle_screen.dart b/lib/src/ui/puzzle/puzzle_screen.dart index a8d9363aa8..15e8817111 100644 --- a/lib/src/ui/puzzle/puzzle_screen.dart +++ b/lib/src/ui/puzzle/puzzle_screen.dart @@ -352,7 +352,7 @@ class _BottomBar extends ConsumerWidget { puzzleState.nextContext != null ? () => ref .read(viewModelProvider.notifier) - .continueWithNextPuzzle(puzzleState.nextContext!) + .loadPuzzle(puzzleState.nextContext!) : null, highlighted: true, label: context.l10n.puzzleContinueTraining, @@ -419,7 +419,7 @@ class _DifficultySelector extends ConsumerWidget { if (context.mounted && nextContext != null) { ref .read(viewModelProvider.notifier) - .continueWithNextPuzzle(nextContext); + .loadPuzzle(nextContext); } }, ); diff --git a/lib/src/ui/puzzle/puzzle_session_widget.dart b/lib/src/ui/puzzle/puzzle_session_widget.dart index f0d4beea24..7bbbeb5563 100644 --- a/lib/src/ui/puzzle/puzzle_session_widget.dart +++ b/lib/src/ui/puzzle/puzzle_session_widget.dart @@ -4,8 +4,10 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/widgets/table_board_layout.dart'; +import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/settings/brightness.dart'; import 'package:lichess_mobile/src/styles/lichess_colors.dart'; +import 'package:lichess_mobile/src/model/puzzle/puzzle_providers.dart'; import 'package:lichess_mobile/src/model/puzzle/puzzle_session.dart'; import 'package:lichess_mobile/src/model/puzzle/puzzle_service.dart'; @@ -27,6 +29,7 @@ class PuzzleSessionWidget extends ConsumerStatefulWidget { class PuzzleSessionWidgetState extends ConsumerState { final lastAttemptKey = GlobalKey(); + PuzzleId? loadingPuzzleId; @override void initState() { @@ -94,13 +97,42 @@ class PuzzleSessionWidgetState extends ConsumerState { for (final attempt in session.attempts) _SessionItem( isCurrent: attempt.id == puzzleState.puzzle.puzzle.id, + isLoading: loadingPuzzleId == attempt.id, brightness: brightness, attempt: attempt, + onTap: puzzleState.puzzle.puzzle.id != attempt.id && + loadingPuzzleId == null + ? (id) async { + final provider = puzzleProvider(id); + setState(() { + loadingPuzzleId = id; + }); + try { + final puzzle = await ref.read(provider.future); + final nextContext = PuzzleContext( + userId: widget.initialPuzzleContext.userId, + theme: widget.initialPuzzleContext.theme, + puzzle: puzzle, + ); + + ref + .read(widget.viewModelProvider.notifier) + .loadPuzzle(nextContext); + } finally { + if (mounted) { + setState(() { + loadingPuzzleId = null; + }); + } + } + } + : null, ), if (puzzleState.mode == PuzzleMode.view || currentAttempt == null) _SessionItem( isCurrent: currentAttempt == null, + isLoading: false, brightness: brightness, key: lastAttemptKey, ), @@ -118,13 +150,17 @@ class _SessionItem extends StatelessWidget { const _SessionItem({ this.attempt, required this.isCurrent, + required this.isLoading, required this.brightness, + this.onTap, super.key, }); final bool isCurrent; + final bool isLoading; final PuzzleAttempt? attempt; final Brightness brightness; + final void Function(PuzzleId id)? onTap; Color get good => brightness == Brightness.light ? LichessColors.good.shade300 @@ -142,44 +178,57 @@ class _SessionItem extends StatelessWidget { @override Widget build(BuildContext context) { - return Container( - width: 38, - height: 26, - padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 8), - decoration: BoxDecoration( - color: isCurrent - ? Colors.grey - : attempt != null - ? attempt!.win - ? good - : error - : next, - borderRadius: const BorderRadius.all(Radius.circular(5)), - ), - child: attempt?.ratingDiff != null && attempt!.ratingDiff != 0 - ? Padding( - padding: const EdgeInsets.all(2.0), - child: FittedBox( - fit: BoxFit.cover, - child: Text( - attempt!.ratingDiffString!, - maxLines: 1, - style: const TextStyle( - color: Colors.white, - height: 1, + return GestureDetector( + onTap: attempt != null ? () => onTap?.call(attempt!.id) : null, + child: Container( + width: 38, + height: 26, + padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 8), + decoration: BoxDecoration( + color: isCurrent + ? Colors.grey + : attempt != null + ? attempt!.win + ? good + : error + : next, + borderRadius: const BorderRadius.all(Radius.circular(5)), + ), + child: isLoading + ? const Padding( + padding: EdgeInsets.all(2.0), + child: FittedBox( + fit: BoxFit.cover, + child: CircularProgressIndicator.adaptive( + backgroundColor: Colors.white, ), ), - ), - ) - : Icon( - attempt != null - ? attempt!.win - ? Icons.check - : Icons.close - : null, - color: Colors.white, - size: 18, - ), + ) + : attempt?.ratingDiff != null && attempt!.ratingDiff != 0 + ? Padding( + padding: const EdgeInsets.all(2.0), + child: FittedBox( + fit: BoxFit.cover, + child: Text( + attempt!.ratingDiffString!, + maxLines: 1, + style: const TextStyle( + color: Colors.white, + height: 1, + ), + ), + ), + ) + : Icon( + attempt != null + ? attempt!.win + ? Icons.check + : Icons.close + : null, + color: Colors.white, + size: 18, + ), + ), ); } } diff --git a/lib/src/ui/puzzle/puzzle_streak_screen.dart b/lib/src/ui/puzzle/puzzle_streak_screen.dart index c07a3fa8d9..c226a1619d 100644 --- a/lib/src/ui/puzzle/puzzle_streak_screen.dart +++ b/lib/src/ui/puzzle/puzzle_streak_screen.dart @@ -389,7 +389,7 @@ class _RetryFetchPuzzleDialog extends ConsumerWidget { Navigator.of(context).pop(); } if (data != null) { - ref.read(viewModelProvider.notifier).continueWithNextPuzzle(data); + ref.read(viewModelProvider.notifier).loadPuzzle(data); } }, ); diff --git a/lib/src/ui/puzzle/puzzle_view_model.dart b/lib/src/ui/puzzle/puzzle_view_model.dart index 2fc30e4f72..2d71f69688 100644 --- a/lib/src/ui/puzzle/puzzle_view_model.dart +++ b/lib/src/ui/puzzle/puzzle_view_model.dart @@ -197,7 +197,7 @@ class PuzzleViewModel extends _$PuzzleViewModel { return nextPuzzle; } - void continueWithNextPuzzle(PuzzleContext nextContext) { + void loadPuzzle(PuzzleContext nextContext) { state = _loadNewContext(nextContext, state.streak); } @@ -338,7 +338,7 @@ class PuzzleViewModel extends _$PuzzleViewModel { if (nextContext != null) { await Future.delayed(const Duration(milliseconds: 250)); soundService.play(Sound.confirmation); - continueWithNextPuzzle(nextContext); + loadPuzzle(nextContext); } else { // no more puzzle state = state.copyWith.streak!(