Skip to content

Commit

Permalink
feat: add study list screen
Browse files Browse the repository at this point in the history
  • Loading branch information
tom-anders committed Sep 6, 2024
1 parent 96c3c1c commit 2ac0588
Show file tree
Hide file tree
Showing 5 changed files with 435 additions and 0 deletions.
8 changes: 8 additions & 0 deletions lib/src/model/common/id.dart
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,14 @@ extension type const BroadcastRoundId(String value) implements StringId {}

extension type const BroadcastGameId(String value) implements StringId {}

extension type const StudyId(String value) implements StringId {
StudyId.fromJson(dynamic json) : this(json as String);
}

extension type const StudyChapterId(String value) implements StringId {
StudyChapterId.fromJson(dynamic json) : this(json as String);
}

extension IDPick on Pick {
UserId asUserIdOrThrow() {
final value = required().value;
Expand Down
100 changes: 100 additions & 0 deletions lib/src/model/study/study.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
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/analysis/analysis_controller.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 bool liked,
required int likes,
required LightUser? owner,
required IList<StudyChapterMeta> chapters,
required StudyChapter chapter,
}) = _Study;

factory Study.fromJson(Map<String, Object?> json) => _$StudyFromJson(json);
}

@Freezed(fromJson: true)
class StudyChapter with _$StudyChapter {
const StudyChapter._();

const factory StudyChapter({
required StudyChapterId id,
required StudyChapterSetup setup,
required String? description,
}) = _StudyChapter;

factory StudyChapter.fromJson(Map<String, Object?> json) =>
_$StudyChapterFromJson(json);
}

@Freezed(fromJson: true)
class StudyChapterSetup with _$StudyChapterSetup {
const StudyChapterSetup._();

const factory StudyChapterSetup({
required GameId? id,
required Side orientation,
@JsonKey(fromJson: _variantFromJson)
required (Variant key, String name) variant,
required bool? fromFen,
}) = _StudyChapterSetup;

factory StudyChapterSetup.fromJson(Map<String, Object?> json) =>
_$StudyChapterSetupFromJson(json);
}

(Variant, String) _variantFromJson(Map<String, Object?> json) {
final key = Variant.values.firstWhereOrNull(
(v) => v.name == json['key'],
);
return (key!, json['name']! as String);
}

@Freezed(fromJson: true)
class StudyChapterMeta with _$StudyChapterMeta {
const StudyChapterMeta._();

const factory StudyChapterMeta({
required StudyChapterId id,
required String name,
}) = _StudyChapterMeta;

factory StudyChapterMeta.fromJson(Map<String, Object?> json) =>
_$StudyChapterMetaFromJson(json);
}

@Freezed(fromJson: true)
class StudyPageData with _$StudyPageData {
const StudyPageData._();

const factory StudyPageData({
required StudyId id,
required String name,
required bool liked,
required int likes,
@JsonKey(fromJson: DateTime.fromMillisecondsSinceEpoch)
required DateTime updatedAt,
required LightUser? owner,
// TODO members
required IList<String> topics,
required IList<String> chapters,
required String? flair,
}) = _StudyPageData;

factory StudyPageData.fromJson(Map<String, Object?> json) =>
_$StudyPageDataFromJson(json);
}
123 changes: 123 additions & 0 deletions lib/src/model/study/study_repository.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
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/http.dart';
import 'package:lichess_mobile/src/model/common/id.dart';
import 'package:lichess_mobile/src/model/study/study.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';

part 'study_repository.g.dart';

enum StudyCategory { all, mine, member, public, private, likes }

enum StudyListOrder { hot, newest, oldest, updated, popular }

typedef CategoryAndOrder = ({StudyCategory category, StudyListOrder order});
typedef StudyList = ({IList<StudyPageData> studies, int? nextPage});

class StudyRepository {
StudyRepository(this.client);

final Client client;

Future<StudyList> getStudies({
required StudyCategory category,
required StudyListOrder order,
int page = 1,
}) {
return _requestStudies(
path: '${category.name}/${order.name}',
queryParameters: {'page': page.toString()},
);
}

Future<StudyList> searchStudies({
required String query,
int page = 1,
}) {
return _requestStudies(
path: 'search',
queryParameters: {'page': page.toString(), 'q': query},
);
}

Future<StudyList> _requestStudies({
required String path,
required Map<String, String> queryParameters,
}) {
return client.readJson(
Uri(
path: '/study/$path',
queryParameters: queryParameters,
),
headers: {'Accept': 'application/json'},
mapper: (Map<String, dynamic> json) {
final paginator =
pick(json, 'paginator').asMapOrThrow<String, dynamic>();

return (
studies: pick(paginator, 'currentPageResults')
.asListOrThrow(
(pick) => StudyPageData.fromJson(pick.asMapOrThrow()),
)
.toIList(),
nextPage: pick(paginator, 'nextPage').asIntOrNull(),
);
},
);
}

Future<Study> getStudy({required StudyId id, StudyChapterId? chapterId}) {
return client.readJson(
Uri(path: '/study/$id${chapterId != null ? '/$chapterId' : ''}'),
headers: {'Accept': 'application/json'},
mapper: (Map<String, dynamic> json) {
return Study.fromJson(
pick(json, 'study').asMapOrThrow(),
);
},
);
}
}

/// This provider is used to get a list of studies from the paginated API
@riverpod
class StudiesPaginator extends _$StudiesPaginator {
Future<StudyList> nextPage() async {
final nextPage = state.value?.nextPage ?? 1;

return await ref.withClient(
(client) => search == null
? StudyRepository(client).getStudies(
category: categoryAndOrder.category,
order: categoryAndOrder.order,
page: nextPage,
)
: StudyRepository(client).searchStudies(
query: search!,
page: nextPage,
),
);
}

@override
Future<StudyList> build({
required CategoryAndOrder categoryAndOrder,
String? search,
}) async {
return nextPage();
}

Future<void> next() async {
final studyList = state.requireValue;

final newStudyPage = await nextPage();

state = AsyncData(
(
nextPage: newStudyPage.nextPage,
studies: studyList.studies.addAll(newStudyPage.studies),
),
);
}
}
Loading

0 comments on commit 2ac0588

Please sign in to comment.