/ app / (tabs) / bookmarks.tsx
bookmarks.tsx
   1  import React, {
   2    useCallback,
   3    useState,
   4    useEffect,
   5    useRef,
   6    useMemo,
   7  } from 'react';
   8  import {
   9    View,
  10    Text,
  11    FlatList,
  12    TouchableOpacity,
  13    StyleSheet,
  14    ActivityIndicator,
  15    SafeAreaView,
  16    ScrollView,
  17    TextInput,
  18    Platform,
  19    Dimensions,
  20    ListRenderItemInfo,
  21  } from 'react-native';
  22  import AsyncStorage from '@react-native-async-storage/async-storage';
  23  import { getMangaData } from '@/services/bookmarkService';
  24  import { useRouter } from 'expo-router';
  25  import { useFocusEffect } from '@react-navigation/native';
  26  import { useTheme } from '@/constants/ThemeContext';
  27  import { Colors } from '@/constants/Colors';
  28  import { Ionicons } from '@expo/vector-icons';
  29  import MangaCard from '@/components/MangaCard';
  30  import { useSafeAreaInsets } from 'react-native-safe-area-context';
  31  import { BookmarkItem, BookmarkStatus } from '@/types';
  32  import Animated, {
  33    useSharedValue,
  34    useAnimatedStyle,
  35    withTiming,
  36    Easing,
  37    runOnJS,
  38  } from 'react-native-reanimated';
  39  import {
  40    Gesture,
  41    GestureDetector,
  42    GestureHandlerRootView,
  43  } from 'react-native-gesture-handler';
  44  import { imageCache } from '@/services/CacheImages';
  45  
  46  // Constants
  47  const SECTIONS: BookmarkStatus[] = ['Reading', 'To Read', 'On Hold', 'Read'];
  48  const SORT_OPTIONS = [
  49    { id: 'title-asc', label: 'Title (A‑Z)', icon: 'text' },
  50    { id: 'title-desc', label: 'Title (Z‑A)', icon: 'text' },
  51    { id: 'updated-desc', label: 'Last Read (Recent)', icon: 'time' },
  52    { id: 'updated-asc', label: 'Last Read (Oldest)', icon: 'time' },
  53  ];
  54  const { width: SCREEN_WIDTH } = Dimensions.get('window');
  55  const VIEW_MODE_STORAGE_KEY = 'bookmarksViewMode';
  56  
  57  // Types
  58  type ViewMode = 'grid' | 'list';
  59  type AnimatedFlatListProps = Animated.AnimateProps<
  60    React.ComponentProps<typeof FlatList<BookmarkItem>>
  61  >;
  62  
  63  // Animated Components
  64  const AnimatedTouchableOpacity =
  65    Animated.createAnimatedComponent(TouchableOpacity);
  66  const AnimatedFlatList = Animated.createAnimatedComponent(
  67    FlatList
  68  ) as React.ComponentType<AnimatedFlatListProps>;
  69  const AnimatedView = Animated.createAnimatedComponent(View);
  70  
  71  // Helper component for preloading images
  72  const ImagePreloader = ({ urls }: { urls: string[] }) => {
  73    useEffect(() => {
  74      urls.forEach((url) => {
  75        if (url) imageCache.getCachedImagePath(url, 'bookmark');
  76      });
  77    }, [urls]);
  78    return null;
  79  };
  80  
  81  export default function BookmarksScreen() {
  82    // State
  83    const [bookmarks, setBookmarks] = useState<BookmarkItem[]>([]);
  84    const [sectionData, setSectionData] = useState<
  85      Record<BookmarkStatus, BookmarkItem[]>
  86    >({
  87      Reading: [],
  88      'On Hold': [],
  89      'To Read': [],
  90      Read: [],
  91    });
  92    const [isLoading, setIsLoading] = useState(true);
  93    const [isViewModeLoading, setIsViewModeLoading] = useState(true);
  94    const [activeSection, setActiveSection] = useState<BookmarkStatus>('Reading');
  95    const [searchQuery, setSearchQuery] = useState('');
  96    const [sortOption, setSortOption] = useState(SORT_OPTIONS[0]?.id || 'title-asc');
  97    const [viewMode, setViewMode] = useState<ViewMode>('grid');
  98    const [showSortOptions, setShowSortOptions] = useState(false);
  99    const [allImageUrls, setAllImageUrls] = useState<string[]>([]);
 100  
 101    // Animation values
 102    const translateX = useSharedValue(0);
 103    const sortOptionsHeight = useSharedValue(0);
 104    const isAnimating = useSharedValue(false);
 105  
 106    // Refs
 107    const router = useRouter();
 108    const flatListRef = useRef<FlatList<BookmarkItem>>(null);
 109    const sectionScrollRef = useRef<ScrollView>(null);
 110    const searchInputRef = useRef<TextInput>(null);
 111  
 112    // Theme
 113    const { actualTheme } = useTheme();
 114    const colors = Colors[actualTheme];
 115    const styles = getStyles(colors);
 116    const insets = useSafeAreaInsets();
 117  
 118    // Load view mode preference
 119    useEffect(() => {
 120      const loadViewMode = async () => {
 121        try {
 122          const saved = (await AsyncStorage.getItem(
 123            VIEW_MODE_STORAGE_KEY
 124          )) as ViewMode | null;
 125          if (saved) setViewMode(saved);
 126        } catch (e) {
 127          console.error('Failed to load view mode:', e);
 128        } finally {
 129          setIsViewModeLoading(false);
 130        }
 131      };
 132  
 133      loadViewMode();
 134    }, []);
 135  
 136    // Process bookmarks: filter, sort and group by section
 137    const processBookmarks = useCallback(
 138      (items: BookmarkItem[], query: string, sort: string) => {
 139        const sections: Record<BookmarkStatus, BookmarkItem[]> = {
 140          Reading: [],
 141          'On Hold': [],
 142          'To Read': [],
 143          Read: [],
 144        };
 145  
 146        // Filter by search query
 147        let filtered = items;
 148        if (query.trim()) {
 149          const q = query.toLowerCase();
 150          filtered = items.filter((it) => it.title.toLowerCase().includes(q));
 151        }
 152  
 153        // Sort function based on selected option
 154        const sortFn = (arr: BookmarkItem[]) => {
 155          const a = [...arr];
 156          switch (sort) {
 157            case 'title-asc':
 158              a.sort((x, y) => x.title.localeCompare(y.title));
 159              break;
 160            case 'title-desc':
 161              a.sort((x, y) => y.title.localeCompare(x.title));
 162              break;
 163            case 'updated-desc':
 164              a.sort((x, y) => (y.lastUpdated ?? 0) - (x.lastUpdated ?? 0));
 165              break;
 166            case 'updated-asc':
 167              a.sort((x, y) => (x.lastUpdated ?? 0) - (y.lastUpdated ?? 0));
 168              break;
 169          }
 170          return a;
 171        };
 172  
 173        // Group by section
 174        filtered.forEach((it) => {
 175          const status = it.status as BookmarkStatus;
 176          if (status && sections[status]) {
 177            sections[status].push(it);
 178          }
 179        });
 180  
 181        // Sort each section
 182        for (const k in sections) {
 183          sections[k as BookmarkStatus] = sortFn(sections[k as BookmarkStatus]);
 184        }
 185  
 186        setSectionData(sections);
 187        setAllImageUrls(filtered.map((it) => it.imageUrl).filter(Boolean));
 188      },
 189      []
 190    );
 191  
 192    // Fetch bookmarks from storage
 193    const fetchBookmarks = useCallback(async () => {
 194      setIsLoading(true);
 195      try {
 196        const raw = await AsyncStorage.getItem('bookmarkKeys');
 197        const keys = raw ? JSON.parse(raw) : [];
 198        const arr = await Promise.all(
 199          keys.map(async (key: string) => {
 200            const id = key.split('_')[1];
 201            if (!id) return null;
 202            const d = await getMangaData(id);
 203            if (!d) return null;
 204            return {
 205              id: d.id,
 206              title: d.title,
 207              status: (d.bookmarkStatus as BookmarkStatus) || 'Reading',
 208              lastReadChapter: d.lastReadChapter
 209                ? `Chapter ${d.lastReadChapter}`
 210                : 'Not started',
 211              imageUrl: d.bannerImage || '',
 212              lastUpdated: d.lastUpdated ?? 0,
 213            } as BookmarkItem;
 214          })
 215        );
 216        setBookmarks(arr.filter((x): x is BookmarkItem => x != null));
 217      } catch (e) {
 218        console.error('Failed to fetch bookmarks:', e);
 219      } finally {
 220        setIsLoading(false);
 221      }
 222    }, []);
 223  
 224    // Initial fetch
 225    useEffect(() => {
 226      fetchBookmarks();
 227    }, [fetchBookmarks]);
 228  
 229    // Process bookmarks when data, search or sort changes
 230    useEffect(() => {
 231      processBookmarks(bookmarks, searchQuery, sortOption);
 232    }, [bookmarks, searchQuery, sortOption, processBookmarks]);
 233  
 234    // Refresh bookmarks when screen comes into focus and changes detected
 235    useFocusEffect(
 236      useCallback(() => {
 237        const checkForChanges = async () => {
 238          const changed = await AsyncStorage.getItem('bookmarkChanged');
 239          if (changed === 'true') {
 240            await fetchBookmarks();
 241            await AsyncStorage.setItem('bookmarkChanged', 'false');
 242          }
 243        };
 244  
 245        checkForChanges();
 246      }, [fetchBookmarks])
 247    );
 248  
 249    // Animated styles
 250    const contentAnim = useAnimatedStyle(() => ({
 251      transform: [{ translateX: translateX.value }],
 252    }));
 253  
 254    const sortOptsAnim = useAnimatedStyle(() => ({
 255      height: sortOptionsHeight.value,
 256      overflow: 'hidden',
 257    }));
 258  
 259    // Event Handlers
 260    const handleBookmarkPress = useCallback(
 261      (id: string) => {
 262        router.push(`/manga/${id}`);
 263      },
 264      [router]
 265    );
 266  
 267    const handleClearSearch = useCallback(() => {
 268      setSearchQuery('');
 269    }, []);
 270  
 271    const toggleSortOptions = useCallback(() => {
 272      if (showSortOptions) {
 273        sortOptionsHeight.value = withTiming(0, { duration: 200 }, () =>
 274          runOnJS(setShowSortOptions)(false)
 275        );
 276      } else {
 277        setShowSortOptions(true);
 278        const height = SORT_OPTIONS.length * 40 + 10;
 279        sortOptionsHeight.value = withTiming(height, { duration: 200 });
 280      }
 281    }, [showSortOptions]);
 282  
 283    const selectSort = useCallback((opt: string) => {
 284      setSortOption(opt);
 285      sortOptionsHeight.value = withTiming(0, { duration: 200 }, () =>
 286        runOnJS(setShowSortOptions)(false)
 287      );
 288    }, []);
 289  
 290    const toggleView = useCallback(async () => {
 291      const newMode: ViewMode = viewMode === 'grid' ? 'list' : 'grid';
 292      setViewMode(newMode);
 293      try {
 294        await AsyncStorage.setItem(VIEW_MODE_STORAGE_KEY, newMode);
 295      } catch (e) {
 296        console.error('Failed to save view mode:', e);
 297      }
 298    }, [viewMode]);
 299  
 300    // Section change animation completed
 301    const onSectionAnimDone = useCallback((section: BookmarkStatus) => {
 302      setActiveSection(section);
 303  
 304      // Center tab in scroll view
 305      const idx = SECTIONS.indexOf(section);
 306      if (sectionScrollRef.current) {
 307        const visibleTabs = Math.min(SECTIONS.length, 4);
 308        const tabWidth = SCREEN_WIDTH / visibleTabs;
 309        const scrollX = Math.max(
 310          0,
 311          idx * tabWidth - tabWidth * (visibleTabs / 2 - 0.5)
 312        );
 313        sectionScrollRef.current.scrollTo({ x: scrollX, animated: true });
 314      }
 315  
 316      // Scroll to top of list
 317      flatListRef.current?.scrollToOffset({ offset: 0, animated: false });
 318      isAnimating.value = false;
 319    }, []);
 320  
 321    // Handle section change with animation
 322    const changeSection = useCallback(
 323      (section: BookmarkStatus) => {
 324        if (section === activeSection || isAnimating.value) return;
 325  
 326        isAnimating.value = true;
 327        const currentIndex = SECTIONS.indexOf(activeSection);
 328        const nextIndex = SECTIONS.indexOf(section);
 329        const direction = currentIndex < nextIndex ? 1 : -1;
 330  
 331        // Animate out
 332        translateX.value = withTiming(
 333          -direction * SCREEN_WIDTH,
 334          {
 335            duration: 250,
 336            easing: Easing.out(Easing.cubic),
 337          },
 338          (finished) => {
 339            if (finished) {
 340              // Jump to other side
 341              translateX.value = direction * SCREEN_WIDTH;
 342              // Update section
 343              runOnJS(onSectionAnimDone)(section);
 344              // Animate in
 345              translateX.value = withTiming(0, {
 346                duration: 250,
 347                easing: Easing.out(Easing.cubic),
 348              });
 349            } else {
 350              runOnJS(() => {
 351                isAnimating.value = false;
 352              })();
 353              translateX.value = withTiming(0);
 354            }
 355          }
 356        );
 357      },
 358      [activeSection, onSectionAnimDone]
 359    );
 360  
 361    // Pan gesture for swipe between sections
 362    const pan = useMemo(
 363      () =>
 364        Gesture.Pan()
 365          .activeOffsetX([-20, 20])
 366          .failOffsetY([-10, 10])
 367          .onUpdate((e) => {
 368            if (!isAnimating.value) {
 369              translateX.value = Math.max(
 370                -SCREEN_WIDTH / 2,
 371                Math.min(SCREEN_WIDTH / 2, e.translationX)
 372              );
 373            }
 374          })
 375          .onEnd((e) => {
 376            if (isAnimating.value) return;
 377  
 378            const { velocityX, translationX } = e;
 379            const threshold = SCREEN_WIDTH * 0.25;
 380            const velocityThreshold = 400;
 381  
 382            if (
 383              Math.abs(translationX) > threshold ||
 384              Math.abs(velocityX) > velocityThreshold
 385            ) {
 386              const direction = translationX > 0 ? -1 : 1;
 387              const currentIndex = SECTIONS.indexOf(activeSection);
 388              const nextIndex = Math.max(
 389                0,
 390                Math.min(SECTIONS.length - 1, currentIndex + direction)
 391              );
 392  
 393              if (currentIndex !== nextIndex) {
 394                const section = SECTIONS[nextIndex];
 395                if (section) {
 396                  runOnJS(changeSection)(section);
 397                }
 398              } else {
 399                translateX.value = withTiming(0, {
 400                  duration: 200,
 401                  easing: Easing.out(Easing.cubic),
 402                });
 403              }
 404            } else {
 405              translateX.value = withTiming(0, {
 406                duration: 200,
 407                easing: Easing.out(Easing.cubic),
 408              });
 409            }
 410          }),
 411      [activeSection, changeSection]
 412    );
 413  
 414    // Render bookmark item (grid or list view)
 415    const renderBookmarkItem = useCallback(
 416      (info: ListRenderItemInfo<BookmarkItem>) => {
 417        const item = info.item;
 418  
 419        if (viewMode === 'grid') {
 420          return (
 421            <View style={styles.bookmarkCardWrapper}>
 422              <MangaCard
 423                title={item.title}
 424                imageUrl={item.imageUrl}
 425                onPress={() => handleBookmarkPress(item.id)}
 426                lastReadChapter={item.lastReadChapter}
 427                context="bookmark"
 428                mangaId={item.id}
 429                onBookmarkChange={(_mangaId, newStatus) => {
 430                  // If unbookmarked (newStatus is null), refresh immediately
 431                  if (newStatus === null) {
 432                    fetchBookmarks();
 433                  } else {
 434                    // For status changes, just refresh to update the display
 435                    fetchBookmarks();
 436                  }
 437                }}
 438              />
 439            </View>
 440          );
 441        }
 442  
 443        return (
 444          <TouchableOpacity
 445            style={styles.listItem}
 446            onPress={() => handleBookmarkPress(item.id)}
 447            activeOpacity={0.7}
 448          >
 449            <View style={styles.listItemImageContainer}>
 450              <MangaCard
 451                title=""
 452                imageUrl={item.imageUrl}
 453                onPress={() => {}}
 454                lastReadChapter={null}
 455                style={styles.listItemImage}
 456                context="bookmark"
 457                mangaId={item.id}
 458                onBookmarkChange={(_mangaId, newStatus) => {
 459                  // If unbookmarked (newStatus is null), refresh immediately
 460                  if (newStatus === null) {
 461                    fetchBookmarks();
 462                  } else {
 463                    // For status changes, just refresh to update the display
 464                    fetchBookmarks();
 465                  }
 466                }}
 467              />
 468            </View>
 469            <View style={styles.listItemContent}>
 470              <Text style={styles.listItemTitle} numberOfLines={2}>
 471                {item.title}
 472              </Text>
 473              <Text style={styles.listItemChapter}>{item.lastReadChapter}</Text>
 474            </View>
 475            <Ionicons
 476              name="chevron-forward"
 477              size={24}
 478              color={colors.tabIconDefault}
 479            />
 480          </TouchableOpacity>
 481        );
 482      },
 483      [viewMode, handleBookmarkPress, styles, colors.tabIconDefault]
 484    );
 485  
 486    // Render section button
 487    const renderSectionButton = useCallback(
 488      (title: BookmarkStatus) => {
 489        // Select icon based on section
 490        let icon: keyof typeof Ionicons.glyphMap = 'book';
 491        switch (title) {
 492          case 'To Read':
 493            icon = 'book-outline';
 494            break;
 495          case 'Reading':
 496            icon = 'book';
 497            break;
 498          case 'On Hold':
 499            icon = 'pause-circle-outline';
 500            break;
 501          case 'Read':
 502            icon = 'checkmark-circle-outline';
 503            break;
 504        }
 505  
 506        const count = sectionData[title]?.length ?? 0;
 507        const isActive = title === activeSection;
 508  
 509        return (
 510          <AnimatedTouchableOpacity
 511            key={title}
 512            style={[styles.sectionButton, isActive && styles.activeSectionButton]}
 513            onPress={() => changeSection(title)}
 514            activeOpacity={0.7}
 515          >
 516            <Ionicons
 517              name={icon}
 518              size={18}
 519              color={isActive ? colors.card : colors.text}
 520              style={styles.sectionButtonIcon}
 521            />
 522            <Text
 523              style={[
 524                styles.sectionButtonText,
 525                isActive && styles.activeSectionButtonText,
 526              ]}
 527            >
 528              {title}
 529            </Text>
 530            <View
 531              style={[styles.sectionCount, isActive && styles.activeSectionCount]}
 532            >
 533              <Text
 534                style={[
 535                  styles.sectionCountText,
 536                  isActive && styles.activeSectionCountText,
 537                ]}
 538              >
 539                {count}
 540              </Text>
 541            </View>
 542          </AnimatedTouchableOpacity>
 543        );
 544      },
 545      [activeSection, sectionData, changeSection, styles, colors]
 546    );
 547  
 548    // Current section's items
 549    const currentItems = useMemo(
 550      () => sectionData[activeSection] || [],
 551      [sectionData, activeSection]
 552    );
 553  
 554    // Loading state
 555    if (isLoading || isViewModeLoading) {
 556      return (
 557        <SafeAreaView style={[styles.container, { paddingTop: insets.top }]}>
 558          <View style={styles.loadingContainer}>
 559            <ActivityIndicator size="large" color={colors.primary} />
 560            <Text style={styles.loadingText}>Loading...</Text>
 561          </View>
 562        </SafeAreaView>
 563      );
 564    }
 565  
 566    return (
 567      <GestureHandlerRootView style={{ flex: 1 }}>
 568        <SafeAreaView style={[styles.container, { paddingTop: insets.top }]}>
 569          <ImagePreloader urls={allImageUrls} />
 570  
 571          {/* Header */}
 572          <View style={styles.header}>
 573            <Text style={styles.headerTitle}>My Bookmarks</Text>
 574            <View style={styles.headerButtons}>
 575              <TouchableOpacity
 576                style={styles.headerButton}
 577                onPress={toggleSortOptions}
 578                activeOpacity={0.7}
 579              >
 580                <Ionicons name="options-outline" size={22} color={colors.text} />
 581              </TouchableOpacity>
 582              <TouchableOpacity
 583                style={styles.headerButton}
 584                onPress={toggleView}
 585                activeOpacity={0.7}
 586              >
 587                <Ionicons
 588                  name={viewMode === 'grid' ? 'list-outline' : 'grid-outline'}
 589                  size={22}
 590                  color={colors.text}
 591                />
 592              </TouchableOpacity>
 593            </View>
 594          </View>
 595  
 596          {/* Search */}
 597          <View style={styles.searchContainer}>
 598            <View style={styles.searchInputContainer}>
 599              <Ionicons
 600                name="search"
 601                size={20}
 602                color={colors.tabIconDefault}
 603                style={styles.searchIcon}
 604              />
 605              <TextInput
 606                ref={searchInputRef}
 607                style={styles.searchInput}
 608                placeholder="Search bookmarks..."
 609                placeholderTextColor={colors.tabIconDefault}
 610                value={searchQuery}
 611                onChangeText={setSearchQuery}
 612              />
 613              {searchQuery ? (
 614                <TouchableOpacity
 615                  onPress={handleClearSearch}
 616                  style={styles.clearButton}
 617                  hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
 618                >
 619                  <Ionicons
 620                    name="close-circle"
 621                    size={20}
 622                    color={colors.tabIconDefault}
 623                  />
 624                </TouchableOpacity>
 625              ) : null}
 626            </View>
 627          </View>
 628  
 629          {/* Sort Options */}
 630          <Animated.View
 631            style={[styles.sortOptionsContainerWrapper, sortOptsAnim]}
 632          >
 633            {showSortOptions && (
 634              <View style={styles.sortOptionsContainer}>
 635                {SORT_OPTIONS.map((opt) => (
 636                  <TouchableOpacity
 637                    key={opt.id}
 638                    style={[
 639                      styles.sortOption,
 640                      sortOption === opt.id && styles.activeSortOption,
 641                    ]}
 642                    onPress={() => selectSort(opt.id)}
 643                    activeOpacity={0.7}
 644                  >
 645                    <Ionicons
 646                      name={opt.icon as keyof typeof Ionicons.glyphMap}
 647                      size={18}
 648                      color={sortOption === opt.id ? colors.primary : colors.text}
 649                      style={styles.sortOptionIcon}
 650                    />
 651                    <Text
 652                      style={[
 653                        styles.sortOptionText,
 654                        sortOption === opt.id && styles.activeSortOptionText,
 655                      ]}
 656                    >
 657                      {opt.label}
 658                    </Text>
 659                    {sortOption === opt.id && (
 660                      <Ionicons
 661                        name="checkmark"
 662                        size={18}
 663                        color={colors.primary}
 664                      />
 665                    )}
 666                  </TouchableOpacity>
 667                ))}
 668              </View>
 669            )}
 670          </Animated.View>
 671  
 672          {/* Section Tabs */}
 673          <View style={styles.sectionButtonsContainer}>
 674            <ScrollView
 675              ref={sectionScrollRef}
 676              horizontal
 677              showsHorizontalScrollIndicator={false}
 678              contentContainerStyle={styles.sectionButtonsScroll}
 679            >
 680              {SECTIONS.map(renderSectionButton)}
 681            </ScrollView>
 682          </View>
 683  
 684          {/* Content Area with Gesture Detector */}
 685          <GestureDetector gesture={pan}>
 686            <View style={styles.contentWrapper}>
 687              <AnimatedView style={[styles.contentContainer, contentAnim]}>
 688                {currentItems.length === 0 ? (
 689                  <View style={styles.emptyStateContainer}>
 690                    <Ionicons
 691                      name="bookmark-outline"
 692                      size={64}
 693                      color={colors.tabIconDefault}
 694                    />
 695                    <Text style={styles.emptyStateText}>
 696                      {searchQuery
 697                        ? `No bookmarks found for "${searchQuery}"`
 698                        : `No ${activeSection.toLowerCase()} manga found`}
 699                    </Text>
 700                    {searchQuery ? (
 701                      <TouchableOpacity
 702                        style={styles.clearSearchButton}
 703                        onPress={handleClearSearch}
 704                      >
 705                        <Text style={styles.clearSearchButtonText}>
 706                          Clear Search
 707                        </Text>
 708                      </TouchableOpacity>
 709                    ) : null}
 710                  </View>
 711                ) : (
 712                  <>
 713                    <Text style={styles.resultCount}>
 714                      {currentItems.length}{' '}
 715                      {currentItems.length > 1 ? 'mangas' : 'manga'}
 716                    </Text>
 717                    <AnimatedFlatList
 718                      //@ts-ignore
 719                      ref={flatListRef}
 720                      data={currentItems}
 721                      renderItem={renderBookmarkItem}
 722                      keyExtractor={(item) => item.id}
 723                      numColumns={viewMode === 'grid' ? 2 : 1}
 724                      key={viewMode}
 725                      extraData={[activeSection, viewMode]}
 726                      columnWrapperStyle={
 727                        viewMode === 'grid' ? styles.columnWrapper : undefined
 728                      }
 729                      contentContainerStyle={styles.listContentContainer}
 730                      showsVerticalScrollIndicator={false}
 731                      removeClippedSubviews
 732                      initialNumToRender={10}
 733                      maxToRenderPerBatch={10}
 734                      windowSize={11}
 735                      keyboardShouldPersistTaps="always"
 736                      keyboardDismissMode="on-drag"
 737                    />
 738                  </>
 739                )}
 740              </AnimatedView>
 741            </View>
 742          </GestureDetector>
 743        </SafeAreaView>
 744      </GestureHandlerRootView>
 745    );
 746  }
 747  
 748  const getStyles = (colors: typeof Colors.light) =>
 749    StyleSheet.create({
 750      container: {
 751        flex: 1,
 752        backgroundColor: colors.background,
 753      },
 754      header: {
 755        flexDirection: 'row',
 756        justifyContent: 'space-between',
 757        alignItems: 'center',
 758        paddingHorizontal: 20,
 759        paddingBottom: 10,
 760      },
 761      headerTitle: {
 762        fontSize: 26,
 763        fontWeight: 'bold',
 764        color: colors.text,
 765      },
 766      headerButtons: {
 767        flexDirection: 'row',
 768        alignItems: 'center',
 769      },
 770      headerButton: {
 771        width: 38,
 772        height: 38,
 773        borderRadius: 19,
 774        backgroundColor: colors.card,
 775        justifyContent: 'center',
 776        alignItems: 'center',
 777        marginLeft: 10,
 778        shadowColor: '#000',
 779        shadowOffset: { width: 0, height: 1 },
 780        shadowOpacity: 0.1,
 781        shadowRadius: 1.5,
 782        elevation: 2,
 783      },
 784      searchContainer: {
 785        paddingHorizontal: 20,
 786        marginVertical: 6,
 787      },
 788      searchInputContainer: {
 789        flexDirection: 'row',
 790        alignItems: 'center',
 791        backgroundColor: colors.card,
 792        borderRadius: 8,
 793        paddingHorizontal: 10,
 794        height: 36,
 795        shadowColor: '#000',
 796        shadowOffset: { width: 0, height: 1 },
 797        shadowOpacity: 0.1,
 798        shadowRadius: 1.5,
 799        elevation: 2,
 800      },
 801      searchIcon: {
 802        marginRight: 5,
 803      },
 804      searchInput: {
 805        flex: 1,
 806        fontSize: 15,
 807        color: colors.text,
 808        paddingVertical: Platform.OS === 'ios' ? 5 : 3,
 809      },
 810      clearButton: {
 811        padding: 3,
 812        marginLeft: 3,
 813      },
 814      sortOptionsContainerWrapper: {
 815        marginHorizontal: 20,
 816        borderRadius: 10,
 817        marginBottom: 8,
 818        overflow: 'hidden',
 819        backgroundColor: colors.card,
 820        shadowColor: '#000',
 821        shadowOffset: { width: 0, height: 1 },
 822        shadowOpacity: 0.1,
 823        shadowRadius: 1.5,
 824        elevation: 2,
 825      },
 826      sortOptionsContainer: {
 827        borderRadius: 10,
 828        paddingVertical: 5,
 829      },
 830      sortOption: {
 831        flexDirection: 'row',
 832        alignItems: 'center',
 833        paddingVertical: 9,
 834        paddingHorizontal: 15,
 835        height: 40,
 836      },
 837      activeSortOption: {
 838        backgroundColor: colors.primary + '20',
 839      },
 840      sortOptionIcon: {
 841        marginRight: 12,
 842      },
 843      sortOptionText: {
 844        fontSize: 14,
 845        color: colors.text,
 846        flex: 1,
 847      },
 848      activeSortOptionText: {
 849        color: colors.primary,
 850        fontWeight: '600',
 851      },
 852      sectionButtonsContainer: {
 853        marginBottom: 8,
 854        paddingBottom: 5,
 855      },
 856      sectionButtonsScroll: {
 857        paddingHorizontal: 15,
 858        alignItems: 'center',
 859      },
 860      sectionButton: {
 861        flexDirection: 'row',
 862        alignItems: 'center',
 863        paddingVertical: 7,
 864        paddingHorizontal: 14,
 865        borderRadius: 18,
 866        backgroundColor: colors.card,
 867        marginHorizontal: 4,
 868        shadowColor: '#000',
 869        shadowOffset: { width: 0, height: 1 },
 870        shadowOpacity: 0.08,
 871        shadowRadius: 1,
 872        elevation: 1.5,
 873      },
 874      activeSectionButton: {
 875        backgroundColor: colors.primary,
 876        elevation: 3,
 877      },
 878      sectionButtonIcon: {
 879        marginRight: 5,
 880      },
 881      sectionButtonText: {
 882        fontSize: 13,
 883        fontWeight: '600',
 884        color: colors.text,
 885      },
 886      activeSectionButtonText: {
 887        color: colors.card,
 888      },
 889      sectionCount: {
 890        backgroundColor: colors.background + '99',
 891        minWidth: 20,
 892        height: 20,
 893        borderRadius: 10,
 894        justifyContent: 'center',
 895        alignItems: 'center',
 896        marginLeft: 6,
 897        paddingHorizontal: 5,
 898        borderWidth: 1,
 899        borderColor: colors.border,
 900      },
 901      activeSectionCount: {
 902        backgroundColor: colors.card + 'CC',
 903        borderColor: colors.primary + '50',
 904      },
 905      sectionCountText: {
 906        fontSize: 11,
 907        fontWeight: '700',
 908        color: colors.text,
 909      },
 910      activeSectionCountText: {
 911        color: colors.primary,
 912      },
 913      contentWrapper: {
 914        flex: 1,
 915        overflow: 'hidden',
 916      },
 917      contentContainer: {
 918        flex: 1,
 919        backgroundColor: colors.background,
 920        width: '100%',
 921      },
 922      resultCount: {
 923        paddingHorizontal: 20,
 924        paddingBottom: 8,
 925        fontSize: 13,
 926        color: colors.tabIconDefault,
 927      },
 928      listContentContainer: {
 929        paddingHorizontal: 15,
 930        paddingBottom: 80,
 931      },
 932      columnWrapper: {
 933        justifyContent: 'space-between',
 934      },
 935      bookmarkCardWrapper: {
 936        width: '48%',
 937        marginBottom: 15,
 938      },
 939      listItem: {
 940        flexDirection: 'row',
 941        backgroundColor: colors.card,
 942        borderRadius: 10,
 943        marginBottom: 10,
 944        padding: 10,
 945        alignItems: 'center',
 946        shadowColor: '#000',
 947        shadowOffset: { width: 0, height: 1 },
 948        shadowOpacity: 0.1,
 949        shadowRadius: 1.5,
 950        elevation: 2,
 951      },
 952      listItemImageContainer: {
 953        width: 65,
 954        height: 90,
 955        borderRadius: 6,
 956        overflow: 'hidden',
 957        backgroundColor: colors.border,
 958      },
 959      listItemImage: {
 960        width: '100%',
 961        height: '100%',
 962        borderRadius: 0,
 963      },
 964      listItemContent: {
 965        flex: 1,
 966        marginLeft: 12,
 967        marginRight: 8,
 968        justifyContent: 'center',
 969      },
 970      listItemTitle: {
 971        fontSize: 15,
 972        fontWeight: 'bold',
 973        color: colors.text,
 974        marginBottom: 4,
 975      },
 976      listItemChapter: {
 977        fontSize: 13,
 978        color: colors.tabIconDefault,
 979      },
 980      loadingContainer: {
 981        flex: 1,
 982        justifyContent: 'center',
 983        alignItems: 'center',
 984        backgroundColor: colors.background,
 985      },
 986      loadingText: {
 987        marginTop: 16,
 988        fontSize: 16,
 989        color: colors.text,
 990      },
 991      emptyStateContainer: {
 992        flex: 1,
 993        justifyContent: 'center',
 994        alignItems: 'center',
 995        padding: 30,
 996        marginTop: -50,
 997      },
 998      emptyStateText: {
 999        fontSize: 17,
1000        textAlign: 'center',
1001        marginTop: 20,
1002        marginBottom: 25,
1003        color: colors.tabIconDefault,
1004        lineHeight: 24,
1005      },
1006      clearSearchButton: {
1007        backgroundColor: colors.primary,
1008        paddingVertical: 10,
1009        paddingHorizontal: 25,
1010        borderRadius: 20,
1011      },
1012      clearSearchButtonText: {
1013        color: colors.card,
1014        fontWeight: '600',
1015        fontSize: 15,
1016      },
1017    });