Skip to content

Commit db97f9c

Browse files
committed
feat(reader): add bookmark and highlight model, service, state, and list sheet
1 parent 0ac1423 commit db97f9c

4 files changed

Lines changed: 494 additions & 0 deletions

File tree

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import 'package:flutter_riverpod/flutter_riverpod.dart';
2+
import 'package:readmigo/core/network/api_client.dart';
3+
import 'package:readmigo/models/bookmark.dart';
4+
5+
final bookmarkServiceProvider = Provider<BookmarkService>((ref) {
6+
return BookmarkService(apiClient: ref.watch(apiClientProvider));
7+
});
8+
9+
class BookmarkService {
10+
final ApiClient _apiClient;
11+
12+
BookmarkService({required ApiClient apiClient}) : _apiClient = apiClient;
13+
14+
Future<List<Bookmark>> getBookmarks(String bookId) async {
15+
final response = await _apiClient.get('/books/$bookId/bookmarks');
16+
final list = response.data is List
17+
? response.data as List
18+
: (response.data as Map)['bookmarks'] as List? ?? [];
19+
return list
20+
.map((e) => Bookmark.fromJson(e as Map<String, dynamic>))
21+
.toList();
22+
}
23+
24+
Future<List<Bookmark>> getHighlights(String bookId) async {
25+
final response = await _apiClient.get('/books/$bookId/highlights');
26+
final list = response.data is List
27+
? response.data as List
28+
: (response.data as Map)['highlights'] as List? ?? [];
29+
return list
30+
.map((e) => Bookmark.fromJson(e as Map<String, dynamic>))
31+
.toList();
32+
}
33+
34+
Future<Bookmark> createBookmark(
35+
String bookId,
36+
Map<String, dynamic> data,
37+
) async {
38+
final response = await _apiClient.post(
39+
'/bookmarks',
40+
data: {'bookId': bookId, ...data},
41+
);
42+
return Bookmark.fromJson(response.data as Map<String, dynamic>);
43+
}
44+
45+
Future<Bookmark> createHighlight(
46+
String bookId,
47+
Map<String, dynamic> data,
48+
) async {
49+
final response = await _apiClient.post(
50+
'/books/$bookId/highlights',
51+
data: data,
52+
);
53+
return Bookmark.fromJson(response.data as Map<String, dynamic>);
54+
}
55+
56+
Future<void> deleteBookmark(String bookmarkId) async {
57+
await _apiClient.delete('/bookmarks/$bookmarkId');
58+
}
59+
}
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
import 'package:flutter_riverpod/flutter_riverpod.dart';
2+
import 'package:readmigo/features/reader/data/bookmark_service.dart';
3+
import 'package:readmigo/models/bookmark.dart';
4+
5+
class BookmarkState {
6+
final List<Bookmark> bookmarks;
7+
final List<Bookmark> highlights;
8+
final bool isLoading;
9+
10+
const BookmarkState({
11+
this.bookmarks = const [],
12+
this.highlights = const [],
13+
this.isLoading = false,
14+
});
15+
16+
List<Bookmark> get all =>
17+
[...bookmarks, ...highlights]
18+
..sort((a, b) => b.createdAt.compareTo(a.createdAt));
19+
20+
BookmarkState copyWith({
21+
List<Bookmark>? bookmarks,
22+
List<Bookmark>? highlights,
23+
bool? isLoading,
24+
}) {
25+
return BookmarkState(
26+
bookmarks: bookmarks ?? this.bookmarks,
27+
highlights: highlights ?? this.highlights,
28+
isLoading: isLoading ?? this.isLoading,
29+
);
30+
}
31+
}
32+
33+
class BookmarkNotifier extends FamilyNotifier<BookmarkState, String> {
34+
@override
35+
BookmarkState build(String arg) => const BookmarkState();
36+
37+
String get bookId => arg;
38+
39+
BookmarkService get _service => ref.read(bookmarkServiceProvider);
40+
41+
Future<void> loadAll() async {
42+
state = state.copyWith(isLoading: true);
43+
try {
44+
final results = await Future.wait([
45+
_service.getBookmarks(bookId),
46+
_service.getHighlights(bookId),
47+
]);
48+
state = state.copyWith(
49+
bookmarks: results[0],
50+
highlights: results[1],
51+
isLoading: false,
52+
);
53+
} catch (_) {
54+
state = state.copyWith(isLoading: false);
55+
}
56+
}
57+
58+
Future<void> addBookmark({
59+
required String chapterId,
60+
int? paragraphIndex,
61+
}) async {
62+
try {
63+
final bm = await _service.createBookmark(bookId, {
64+
'chapterId': chapterId,
65+
if (paragraphIndex != null) 'paragraphIndex': paragraphIndex,
66+
'type': 'bookmark',
67+
});
68+
state = state.copyWith(bookmarks: [...state.bookmarks, bm]);
69+
} catch (_) {
70+
// Silently ignore errors
71+
}
72+
}
73+
74+
Future<void> addHighlight({
75+
required String chapterId,
76+
required int paragraphIndex,
77+
required int charOffset,
78+
required int charLength,
79+
required String selectedText,
80+
HighlightColor color = HighlightColor.yellow,
81+
HighlightStyle style = HighlightStyle.background,
82+
String? note,
83+
}) async {
84+
try {
85+
final hl = await _service.createHighlight(bookId, {
86+
'chapterId': chapterId,
87+
'paragraphIndex': paragraphIndex,
88+
'charOffset': charOffset,
89+
'charLength': charLength,
90+
'selectedText': selectedText,
91+
'color': color.name,
92+
'style': style.name,
93+
'type': note != null ? 'annotation' : 'highlight',
94+
if (note != null) 'note': note,
95+
});
96+
state = state.copyWith(highlights: [...state.highlights, hl]);
97+
} catch (_) {
98+
// Silently ignore errors
99+
}
100+
}
101+
102+
Future<void> deleteBookmark(String bookmarkId) async {
103+
state = state.copyWith(
104+
bookmarks: state.bookmarks.where((b) => b.id != bookmarkId).toList(),
105+
highlights: state.highlights.where((b) => b.id != bookmarkId).toList(),
106+
);
107+
try {
108+
await _service.deleteBookmark(bookmarkId);
109+
} catch (_) {
110+
await loadAll();
111+
}
112+
}
113+
}
114+
115+
final bookmarkProvider =
116+
NotifierProvider.family<BookmarkNotifier, BookmarkState, String>(
117+
BookmarkNotifier.new,
118+
);

0 commit comments

Comments
 (0)