From cc30a898bfb0f32cc7f189d7ac7f81fcc7e4d8f2 Mon Sep 17 00:00:00 2001 From: tom-anders <13141438+tom-anders@users.noreply.github.com> Date: Tue, 8 Oct 2024 23:40:50 +0200 Subject: [PATCH 1/7] add models to get study as JSON and PGN --- lib/src/model/study/study.dart | 105 ++++++++++++++++++++++ lib/src/model/study/study_repository.dart | 30 +++++++ 2 files changed, 135 insertions(+) diff --git a/lib/src/model/study/study.dart b/lib/src/model/study/study.dart index 8ff3fa9c1a..d1de222a78 100644 --- a/lib/src/model/study/study.dart +++ b/lib/src/model/study/study.dart @@ -1,11 +1,116 @@ +import 'package:collection/collection.dart'; +import 'package:dartchess/dartchess.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:freezed_annotation/freezed_annotation.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/user/user.dart'; part 'study.freezed.dart'; part 'study.g.dart'; +@Freezed(fromJson: true) +class Study with _$Study { + const Study._(); + + const factory Study({ + required StudyId id, + required String name, + required ({StudyChapterId chapterId, String path}) position, + required bool liked, + required int likes, + required UserId? ownerId, + @JsonKey(fromJson: studyFeaturesFromJson) required StudyFeatures features, + required IList topics, + required IList chapters, + required StudyChapter chapter, + }) = _Study; + + StudyChapterMeta get currentChapterMeta => + chapters.firstWhere((c) => c.id == chapter.id); + + factory Study.fromJson(Map json) => _$StudyFromJson(json); +} + +typedef StudyFeatures = ({ + bool cloneable, + bool chat, + bool sticky, +}); + +StudyFeatures studyFeaturesFromJson(Map json) { + return ( + cloneable: json['cloneable'] as bool? ?? false, + chat: json['chat'] as bool? ?? false, + sticky: json['sticky'] as bool? ?? false, + ); +} + +@Freezed(fromJson: true) +class StudyChapter with _$StudyChapter { + const StudyChapter._(); + + const factory StudyChapter({ + required StudyChapterId id, + required StudyChapterSetup setup, + @JsonKey(defaultValue: false) required bool practise, + required int? conceal, + @JsonKey(defaultValue: false) required bool gamebook, + @JsonKey(fromJson: studyChapterFeaturesFromJson) + required StudyChapterFeatures features, + }) = _StudyChapter; + + factory StudyChapter.fromJson(Map json) => + _$StudyChapterFromJson(json); +} + +typedef StudyChapterFeatures = ({ + bool computer, + bool explorer, +}); + +StudyChapterFeatures studyChapterFeaturesFromJson(Map json) { + return ( + computer: json['computer'] as bool? ?? false, + explorer: json['explorer'] as bool? ?? false, + ); +} + +@Freezed(fromJson: true) +class StudyChapterSetup with _$StudyChapterSetup { + const StudyChapterSetup._(); + + const factory StudyChapterSetup({ + required GameId? id, + required Side orientation, + @JsonKey(fromJson: _variantFromJson) required Variant variant, + required bool? fromFen, + }) = _StudyChapterSetup; + + factory StudyChapterSetup.fromJson(Map json) => + _$StudyChapterSetupFromJson(json); +} + +Variant _variantFromJson(Map json) { + return Variant.values.firstWhereOrNull( + (v) => v.name == json['key'], + )!; +} + +@Freezed(fromJson: true) +class StudyChapterMeta with _$StudyChapterMeta { + const StudyChapterMeta._(); + + const factory StudyChapterMeta({ + required StudyChapterId id, + required String name, + required String? fen, + }) = _StudyChapterMeta; + + factory StudyChapterMeta.fromJson(Map json) => + _$StudyChapterMetaFromJson(json); +} + @Freezed(fromJson: true) class StudyPageData with _$StudyPageData { const StudyPageData._(); diff --git a/lib/src/model/study/study_repository.dart b/lib/src/model/study/study_repository.dart index b0e6b84461..5b479a7ef7 100644 --- a/lib/src/model/study/study_repository.dart +++ b/lib/src/model/study/study_repository.dart @@ -1,6 +1,9 @@ +import 'dart:convert'; + import 'package:deep_pick/deep_pick.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:http/http.dart'; +import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/study/study.dart'; import 'package:lichess_mobile/src/model/study/study_filter.dart'; import 'package:lichess_mobile/src/network/http.dart'; @@ -53,4 +56,31 @@ class StudyRepository { }, ); } + + Future<(Study study, String pgn)> getStudy({ + required StudyId id, + StudyChapterId? chapterId, + }) async { + final study = await client.readJson( + Uri( + path: (chapterId != null) ? '/study/$id/$chapterId' : '/study/$id', + queryParameters: { + 'chapters': '1', + }, + ), + headers: {'Accept': 'application/json'}, + mapper: (Map json) { + return Study.fromJson( + pick(json, 'study').asMapOrThrow(), + ); + }, + ); + + final pgnBytes = await client.readBytes( + Uri(path: '/api/study/$id/${chapterId ?? study.chapter.id}.pgn'), + headers: {'Accept': 'application/x-chess-pgn'}, + ); + + return (study, utf8.decode(pgnBytes)); + } } From 8b324a33ee4b485fe9cbeb2dea7a826c63cddbf8 Mon Sep 17 00:00:00 2001 From: tom-anders <13141438+tom-anders@users.noreply.github.com> Date: Sun, 20 Oct 2024 22:31:14 +0200 Subject: [PATCH 2/7] add tests for study json parsing --- test/model/study/study_repository_test.dart | 443 ++++++++++++++++++++ 1 file changed, 443 insertions(+) create mode 100644 test/model/study/study_repository_test.dart diff --git a/test/model/study/study_repository_test.dart b/test/model/study/study_repository_test.dart new file mode 100644 index 0000000000..2a120dcd5f --- /dev/null +++ b/test/model/study/study_repository_test.dart @@ -0,0 +1,443 @@ +import 'package:dartchess/dartchess.dart'; +import 'package:fast_immutable_collections/fast_immutable_collections.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:http/testing.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/study/study.dart'; +import 'package:lichess_mobile/src/model/study/study_repository.dart'; + +import '../../test_helpers.dart'; + +void main() { + group('StudyRepository.getStudy', () { + test('correctly parse study JSON', () async { + // curl -X GET https://lichess.org/study/JbWtuaeK/7OJXp679\?chapters\=1 -H "Accept: application/json" | sed "s/\\\\n/ /g" | jq 'del(.study.chat)' + const response = ''' +{ + "study": { + "id": "JbWtuaeK", + "name": "How to Solve Puzzles Correctly", + "members": { + "kyle-and-jess": { + "user": { + "name": "Kyle-and-Jess", + "flair": "nature.chipmunk", + "id": "kyle-and-jess" + }, + "role": "w" + }, + "jessieu726": { + "user": { + "name": "jessieu726", + "flair": "nature.duck", + "id": "jessieu726" + }, + "role": "w" + }, + "kyle11878": { + "user": { + "name": "kyle11878", + "flair": "activity.lichess-horsey", + "id": "kyle11878" + }, + "role": "w" + } + }, + "position": { + "chapterId": "EgqyeQIp", + "path": "" + }, + "ownerId": "kyle-and-jess", + "settings": { + "explorer": "contributor", + "description": false, + "computer": "contributor", + "chat": "everyone", + "sticky": false, + "shareable": "contributor", + "cloneable": "contributor" + }, + "visibility": "public", + "createdAt": 1729286237789, + "secondsSinceUpdate": 4116, + "from": "scratch", + "likes": 29, + "flair": "activity.puzzle-piece", + "liked": false, + "features": { + "cloneable": false, + "shareable": false, + "chat": true + }, + "topics": [], + "chapter": { + "id": "7OJXp679", + "ownerId": "kyle-and-jess", + "setup": { + "variant": { + "key": "standard", + "name": "Standard" + }, + "orientation": "black", + "fromFen": true + }, + "tags": [], + "features": { + "computer": false, + "explorer": false + }, + "gamebook": true + }, + "chapters": [ + { + "id": "EgqyeQIp", + "name": "Introduction" + }, + { + "id": "z6tGV47W", + "name": "Practice Your Thought Process", + "fen": "2k4r/p1p2p2/1p2b2p/1Pqn2r1/2B5/B1PP4/P4PPP/RN2Q1K1 b - - 6 20", + "orientation": "black" + }, + { + "id": "dTfxbccx", + "name": "Practice Strategic Thinking", + "fen": "r3r1k1/1b2b2p/pq4pB/1p3pN1/2p5/2P5/PPn1QPPP/3RR1K1 w - - 0 23" + }, + { + "id": "B1U4pFdG", + "name": "Calculate Fully", + "fen": "3r3r/1Rpk1p2/2p2q1p/Q2pp3/P2PP1n1/2P1B1Pp/5P2/1N3RK1 b - - 2 26", + "orientation": "black" + }, + { + "id": "NJLW7jil", + "name": "Calculate Freely", + "fen": "4k3/8/6p1/R1p1r1n1/P3Pp2/2N2r2/1PP1K1R1/8 b - - 2 39", + "orientation": "black" + }, + { + "id": "7OJXp679", + "name": "Use a Timer", + "fen": "r5k1/ppp2ppp/7r/4Nb2/3P4/1QN1PPq1/PP2B1P1/R4RK1 b - - 1 20", + "orientation": "black" + }, + { + "id": "Rgk6UlTP", + "name": "Understand Your Mistakes", + "fen": "r4rk1/1R3pb1/pR2N1p1/2q5/4p3/2P1P1Pp/Q2P1P1P/6K1 b - - 1 26", + "orientation": "black" + }, + { + "id": "VsdxmjCf", + "name": "Adjusting Difficulty", + "fen": "3r4/k1pq1p1r/pp1p2p1/8/3P4/P1P2BP1/1P1N1Pp1/R3R1K1 b - - 0 1", + "orientation": "black" + }, + { + "id": "FHU6xhYs", + "name": "Using Themes", + "fen": "r2k3N/pbpp1Bpp/1p6/2b1p3/3n3q/P7/1PPP1RPP/RNB2QK1 b - - 3 12", + "orientation": "black" + }, + { + "id": "8FhO455h", + "name": "Endurance Training", + "fen": "8/1p5k/2qPQ2p/p5p1/5r1n/2B4P/5P2/4R1K1 w - - 3 41" + }, + { + "id": "jWUEWsEf", + "name": "Final Thoughts", + "fen": "8/1PP2PP1/PppPPppP/Pp1pp1pP/Pp4pP/1Pp2pP1/2PppP2/3PP3 w - - 0 1" + } + ], + "federations": {} + }, + "analysis": { + "game": { + "id": "synthetic", + "variant": { + "key": "standard", + "name": "Standard", + "short": "Std" + }, + "opening": null, + "fen": "r5k1/ppp2ppp/7r/4Nb2/3P4/1QN1PPq1/PP2B1P1/R4RK1 b - - 1 20", + "turns": 39, + "player": "black", + "status": { + "id": 10, + "name": "created" + }, + "initialFen": "r5k1/ppp2ppp/7r/4Nb2/3P4/1QN1PPq1/PP2B1P1/R4RK1 b - - 1 20" + }, + "player": { + "id": null, + "color": "black" + }, + "opponent": { + "color": "white", + "ai": null + }, + "orientation": "black", + "pref": { + "animationDuration": 300, + "coords": 1, + "moveEvent": 2, + "showCaptured": true, + "keyboardMove": false, + "rookCastle": true, + "highlight": true, + "destination": true + }, + "userAnalysis": true, + "treeParts": [ + { + "ply": 39, + "fen": "r5k1/ppp2ppp/7r/4Nb2/3P4/1QN1PPq1/PP2B1P1/R4RK1 b - - 1 20", + "comments": [ + { + "id": "4nZ6", + "text": "Using a timer can be great during puzzle solving, and I don't mean timing yourself to solve quickly. What I mean is setting a timer that restricts when you're allowed to play a move. Start with a minute or two (for more difficult puzzles; if you're solving easy puzzles, you don't need the timer) and calculate the entire time. When you're solving even harder puzzles, set an even longer timer (5-10 minutes maybe). Practice pushing calculations further and looking at different lines during that time (for very difficult puzzles, you should have plenty to calculate). This is to train yourself to take time during important moments, instead of rushing through the position. Set a timer for one to two minutes and calculate this position as black as fully as you can.", + "by": { + "id": "kyle-and-jess", + "name": "Kyle-and-Jess" + } + } + ], + "gamebook": { + "hint": "The white king is not very safe. Can black increase the pressure on the king?" + }, + "dests": "456789 LbktxCESUZ6 wenopvxDEFKMU WGO YIQ 2MU XHP VhpxFNOPQRSTU !9?" + }, + { + "ply": 40, + "fen": "r5k1/ppp2ppp/8/4Nb2/3P4/1QN1PPq1/PP2B1Pr/R4RK1 w - - 2 21", + "id": "R2", + "uci": "h6h2", + "san": "Rh2", + "gamebook": { + "deviation": "Black has to be quick to jump on the initiative of white's king being vulnerable." + } + }, + { + "ply": 41, + "fen": "r5k1/ppp2Qpp/8/4Nb2/3P4/2N1PPq1/PP2B1Pr/R4RK1 b - - 0 21", + "id": "4X", + "uci": "b3f7", + "san": "Qxf7+", + "check": true + }, + { + "ply": 42, + "fen": "r6k/ppp2Qpp/8/4Nb2/3P4/2N1PPq1/PP2B1Pr/R4RK1 w - - 1 22", + "id": "ab", + "uci": "g8h8", + "san": "Kh8" + }, + { + "ply": 43, + "fen": "r6k/ppp2Qpp/8/4Nb2/3P4/2N1PPq1/PP2BRPr/R5K1 b - - 2 22", + "id": "(0", + "uci": "f1f2", + "san": "Rf2" + }, + { + "ply": 44, + "fen": "r6k/ppp2Qpp/8/4Nb2/3P3q/2N1PP2/PP2BRPr/R5K1 w - - 3 23", + "id": "9B", + "uci": "g3h4", + "san": "Qh4", + "gamebook": { + "deviation": "Keep the initiative going! Go for the king!" + } + }, + { + "ply": 45, + "fen": "r5Qk/ppp3pp/8/4Nb2/3P3q/2N1PP2/PP2BRPr/R5K1 b - - 4 23", + "id": "Xa", + "uci": "f7g8", + "san": "Qg8+", + "check": true, + "children": [ + { + "ply": 46, + "fen": "6rk/ppp3pp/8/4Nb2/3P3q/2N1PP2/PP2BRPr/R5K1 w - - 0 24", + "id": "[a", + "uci": "a8g8", + "san": "Rxg8", + "comments": [ + { + "id": "lq80", + "text": "This allows for Nf7#", + "by": { + "id": "kyle-and-jess", + "name": "Kyle-and-Jess" + } + } + ], + "glyphs": [ + { + "id": 4, + "symbol": "??", + "name": "Blunder" + } + ], + "children": [] + } + ] + }, + { + "ply": 46, + "fen": "r5k1/ppp3pp/8/4Nb2/3P3q/2N1PP2/PP2BRPr/R5K1 w - - 0 24", + "id": "ba", + "uci": "h8g8", + "san": "Kxg8", + "comments": [ + { + "id": "sAXm", + "text": "Good job avoiding the smothered mate!", + "by": { + "id": "kyle-and-jess", + "name": "Kyle-and-Jess" + } + } + ] + } + ] + } +} +'''; + + final mockClient = MockClient((request) { + if (request.url.path == '/study/JbWtuaeK/7OJXp679') { + expect(request.url.queryParameters['chapters'], '1'); + return mockResponse( + response, + 200, + ); + } else if (request.url.path == '/api/study/JbWtuaeK/7OJXp679.pgn') { + return mockResponse( + 'pgn', + 200, + ); + } + return mockResponse('', 404); + }); + + final repo = StudyRepository(mockClient); + + final (study, pgn) = await repo.getStudy( + id: const StudyId('JbWtuaeK'), + chapterId: const StudyChapterId('7OJXp679'), + ); + + expect(pgn, 'pgn'); + + expect( + study, + Study( + id: const StudyId('JbWtuaeK'), + name: 'How to Solve Puzzles Correctly', + position: const ( + chapterId: StudyChapterId('EgqyeQIp'), + path: '', + ), + liked: false, + likes: 29, + ownerId: const UserId('kyle-and-jess'), + features: ( + cloneable: false, + chat: true, + sticky: false, + ), + topics: const IList.empty(), + chapters: IList( + const [ + StudyChapterMeta( + id: StudyChapterId('EgqyeQIp'), + name: 'Introduction', + fen: null, + ), + StudyChapterMeta( + id: StudyChapterId('z6tGV47W'), + name: 'Practice Your Thought Process', + fen: + '2k4r/p1p2p2/1p2b2p/1Pqn2r1/2B5/B1PP4/P4PPP/RN2Q1K1 b - - 6 20', + ), + StudyChapterMeta( + id: StudyChapterId('dTfxbccx'), + name: 'Practice Strategic Thinking', + fen: + 'r3r1k1/1b2b2p/pq4pB/1p3pN1/2p5/2P5/PPn1QPPP/3RR1K1 w - - 0 23', + ), + StudyChapterMeta( + id: StudyChapterId('B1U4pFdG'), + name: 'Calculate Fully', + fen: + '3r3r/1Rpk1p2/2p2q1p/Q2pp3/P2PP1n1/2P1B1Pp/5P2/1N3RK1 b - - 2 26', + ), + StudyChapterMeta( + id: StudyChapterId('NJLW7jil'), + name: 'Calculate Freely', + fen: '4k3/8/6p1/R1p1r1n1/P3Pp2/2N2r2/1PP1K1R1/8 b - - 2 39', + ), + StudyChapterMeta( + id: StudyChapterId('7OJXp679'), + name: 'Use a Timer', + fen: + 'r5k1/ppp2ppp/7r/4Nb2/3P4/1QN1PPq1/PP2B1P1/R4RK1 b - - 1 20', + ), + StudyChapterMeta( + id: StudyChapterId('Rgk6UlTP'), + name: 'Understand Your Mistakes', + fen: + 'r4rk1/1R3pb1/pR2N1p1/2q5/4p3/2P1P1Pp/Q2P1P1P/6K1 b - - 1 26', + ), + StudyChapterMeta( + id: StudyChapterId('VsdxmjCf'), + name: 'Adjusting Difficulty', + fen: + '3r4/k1pq1p1r/pp1p2p1/8/3P4/P1P2BP1/1P1N1Pp1/R3R1K1 b - - 0 1', + ), + StudyChapterMeta( + id: StudyChapterId('FHU6xhYs'), + name: 'Using Themes', + fen: + 'r2k3N/pbpp1Bpp/1p6/2b1p3/3n3q/P7/1PPP1RPP/RNB2QK1 b - - 3 12', + ), + StudyChapterMeta( + id: StudyChapterId('8FhO455h'), + name: 'Endurance Training', + fen: '8/1p5k/2qPQ2p/p5p1/5r1n/2B4P/5P2/4R1K1 w - - 3 41', + ), + StudyChapterMeta( + id: StudyChapterId('jWUEWsEf'), + name: 'Final Thoughts', + fen: + '8/1PP2PP1/PppPPppP/Pp1pp1pP/Pp4pP/1Pp2pP1/2PppP2/3PP3 w - - 0 1', + ), + ], + ), + chapter: const StudyChapter( + id: StudyChapterId('7OJXp679'), + setup: StudyChapterSetup( + id: null, + orientation: Side.black, + variant: Variant.standard, + fromFen: true, + ), + practise: false, + conceal: null, + gamebook: true, + features: ( + computer: false, + explorer: false, + ), + ), + ), + ); + }); + }); +} From 9605fa4210d08900f1fa0e04351955146ad0a496 Mon Sep 17 00:00:00 2001 From: tom-anders <13141438+tom-anders@users.noreply.github.com> Date: Sun, 20 Oct 2024 23:27:43 +0200 Subject: [PATCH 3/7] use pick instead of generated json --- lib/src/model/common/id.dart | 10 +++++++ lib/src/model/study/study.dart | 31 ++++++++++++++++++--- lib/src/model/study/study_repository.dart | 6 +--- test/model/study/study_repository_test.dart | 4 --- 4 files changed, 38 insertions(+), 13 deletions(-) diff --git a/lib/src/model/common/id.dart b/lib/src/model/common/id.dart index 8d9b7225e7..813de7f8b4 100644 --- a/lib/src/model/common/id.dart +++ b/lib/src/model/common/id.dart @@ -196,4 +196,14 @@ extension IDPick on Pick { return null; } } + + StudyId asStudyIdOrThrow() { + final value = required().value; + if (value is String) { + return StudyId(value); + } + throw PickException( + "value $value at $debugParsingExit can't be casted to StudyId", + ); + } } diff --git a/lib/src/model/study/study.dart b/lib/src/model/study/study.dart index d1de222a78..d3eb929b99 100644 --- a/lib/src/model/study/study.dart +++ b/lib/src/model/study/study.dart @@ -1,5 +1,6 @@ import 'package:collection/collection.dart'; import 'package:dartchess/dartchess.dart'; +import 'package:deep_pick/deep_pick.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:lichess_mobile/src/model/common/chess.dart'; @@ -9,18 +10,17 @@ import 'package:lichess_mobile/src/model/user/user.dart'; part 'study.freezed.dart'; part 'study.g.dart'; -@Freezed(fromJson: true) +@freezed class Study with _$Study { const Study._(); const factory Study({ required StudyId id, required String name, - required ({StudyChapterId chapterId, String path}) position, required bool liked, required int likes, required UserId? ownerId, - @JsonKey(fromJson: studyFeaturesFromJson) required StudyFeatures features, + required StudyFeatures features, required IList topics, required IList chapters, required StudyChapter chapter, @@ -29,7 +29,30 @@ class Study with _$Study { StudyChapterMeta get currentChapterMeta => chapters.firstWhere((c) => c.id == chapter.id); - factory Study.fromJson(Map json) => _$StudyFromJson(json); + factory Study.fromServerJson(Map json) => + _studyFromPick(pick(json).required()); +} + +Study _studyFromPick(RequiredPick pick) { + final study = pick('study'); + return Study( + id: study('id').asStudyIdOrThrow(), + name: study('name').asStringOrThrow(), + liked: study('liked').asBoolOrThrow(), + likes: study('likes').asIntOrThrow(), + ownerId: study('ownerId').asUserIdOrNull(), + features: ( + cloneable: study('features', 'cloneable').asBoolOrFalse(), + chat: study('features', 'chat').asBoolOrFalse(), + sticky: study('features', 'sticky').asBoolOrFalse(), + ), + topics: + study('topics').asListOrThrow((pick) => pick.asStringOrThrow()).lock, + chapters: study('chapters') + .asListOrThrow((pick) => StudyChapterMeta.fromJson(pick.asMapOrThrow())) + .lock, + chapter: StudyChapter.fromJson(study('chapter').asMapOrThrow()), + ); } typedef StudyFeatures = ({ diff --git a/lib/src/model/study/study_repository.dart b/lib/src/model/study/study_repository.dart index 5b479a7ef7..59e552d4d3 100644 --- a/lib/src/model/study/study_repository.dart +++ b/lib/src/model/study/study_repository.dart @@ -69,11 +69,7 @@ class StudyRepository { }, ), headers: {'Accept': 'application/json'}, - mapper: (Map json) { - return Study.fromJson( - pick(json, 'study').asMapOrThrow(), - ); - }, + mapper: Study.fromServerJson, ); final pgnBytes = await client.readBytes( diff --git a/test/model/study/study_repository_test.dart b/test/model/study/study_repository_test.dart index 2a120dcd5f..baa8565087 100644 --- a/test/model/study/study_repository_test.dart +++ b/test/model/study/study_repository_test.dart @@ -340,10 +340,6 @@ void main() { Study( id: const StudyId('JbWtuaeK'), name: 'How to Solve Puzzles Correctly', - position: const ( - chapterId: StudyChapterId('EgqyeQIp'), - path: '', - ), liked: false, likes: 29, ownerId: const UserId('kyle-and-jess'), From 19c946b35dd399cf6fb882063e0a5d61fd57e1c2 Mon Sep 17 00:00:00 2001 From: tom-anders <13141438+tom-anders@users.noreply.github.com> Date: Sun, 20 Oct 2024 23:34:35 +0200 Subject: [PATCH 4/7] add support for hints and deviations --- lib/src/model/study/study.dart | 19 +++++++++++++++++++ test/model/study/study_repository_test.dart | 20 ++++++++++++++++++++ 2 files changed, 39 insertions(+) diff --git a/lib/src/model/study/study.dart b/lib/src/model/study/study.dart index d3eb929b99..7295fa3856 100644 --- a/lib/src/model/study/study.dart +++ b/lib/src/model/study/study.dart @@ -24,6 +24,13 @@ class Study with _$Study { required IList topics, required IList chapters, required StudyChapter chapter, + // Hints to display in "gamebook"/"interactive" mode + // Index corresponds to the current ply. + required IList hints, + // Comment to display when deviating from the mainline in "gamebook" mode + // (i.e. when making a wrong move). + // Index corresponds to the current ply. + required IList deviationComments, }) = _Study; StudyChapterMeta get currentChapterMeta => @@ -34,6 +41,16 @@ class Study with _$Study { } Study _studyFromPick(RequiredPick pick) { + final treeParts = pick('analysis', 'treeParts').asListOrThrow((part) => part); + + final hints = []; + final deviationComments = []; + + for (final part in treeParts) { + hints.add(part('gamebook', 'hint').asStringOrNull()); + deviationComments.add(part('gamebook', 'deviation').asStringOrNull()); + } + final study = pick('study'); return Study( id: study('id').asStudyIdOrThrow(), @@ -52,6 +69,8 @@ Study _studyFromPick(RequiredPick pick) { .asListOrThrow((pick) => StudyChapterMeta.fromJson(pick.asMapOrThrow())) .lock, chapter: StudyChapter.fromJson(study('chapter').asMapOrThrow()), + hints: hints.lock, + deviationComments: deviationComments.lock, ); } diff --git a/test/model/study/study_repository_test.dart b/test/model/study/study_repository_test.dart index baa8565087..d3c269da6c 100644 --- a/test/model/study/study_repository_test.dart +++ b/test/model/study/study_repository_test.dart @@ -432,6 +432,26 @@ void main() { explorer: false, ), ), + hints: [ + 'The white king is not very safe. Can black increase the pressure on the king?', + null, + null, + null, + null, + null, + null, + null, + ].lock, + deviationComments: [ + null, + "Black has to be quick to jump on the initiative of white's king being vulnerable.", + null, + null, + null, + 'Keep the initiative going! Go for the king!', + null, + null, + ].lock, ), ); }); From eb3161c9e15f80c607370ca16935924e4e479a78 Mon Sep 17 00:00:00 2001 From: tom-anders <13141438+tom-anders@users.noreply.github.com> Date: Sun, 20 Oct 2024 23:37:52 +0200 Subject: [PATCH 5/7] remove now unused helper function --- lib/src/model/study/study.dart | 8 -------- 1 file changed, 8 deletions(-) diff --git a/lib/src/model/study/study.dart b/lib/src/model/study/study.dart index 7295fa3856..5a68fa7f4e 100644 --- a/lib/src/model/study/study.dart +++ b/lib/src/model/study/study.dart @@ -80,14 +80,6 @@ typedef StudyFeatures = ({ bool sticky, }); -StudyFeatures studyFeaturesFromJson(Map json) { - return ( - cloneable: json['cloneable'] as bool? ?? false, - chat: json['chat'] as bool? ?? false, - sticky: json['sticky'] as bool? ?? false, - ); -} - @Freezed(fromJson: true) class StudyChapter with _$StudyChapter { const StudyChapter._(); From 87c00d532963fa7bcc3e7a71a3b016ee21585f64 Mon Sep 17 00:00:00 2001 From: tom-anders <13141438+tom-anders@users.noreply.github.com> Date: Mon, 21 Oct 2024 09:55:43 +0200 Subject: [PATCH 6/7] fix doc comments --- lib/src/model/study/study.dart | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/lib/src/model/study/study.dart b/lib/src/model/study/study.dart index 5a68fa7f4e..ea04c948e7 100644 --- a/lib/src/model/study/study.dart +++ b/lib/src/model/study/study.dart @@ -24,12 +24,14 @@ class Study with _$Study { required IList topics, required IList chapters, required StudyChapter chapter, - // Hints to display in "gamebook"/"interactive" mode - // Index corresponds to the current ply. + + /// Hints to display in "gamebook"/"interactive" mode + /// Index corresponds to the current ply. required IList hints, - // Comment to display when deviating from the mainline in "gamebook" mode - // (i.e. when making a wrong move). - // Index corresponds to the current ply. + + /// Comment to display when deviating from the mainline in "gamebook" mode + /// (i.e. when making a wrong move). + /// Index corresponds to the current ply. required IList deviationComments, }) = _Study; From a01ed8a7a3e4c2dd80610e0442a80b2545ef9a5f Mon Sep 17 00:00:00 2001 From: tom-anders <13141438+tom-anders@users.noreply.github.com> Date: Fri, 25 Oct 2024 23:21:01 +0200 Subject: [PATCH 7/7] add provider for study repository (so that we can mock it in tests) --- lib/src/model/study/study_repository.dart | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/lib/src/model/study/study_repository.dart b/lib/src/model/study/study_repository.dart index 59e552d4d3..d9032a24ba 100644 --- a/lib/src/model/study/study_repository.dart +++ b/lib/src/model/study/study_repository.dart @@ -2,11 +2,20 @@ import 'dart:convert'; import 'package:deep_pick/deep_pick.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:http/http.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/study/study.dart'; import 'package:lichess_mobile/src/model/study/study_filter.dart'; import 'package:lichess_mobile/src/network/http.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'study_repository.g.dart'; + +@Riverpod(keepAlive: true) +StudyRepository studyRepository(Ref ref) { + return StudyRepository(ref.read(lichessClientProvider)); +} class StudyRepository { StudyRepository(this.client);