/ services / bookmarkService.ts
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  };