Add alphabetical sort toggle to puzzle openings screen (#3045)

Fixes #2981

- Add sort toggle button in app bar (popular ↔ alphabetical)
- Sort is done client-side as the server endpoint does not support order param
- puzzleOpeningsProvider is now a family provider keyed by sort order
- Both orders are cached separately for 1 day
This commit is contained in:
adharsh
2026-05-19 14:06:49 +05:30
committed by GitHub
parent bc122fa9d9
commit bedabb0c19
4 changed files with 61 additions and 18 deletions
+3 -1
View File
@@ -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<IList<PuzzleOpeningData>>((
Ref ref,
) async {
final families = await ref.watch(puzzleOpeningsProvider.future);
final families = await ref.watch(puzzleOpeningsProvider(PuzzleOpeningSort.popular).future);
return families
.map(
(f) => [
+9 -6
View File
@@ -135,9 +135,12 @@ final puzzleThemesProvider = FutureProvider.autoDispose<IMap<PuzzleThemeKey, Puz
}, name: 'PuzzleThemesProvider');
/// Fetches available puzzle openings.
final puzzleOpeningsProvider = FutureProvider.autoDispose<IList<PuzzleOpeningFamily>>((Ref ref) {
return ref.withClientCacheFor(
(client) => PuzzleRepository(client).puzzleOpenings(),
const Duration(days: 1),
);
}, name: 'PuzzleOpeningsProvider');
final puzzleOpeningsProvider = FutureProvider.autoDispose
.family<IList<PuzzleOpeningFamily>, PuzzleOpeningSort>((Ref ref, PuzzleOpeningSort sort) {
return ref.withClientCacheFor(
(client) => PuzzleRepository(
client,
).puzzleOpenings(alphabetical: sort == PuzzleOpeningSort.alphabetical),
const Duration(days: 1),
);
}, name: 'PuzzleOpeningsProvider');
+4 -2
View File
@@ -170,12 +170,14 @@ class PuzzleRepository {
);
}
Future<IList<PuzzleOpeningFamily>> puzzleOpenings() {
return client.readJson(
Future<IList<PuzzleOpeningFamily>> 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<IList<PuzzleId>> puzzleReplay(int days, String theme) {
+45 -9
View File
@@ -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<String, int>, IList<PuzzleOpeningFamily>?)>((ref) async {
final isOnline = await ref.watch(onlineStatusProvider.future);
final _openingsSortProvider = StateProvider.autoDispose<PuzzleOpeningSort>(
(ref) => PuzzleOpeningSort.popular,
);
final _openingsProvider = FutureProvider.autoDispose
.family<(bool, IMap<String, int>, IList<PuzzleOpeningFamily>?), PuzzleOpeningSort>((
ref,
sort,
) async {
final connectivityStatus = await ref.watch(connectivityChangesProvider.future);
final savedOpenings = await ref.watch(savedOpeningBatchesProvider.future);
IList<PuzzleOpeningFamily>? 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<dynamic> 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;