diff --git a/lib/src/model/puzzle/puzzle_opening.dart b/lib/src/model/puzzle/puzzle_opening.dart index e06543749..4972d5628 100644 --- a/lib/src/model/puzzle/puzzle_opening.dart +++ b/lib/src/model/puzzle/puzzle_opening.dart @@ -2,6 +2,8 @@ import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/model/puzzle/puzzle_providers.dart'; +enum PuzzleOpeningSort { popular, alphabetical } + typedef PuzzleOpeningFamily = ({ String key, String name, @@ -15,7 +17,7 @@ typedef PuzzleOpeningData = ({String key, String name, int count}); final flatOpeningsListProvider = FutureProvider.autoDispose>(( Ref ref, ) async { - final families = await ref.watch(puzzleOpeningsProvider.future); + final families = await ref.watch(puzzleOpeningsProvider(PuzzleOpeningSort.popular).future); return families .map( (f) => [ diff --git a/lib/src/model/puzzle/puzzle_providers.dart b/lib/src/model/puzzle/puzzle_providers.dart index 1ebe6027d..b45a24b57 100644 --- a/lib/src/model/puzzle/puzzle_providers.dart +++ b/lib/src/model/puzzle/puzzle_providers.dart @@ -135,9 +135,12 @@ final puzzleThemesProvider = FutureProvider.autoDispose>((Ref ref) { - return ref.withClientCacheFor( - (client) => PuzzleRepository(client).puzzleOpenings(), - const Duration(days: 1), - ); -}, name: 'PuzzleOpeningsProvider'); +final puzzleOpeningsProvider = FutureProvider.autoDispose + .family, PuzzleOpeningSort>((Ref ref, PuzzleOpeningSort sort) { + return ref.withClientCacheFor( + (client) => PuzzleRepository( + client, + ).puzzleOpenings(alphabetical: sort == PuzzleOpeningSort.alphabetical), + const Duration(days: 1), + ); + }, name: 'PuzzleOpeningsProvider'); diff --git a/lib/src/model/puzzle/puzzle_repository.dart b/lib/src/model/puzzle/puzzle_repository.dart index 39355caab..e6dc937f1 100644 --- a/lib/src/model/puzzle/puzzle_repository.dart +++ b/lib/src/model/puzzle/puzzle_repository.dart @@ -170,12 +170,14 @@ class PuzzleRepository { ); } - Future> puzzleOpenings() { - return client.readJson( + Future> puzzleOpenings({bool alphabetical = false}) async { + final result = await client.readJson( Uri(path: '/training/openings'), headers: {'Accept': 'application/json'}, mapper: _puzzleOpeningFromJson, ); + if (!alphabetical) return result; + return result.sort((a, b) => a.name.compareTo(b.name)); } Future> puzzleReplay(int days, String theme) { diff --git a/lib/src/view/puzzle/opening_screen.dart b/lib/src/view/puzzle/opening_screen.dart index fd8b9fbe6..826a968a1 100644 --- a/lib/src/view/puzzle/opening_screen.dart +++ b/lib/src/view/puzzle/opening_screen.dart @@ -1,6 +1,7 @@ import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_riverpod/legacy.dart'; import 'package:lichess_mobile/src/model/puzzle/puzzle_angle.dart'; import 'package:lichess_mobile/src/model/puzzle/puzzle_opening.dart'; import 'package:lichess_mobile/src/model/puzzle/puzzle_providers.dart'; @@ -11,21 +12,29 @@ import 'package:lichess_mobile/src/utils/navigation.dart'; import 'package:lichess_mobile/src/view/puzzle/puzzle_screen.dart'; import 'package:lichess_mobile/src/widgets/list.dart'; import 'package:lichess_mobile/src/widgets/platform.dart'; +import 'package:lichess_mobile/src/widgets/platform_context_menu_button.dart'; -final _openingsProvider = - FutureProvider.autoDispose<(bool, IMap, IList?)>((ref) async { - final isOnline = await ref.watch(onlineStatusProvider.future); +final _openingsSortProvider = StateProvider.autoDispose( + (ref) => PuzzleOpeningSort.popular, +); + +final _openingsProvider = FutureProvider.autoDispose + .family<(bool, IMap, IList?), PuzzleOpeningSort>(( + ref, + sort, + ) async { + final connectivityStatus = await ref.watch(connectivityChangesProvider.future); final savedOpenings = await ref.watch(savedOpeningBatchesProvider.future); IList? onlineOpenings; try { - onlineOpenings = await ref.watch(puzzleOpeningsProvider.future); + onlineOpenings = await ref.watch(puzzleOpeningsProvider(sort).future); } catch (e) { onlineOpenings = null; } - return (isOnline, savedOpenings, onlineOpenings); + return (connectivityStatus.isOnline, savedOpenings, onlineOpenings); }); -class OpeningThemeScreen extends StatelessWidget { +class OpeningThemeScreen extends ConsumerWidget { const OpeningThemeScreen({super.key}); static Route buildRoute() { @@ -33,9 +42,35 @@ class OpeningThemeScreen extends StatelessWidget { } @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { + final sort = ref.watch(_openingsSortProvider); return PlatformScaffold( - appBar: PlatformAppBar(title: Text(context.l10n.puzzlePuzzlesByOpenings)), + appBar: PlatformAppBar( + title: Text(context.l10n.puzzlePuzzlesByOpenings), + actions: [ + ContextMenuIconButton( + consumeOutsideTap: true, + icon: const Icon(Icons.sort_outlined), + semanticsLabel: 'Sort openings', + actions: [ + ContextMenuAction( + icon: sort == PuzzleOpeningSort.popular ? Icons.check : null, + label: context.l10n.popularOpenings, + onPressed: () { + ref.read(_openingsSortProvider.notifier).state = PuzzleOpeningSort.popular; + }, + ), + ContextMenuAction( + icon: sort == PuzzleOpeningSort.alphabetical ? Icons.check : null, + label: 'Alphabetical', + onPressed: () { + ref.read(_openingsSortProvider.notifier).state = PuzzleOpeningSort.alphabetical; + }, + ), + ], + ), + ], + ), body: const _Body(), ); } @@ -46,7 +81,8 @@ class _Body extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final openings = ref.watch(_openingsProvider); + final sort = ref.watch(_openingsSortProvider); + final openings = ref.watch(_openingsProvider(sort)); return openings.when( data: (data) { final (isOnline, savedOpenings, onlineOpenings) = data;