bookmarkService.ts
1 import AsyncStorage from '@react-native-async-storage/async-storage'; 2 import { decode } from 'html-entities'; 3 import { Alert } from 'react-native'; 4 import { updateAniListStatus } from './anilistService'; 5 import { BookmarkStatus, MangaData, IconName } from '@/types'; 6 7 const MANGA_STORAGE_PREFIX = 'manga_'; 8 9 export const getMangaData = async (id: string): Promise<MangaData | null> => { 10 try { 11 const value = await AsyncStorage.getItem(`${MANGA_STORAGE_PREFIX}${id}`); 12 return value ? JSON.parse(value) : null; 13 } catch (e) { 14 console.error('Error reading manga data:', e); 15 return null; 16 } 17 }; 18 19 export const setMangaData = async (data: MangaData): Promise<void> => { 20 try { 21 await AsyncStorage.setItem( 22 `${MANGA_STORAGE_PREFIX}${data.id}`, 23 JSON.stringify(data) 24 ); 25 // Update bookmarkKeys for backwards compatibility and listing 26 const keys = await AsyncStorage.getItem('bookmarkKeys'); 27 const bookmarkKeys = keys ? JSON.parse(keys) : []; 28 if (data.bookmarkStatus && !bookmarkKeys.includes(`bookmark_${data.id}`)) { 29 bookmarkKeys.push(`bookmark_${data.id}`); 30 await AsyncStorage.setItem('bookmarkKeys', JSON.stringify(bookmarkKeys)); 31 } 32 // Set the bookmark changed flag 33 await AsyncStorage.setItem('bookmarkChanged', 'true'); 34 } catch (e) { 35 console.error('Error saving manga data:', e); 36 } 37 }; 38 39 export const fetchBookmarkStatus = async ( 40 id: string 41 ): Promise<string | null> => { 42 const mangaData = await getMangaData(id); 43 return mangaData?.bookmarkStatus || null; 44 }; 45 46 const markAllChaptersAsRead = async ( 47 id: string, 48 mangaDetails: any, 49 setReadChapters: (chapters: string[]) => void 50 ) => { 51 try { 52 if (mangaDetails?.chapters?.length > 0) { 53 const allChapterNumbers = mangaDetails.chapters.map( 54 (chapter: any) => chapter.number 55 ); 56 const mangaData = (await getMangaData(id)) || { 57 id, 58 title: decode(mangaDetails.title || ''), 59 bannerImage: mangaDetails.bannerImage || '', 60 bookmarkStatus: null, 61 readChapters: [], 62 lastUpdated: Date.now(), 63 totalChapters: mangaDetails.chapters.length, 64 }; 65 66 // Get the highest chapter number to set as lastReadChapter 67 const lastChapter = Math.max( 68 ...allChapterNumbers.map((num: string) => parseFloat(num)) 69 ).toString(); 70 await setMangaData({ 71 ...mangaData, 72 readChapters: allChapterNumbers, 73 lastReadChapter: lastChapter, 74 lastUpdated: Date.now(), 75 }); 76 setReadChapters(allChapterNumbers); 77 } else { 78 console.log('No chapters to mark as read'); 79 } 80 } catch (error) { 81 console.error('Error marking all chapters as read:', error); 82 } 83 }; 84 85 export const saveBookmark = async ( 86 id: string, 87 status: BookmarkStatus, 88 mangaDetails: any, 89 readChapters: string[], 90 setBookmarkStatus: (status: string | null) => void, 91 setIsAlertVisible: (visible: boolean) => void, 92 setReadChapters: (chapters: string[]) => void 93 ) => { 94 try { 95 const mangaData: MangaData = { 96 id, 97 title: decode(mangaDetails?.title || ''), 98 bannerImage: mangaDetails?.bannerImage || '', 99 bookmarkStatus: status, 100 readChapters, 101 lastUpdated: Date.now(), 102 totalChapters: mangaDetails?.chapters?.length, 103 }; 104 105 if (status === 'Reading' && mangaDetails?.chapters?.length > 0) { 106 mangaData.lastNotifiedChapter = mangaDetails.chapters[0].number; 107 } 108 109 await setMangaData(mangaData); 110 setBookmarkStatus(status); 111 setIsAlertVisible(false); 112 113 if (status === 'Read') { 114 Alert.alert( 115 'Mark All Chapters as Read', 116 'Do you want to mark all chapters as read?', 117 [ 118 { 119 text: 'No', 120 style: 'cancel', 121 onPress: async () => { 122 // Update lastReadChapter to the highest read chapter 123 if (readChapters.length > 0) { 124 const highestReadChapter = Math.max( 125 ...readChapters.map((num: string) => parseFloat(num)) 126 ).toString(); 127 await setMangaData({ 128 ...mangaData, 129 lastReadChapter: highestReadChapter, 130 }); 131 } 132 await updateAniListStatus( 133 mangaDetails?.title, 134 status, 135 readChapters, 136 mangaDetails?.chapters.length 137 ); 138 }, 139 }, 140 { 141 text: 'Yes', 142 onPress: async () => { 143 await markAllChaptersAsRead(id, mangaDetails, setReadChapters); 144 await updateAniListStatus( 145 mangaDetails?.title, 146 status, 147 readChapters, 148 mangaDetails?.chapters.length 149 ); 150 }, 151 }, 152 ] 153 ); 154 } else if (status !== 'On Hold') { 155 // Only update AniList if status is not "On Hold" since that status doesn't exist on AniList 156 await updateAniListStatus( 157 mangaDetails?.title, 158 status, 159 readChapters, 160 mangaDetails?.chapters.length 161 ); 162 } 163 } catch (error) { 164 console.error('Error saving bookmark:', error); 165 Alert.alert('Error', 'Failed to update status. Please try again.'); 166 } 167 }; 168 169 export const removeBookmark = async ( 170 id: string, 171 setBookmarkStatus: (status: string | null) => void, 172 setIsAlertVisible: (visible: boolean) => void 173 ) => { 174 try { 175 await AsyncStorage.removeItem(`${MANGA_STORAGE_PREFIX}${id}`); 176 177 const keys = await AsyncStorage.getItem('bookmarkKeys'); 178 if (keys) { 179 const bookmarkKeys = JSON.parse(keys); 180 const updatedKeys = bookmarkKeys.filter( 181 (key: string) => key !== `bookmark_${id}` 182 ); 183 await AsyncStorage.setItem('bookmarkKeys', JSON.stringify(updatedKeys)); 184 } 185 186 setBookmarkStatus(null); 187 setIsAlertVisible(false); 188 await AsyncStorage.setItem('bookmarkChanged', 'true'); 189 } catch (error) { 190 console.error('Error removing bookmark:', error); 191 } 192 }; 193 194 export const getBookmarkPopupConfig = ( 195 bookmarkStatus: string | null, 196 mangaTitle: string, 197 handleSaveBookmark: (status: BookmarkStatus) => void, 198 handleRemoveBookmark: () => void 199 ) => { 200 return { 201 title: bookmarkStatus 202 ? `Update Bookmark for ${mangaTitle}` 203 : `Bookmark ${mangaTitle}`, 204 options: bookmarkStatus 205 ? [ 206 { 207 text: 'To Read', 208 onPress: () => handleSaveBookmark('To Read'), 209 icon: 'book-outline' as IconName, 210 }, 211 { 212 text: 'Reading', 213 onPress: () => handleSaveBookmark('Reading'), 214 icon: 'book' as IconName, 215 }, 216 { 217 text: 'On Hold', 218 onPress: () => handleSaveBookmark('On Hold'), 219 icon: 'pause-circle-outline' as IconName, 220 }, 221 { 222 text: 'Read', 223 onPress: () => handleSaveBookmark('Read'), 224 icon: 'checkmark-circle-outline' as IconName, 225 }, 226 { 227 text: 'Unbookmark', 228 onPress: handleRemoveBookmark, 229 icon: 'close-circle-outline' as IconName, 230 }, 231 ] 232 : [ 233 { 234 text: 'To Read', 235 onPress: () => handleSaveBookmark('To Read'), 236 icon: 'book-outline' as IconName, 237 }, 238 { 239 text: 'Reading', 240 onPress: () => handleSaveBookmark('Reading'), 241 icon: 'book' as IconName, 242 }, 243 { 244 text: 'On Hold', 245 onPress: () => handleSaveBookmark('On Hold'), 246 icon: 'pause-circle-outline' as IconName, 247 }, 248 { 249 text: 'Read', 250 onPress: () => handleSaveBookmark('Read'), 251 icon: 'checkmark-circle-outline' as IconName, 252 }, 253 ], 254 }; 255 }; 256 257 export const getChapterLongPressAlertConfig = ( 258 isRead: boolean, 259 chapterNumber: string, 260 mangaDetails: any, 261 id: string, 262 readChapters: string[], 263 setReadChapters: (chapters: string[]) => void 264 ) => { 265 if (!isRead) { 266 return { 267 type: 'confirm', 268 title: 'Mark Chapters as Read', 269 message: `Do you want to mark all chapters up to chapter ${chapterNumber} as read?`, 270 options: [ 271 { 272 text: 'Cancel', 273 onPress: () => {}, 274 }, 275 { 276 text: 'Yes', 277 onPress: async () => { 278 try { 279 const chaptersToMark = 280 mangaDetails?.chapters 281 .filter((ch: any) => { 282 const currentChapter = parseFloat(ch.number); 283 const selectedChapter = parseFloat(chapterNumber); 284 return currentChapter <= selectedChapter; 285 }) 286 .map((ch: any) => ch.number) || []; 287 288 const mangaData = await getMangaData(id); 289 if (mangaData) { 290 const updatedReadChapters = Array.from( 291 new Set([...readChapters, ...chaptersToMark]) 292 ); 293 const highestChapter = Math.max( 294 ...updatedReadChapters.map((ch) => parseFloat(ch)) 295 ).toString(); 296 await setMangaData({ 297 ...mangaData, 298 readChapters: updatedReadChapters, 299 lastReadChapter: highestChapter, // Using highest chapter number 300 lastUpdated: Date.now(), 301 }); 302 setReadChapters(updatedReadChapters); 303 } 304 } catch (error) { 305 console.error('Error marking chapters as read:', error); 306 } 307 }, 308 }, 309 ], 310 }; 311 } 312 return null; 313 };