@@ -3,24 +3,68 @@ import 'dart:io';
33
44import 'package:injectable/injectable.dart' ;
55import 'package:mibook/layers/data/api/custom_errors.dart' ;
6+ import 'package:mibook/layers/data/models/book_list_data.dart' ;
67import 'package:mibook/layers/data/models/reading_data.dart' ;
78import 'package:path_provider/path_provider.dart' ;
89
10+ const _readingList = 'reading_list' ;
11+ const _favoriteBooks = 'favorite_books' ;
12+ const _isFavoriteBook = 'is_favorite_book' ;
13+
914abstract class IStorageClient {
1015 Future <void > saveReading (ReadingData readingData);
1116 Future <List <ReadingData >> getReadingList ();
17+ Future <void > setFavoriteStatus (BookItem book, bool isFavorite);
18+ Future <bool > getFavoriteStatus (String bookId);
19+ Future <List <BookItem >> getFavoriteBooks ();
1220}
1321
14- @Singleton (as : IStorageClient )
22+ @LazySingleton (as : IStorageClient )
1523class StorageClient implements IStorageClient {
1624 Future <File > _getLocalFile (String fileName) async {
1725 final directory = await getApplicationDocumentsDirectory ();
1826 return File ('${directory .path }/$fileName .json' );
1927 }
2028
29+ Future <List <T >> _getListFromFile <T >(
30+ String fileName,
31+ T Function (Map <String , dynamic >) fromJson,
32+ ) async {
33+ final file = await _getLocalFile (fileName);
34+ final invalidFile = await file.exists () == false ;
35+
36+ if (invalidFile) {
37+ return [];
38+ }
39+
40+ final jsonString = await file.readAsString ();
41+
42+ if (jsonString.isEmpty) {
43+ return [];
44+ }
45+
46+ final List <dynamic > decoded = jsonDecode (jsonString);
47+ return decoded.map ((e) => fromJson (e)).toList ();
48+ }
49+
50+ Future <Map <String , bool >> _parseAsBoolMap (String jsonString) async {
51+ final decoded = jsonDecode (jsonString);
52+
53+ if (decoded is ! Map ) {
54+ throw Exception ("JSON root is not a map" );
55+ }
56+
57+ return decoded.map <String , bool >((key, value) {
58+ if (value is ! bool ) {
59+ throw Exception ("Value for '$key ' is not a bool" );
60+ }
61+ return MapEntry (key.toString (), value);
62+ });
63+ }
64+
2165 @override
2266 Future <void > saveReading (ReadingData readingData) async {
23- final file = await _getLocalFile ('reading_list' );
67+ final file = await _getLocalFile (_readingList );
2468 List <ReadingData > currentList = await getReadingList ();
2569
2670 if (currentList.any ((item) => item.bookId == readingData.bookId)) {
@@ -33,19 +77,72 @@ class StorageClient implements IStorageClient {
3377
3478 @override
3579 Future <List <ReadingData >> getReadingList () async {
36- final file = await _getLocalFile ('reading_list' );
80+ final List <ReadingData > readings = await _getListFromFile <ReadingData >(
81+ _readingList,
82+ (json) => ReadingData .fromJson (json),
83+ );
84+ return readings;
85+ }
3786
38- if (! await file.exists ()) {
39- return [];
87+ @override
88+ Future <List <BookItem >> getFavoriteBooks () async {
89+ final List <BookItem > favoriteBooks = await _getListFromFile <BookItem >(
90+ _favoriteBooks,
91+ (json) => BookItem .fromJson (json),
92+ );
93+ return favoriteBooks;
94+ }
95+
96+ @override
97+ Future <void > setFavoriteStatus (BookItem book, bool isFavorite) async {
98+ // 1. update favorite book list
99+ final file = await _getLocalFile (_favoriteBooks);
100+ List <BookItem > currentList = await getFavoriteBooks ();
101+
102+ currentList.add (book);
103+ final jsonString = jsonEncode (currentList.map ((e) => e.toJson ()).toList ());
104+ await file.writeAsString (jsonString);
105+
106+ // 2. update favorite status map
107+ final isFavoriteFile = await _getLocalFile (_isFavoriteBook);
108+
109+ Map <String , bool > isFavoriteBookMap = {};
110+
111+ // Safely read existing map
112+ if (await isFavoriteFile.exists ()) {
113+ final jsonStringIsFavorite = await isFavoriteFile.readAsString ();
114+
115+ if (jsonStringIsFavorite.isNotEmpty) {
116+ isFavoriteBookMap = await _parseAsBoolMap (jsonStringIsFavorite);
117+ }
40118 }
41119
42- final jsonString = await file.readAsString ();
120+ // Update or insert
121+ isFavoriteBookMap[book.id] = isFavorite;
122+
123+ // Write back
124+ final isFavoriteJsonString = jsonEncode (isFavoriteBookMap);
125+ await isFavoriteFile.writeAsString (isFavoriteJsonString);
126+ }
127+
128+ @override
129+ Future <bool > getFavoriteStatus (String bookId) async {
130+ final isFavoriteFile = await _getLocalFile (_isFavoriteBook);
131+
132+ if (! await isFavoriteFile.exists ()) {
133+ return false ;
134+ }
135+
136+ final jsonString = await isFavoriteFile.readAsString ();
43137
44138 if (jsonString.isEmpty) {
45- return [] ;
139+ return false ;
46140 }
47141
48- final List <dynamic > decoded = jsonDecode (jsonString);
49- return decoded.map ((e) => ReadingData .fromJson (e)).toList ();
142+ Map <String , bool > isFavoriteBookMap = await _parseAsBoolMap (
143+ jsonString,
144+ );
145+
146+ return isFavoriteBookMap[bookId] ?? false ;
50147 }
51148}
0 commit comments