├── .eslintrc.js
├── .github
├── ISSUE_TEMPLATE
│ ├── report_issue.yml
│ └── request_feature.yml
├── readme-images
│ ├── icon_alt.png
│ └── screenshots.png
└── workflows
│ ├── build.yml
│ ├── issue_moderator.yml
│ ├── lint.yml
│ └── types.yml
├── .gitignore
├── .husky
├── .gitignore
└── pre-commit
├── .prettierignore
├── .prettierrc.js
├── App.tsx
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── Gemfile
├── LICENSE
├── README.md
├── android
├── app
│ ├── build.gradle
│ ├── debug.keystore
│ ├── google-services.json
│ ├── proguard-rules.pro
│ └── src
│ │ ├── debug
│ │ ├── AndroidManifest.xml
│ │ └── res
│ │ │ └── values
│ │ │ └── string.xml
│ │ └── main
│ │ ├── AndroidManifest.xml
│ │ ├── assets
│ │ ├── css
│ │ │ ├── index.css
│ │ │ ├── pageReader.css
│ │ │ ├── toolWrapper.css
│ │ │ └── tts.css
│ │ ├── fonts
│ │ │ ├── OpenDyslexic3-Regular.ttf
│ │ │ ├── arbutus-slab.ttf
│ │ │ ├── domine.ttf
│ │ │ ├── lato.ttf
│ │ │ ├── lora.ttf
│ │ │ ├── noto-sans.ttf
│ │ │ ├── nunito.ttf
│ │ │ ├── open-sans.ttf
│ │ │ ├── pt-sans-bold.ttf
│ │ │ └── pt-serif.ttf
│ │ └── js
│ │ │ ├── core.js
│ │ │ ├── icons.js
│ │ │ ├── index.d.ts
│ │ │ ├── index.js
│ │ │ ├── text-vibe.js
│ │ │ ├── van.d.ts
│ │ │ └── van.js
│ │ ├── java
│ │ └── com
│ │ │ └── rajarsheechatterjee
│ │ │ ├── LNReader
│ │ │ ├── MainActivity.kt
│ │ │ └── MainApplication.kt
│ │ │ ├── NativeFile
│ │ │ ├── NativeFile.kt
│ │ │ └── NativePackage.kt
│ │ │ ├── NativeVolumeButtonListener
│ │ │ ├── NativeVolumeButtonListener.kt
│ │ │ └── NativeVolumeButtonListenerPackage.kt
│ │ │ └── NativeZipArchive
│ │ │ ├── NativeZipArchive.kt
│ │ │ └── NativeZipArchivePackage.kt
│ │ ├── jni
│ │ ├── CMakeLists.txt
│ │ └── OnLoad.cpp
│ │ └── res
│ │ ├── drawable-hdpi
│ │ ├── ic_action_name.png
│ │ └── notification_icon.png
│ │ ├── drawable-mdpi
│ │ ├── ic_action_name.png
│ │ └── notification_icon.png
│ │ ├── drawable-xhdpi
│ │ ├── ic_action_name.png
│ │ └── notification_icon.png
│ │ ├── drawable-xxhdpi
│ │ ├── ic_action_name.png
│ │ └── notification_icon.png
│ │ ├── drawable-xxxhdpi
│ │ ├── ic_action_name.png
│ │ └── notification_icon.png
│ │ ├── drawable
│ │ ├── invisible.xml
│ │ ├── rn_edit_text_material.xml
│ │ └── splashscreen.xml
│ │ ├── layout
│ │ └── launch_screen.xml
│ │ ├── mipmap-anydpi-v26
│ │ ├── ic_launcher.xml
│ │ └── ic_launcher_round.xml
│ │ ├── mipmap-hdpi
│ │ ├── ic_launcher.webp
│ │ ├── ic_launcher_background.webp
│ │ ├── ic_launcher_foreground.webp
│ │ ├── ic_launcher_monochrome.png
│ │ └── ic_launcher_round.webp
│ │ ├── mipmap-mdpi
│ │ ├── ic_launcher.webp
│ │ ├── ic_launcher_background.webp
│ │ ├── ic_launcher_foreground.webp
│ │ ├── ic_launcher_monochrome.png
│ │ └── ic_launcher_round.webp
│ │ ├── mipmap-xhdpi
│ │ ├── ic_launcher.webp
│ │ ├── ic_launcher_background.webp
│ │ ├── ic_launcher_foreground.webp
│ │ ├── ic_launcher_monochrome.png
│ │ └── ic_launcher_round.webp
│ │ ├── mipmap-xxhdpi
│ │ ├── ic_launcher.webp
│ │ ├── ic_launcher_background.webp
│ │ ├── ic_launcher_foreground.webp
│ │ ├── ic_launcher_monochrome.png
│ │ └── ic_launcher_round.webp
│ │ ├── mipmap-xxxhdpi
│ │ ├── ic_launcher.webp
│ │ ├── ic_launcher_background.webp
│ │ ├── ic_launcher_foreground.webp
│ │ ├── ic_launcher_monochrome.png
│ │ └── ic_launcher_round.webp
│ │ ├── raw
│ │ └── loading.json
│ │ └── values
│ │ ├── colors.xml
│ │ ├── ic_launcher_background.xml
│ │ ├── strings.xml
│ │ └── styles.xml
├── build.gradle
├── gradle.properties
├── gradle
│ └── wrapper
│ │ ├── gradle-wrapper.jar
│ │ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
└── settings.gradle
├── app.json
├── assets
├── anilist.png
├── logo.png
└── mal.png
├── babel.config.js
├── crowdin.yml
├── env.d.ts
├── index.js
├── ios
├── .xcode.env
├── Dynamic.swift
├── LNReader.xcodeproj
│ ├── project.pbxproj
│ ├── project.xcworkspace
│ │ └── contents.xcworkspacedata
│ └── xcshareddata
│ │ └── xcschemes
│ │ └── LNReader.xcscheme
├── LNReader.xcworkspace
│ └── contents.xcworkspacedata
├── LNReader
│ ├── AppDelegate.swift
│ ├── Images.xcassets
│ │ └── AppIcon.appiconset
│ │ │ ├── 1024.png
│ │ │ ├── 114.png
│ │ │ ├── 120.png
│ │ │ ├── 180.png
│ │ │ ├── 29.png
│ │ │ ├── 40.png
│ │ │ ├── 57.png
│ │ │ ├── 58.png
│ │ │ ├── 60.png
│ │ │ ├── 80.png
│ │ │ ├── 87.png
│ │ │ └── Contents.json
│ ├── Info.plist
│ ├── LNReader-Bridging-Header.h
│ ├── LaunchScreen.storyboard
│ └── PrivacyInfo.xcprivacy
├── NativeEpubUtil
│ ├── RCTNativeEpubUtil.h
│ └── RCTNativeEpubUtil.mm
├── NativeFile
│ ├── RCTNativeFile.h
│ └── RCTNativeFile.mm
├── NativeVolumeButtonListener
│ ├── RCTNativeVolumeButtonListener.h
│ └── RCTNativeVolumeButtonListener.mm
├── NativeZipArchive
│ ├── RCTNativeZipArchive.h
│ └── RCTNativeZipArchive.mm
├── Podfile
├── Podfile.lock
└── loading.json
├── metro.config.js
├── package-lock.json
├── package.json
├── scripts
├── setEnvFile.cjs
└── stringTypes.cjs
├── shared
├── Epub.cpp
├── Epub.hpp
├── NativeEpub.cpp
├── NativeEpub.hpp
├── pugiconfig.hpp
├── pugixml.cpp
└── pugixml.hpp
├── specs
├── NativeEpub.ts
├── NativeFile.ts
├── NativeVolumeButtonListener.ts
└── NativeZipArchive.ts
├── src
├── api
│ ├── constants.ts
│ ├── drive
│ │ ├── index.ts
│ │ ├── request.ts
│ │ └── types.ts
│ └── remote
│ │ └── index.ts
├── components
│ ├── Actionbar
│ │ └── Actionbar.tsx
│ ├── AppErrorBoundary
│ │ └── AppErrorBoundary.tsx
│ ├── Appbar
│ │ └── Appbar.tsx
│ ├── BottomSheet
│ │ ├── BottomSheet.tsx
│ │ └── BottomSheetBackdrop.tsx
│ ├── Button
│ │ └── Button.tsx
│ ├── Checkbox
│ │ └── Checkbox.tsx
│ ├── Chip
│ │ └── Chip.tsx
│ ├── ColorPickerModal
│ │ └── ColorPickerModal.tsx
│ ├── ColorPreferenceItem
│ │ └── ColorPreferenceItem.tsx
│ ├── Common.tsx
│ ├── Common
│ │ └── ToggleButton.tsx
│ ├── ConfirmationDialog
│ │ └── ConfirmationDialog.tsx
│ ├── Context
│ │ ├── LibraryContext.tsx
│ │ └── UpdateContext.tsx
│ ├── EmptyView.tsx
│ ├── EmptyView
│ │ └── EmptyView.tsx
│ ├── ErrorScreenV2
│ │ └── ErrorScreenV2.tsx
│ ├── ErrorView
│ │ └── ErrorView.tsx
│ ├── IconButtonV2
│ │ └── IconButtonV2.tsx
│ ├── List
│ │ └── List.tsx
│ ├── ListView.tsx
│ ├── LoadingMoreIndicator
│ │ └── LoadingMoreIndicator.tsx
│ ├── LoadingScreenV2
│ │ └── LoadingScreenV2.tsx
│ ├── Modal
│ │ └── Modal.tsx
│ ├── NewUpdateDialog.tsx
│ ├── NovelCover.tsx
│ ├── NovelList.tsx
│ ├── RadioButton.tsx
│ ├── RadioButton
│ │ └── RadioButton.tsx
│ ├── SafeAreaView
│ │ └── SafeAreaView.tsx
│ ├── SearchbarV2
│ │ └── SearchbarV2.tsx
│ ├── Skeleton
│ │ ├── Skeleton.tsx
│ │ └── useLoadingColors.tsx
│ ├── Switch
│ │ ├── Switch.tsx
│ │ └── SwitchItem.tsx
│ ├── ThemePicker
│ │ └── ThemePicker.tsx
│ └── index.ts
├── database
│ ├── db.ts
│ ├── queries
│ │ ├── CategoryQueries.ts
│ │ ├── ChapterQueries.ts
│ │ ├── HistoryQueries.ts
│ │ ├── LibraryQueries.ts
│ │ ├── NovelQueries.ts
│ │ ├── RepositoryQueries.ts
│ │ └── StatsQueries.ts
│ ├── tables
│ │ ├── CategoryTable.ts
│ │ ├── ChapterTable.ts
│ │ ├── NovelCategoryTable.ts
│ │ ├── NovelTable.ts
│ │ └── RepositoryTable.ts
│ ├── types
│ │ └── index.ts
│ └── utils
│ │ ├── convertDateToISOString.ts
│ │ └── helpers.tsx
├── hooks
│ ├── common
│ │ ├── githubUpdateChecker.ts
│ │ ├── useBackHandler.ts
│ │ ├── useBoolean.ts
│ │ ├── useDeviceOrientation.ts
│ │ ├── useFullscreenMode.ts
│ │ ├── usePreviousRouteName.ts
│ │ └── useSearch.ts
│ ├── index.ts
│ └── persisted
│ │ ├── index.ts
│ │ ├── useCategories.ts
│ │ ├── useDownload.ts
│ │ ├── useHistory.ts
│ │ ├── useImport.ts
│ │ ├── useNovel.ts
│ │ ├── usePlugins.ts
│ │ ├── useSelfHost.ts
│ │ ├── useSettings.ts
│ │ ├── useTheme.ts
│ │ ├── useTracker.ts
│ │ ├── useUpdates.ts
│ │ └── useUserAgent.ts
├── navigators
│ ├── BottomNavigator.tsx
│ ├── Main.tsx
│ ├── MoreStack.tsx
│ ├── ReaderStack.tsx
│ └── types
│ │ └── index.ts
├── plugins
│ ├── helpers
│ │ ├── constants.ts
│ │ ├── fetch.ts
│ │ ├── isAbsoluteUrl.ts
│ │ └── storage.ts
│ ├── pluginManager.ts
│ └── types
│ │ ├── filterTypes.ts
│ │ └── index.ts
├── screens
│ ├── BrowseSourceScreen
│ │ ├── BrowseSourceScreen.tsx
│ │ ├── components
│ │ │ ├── FilterBottomSheet.tsx
│ │ │ └── filterUtils.ts
│ │ └── useBrowseSource.ts
│ ├── Categories
│ │ ├── CategoriesScreen.tsx
│ │ └── components
│ │ │ ├── AddCategoryModal.tsx
│ │ │ ├── CategoryCard.tsx
│ │ │ ├── CategorySkeletonLoading.tsx
│ │ │ └── DeleteCategoryModal.tsx
│ ├── GlobalSearchScreen
│ │ ├── GlobalSearchScreen.tsx
│ │ ├── components
│ │ │ └── GlobalSearchResultsList.tsx
│ │ └── hooks
│ │ │ └── useGlobalSearch.ts
│ ├── StatsScreen
│ │ └── StatsScreen.tsx
│ ├── WebviewScreen
│ │ ├── WebviewScreen.tsx
│ │ └── components
│ │ │ ├── Appbar.tsx
│ │ │ └── Menu.tsx
│ ├── browse
│ │ ├── BrowseScreen.tsx
│ │ ├── SourceNovels.tsx
│ │ ├── components
│ │ │ ├── AvailableTab.tsx
│ │ │ ├── InstalledTab.tsx
│ │ │ └── Modals
│ │ │ │ └── SourceSettings.tsx
│ │ ├── discover
│ │ │ ├── AniListTopNovels.tsx
│ │ │ ├── MalNovelCard
│ │ │ │ └── MalNovelCard.tsx
│ │ │ ├── MalTopNovels.tsx
│ │ │ ├── MyAnimeListScraper.ts
│ │ │ ├── TrackerCard.tsx
│ │ │ └── TrackerNovelCard
│ │ │ │ └── index.tsx
│ │ ├── globalsearch
│ │ │ └── GlobalSearchNovelCover.tsx
│ │ ├── loadingAnimation
│ │ │ ├── GlobalSearchSkeletonLoading.tsx
│ │ │ ├── LoadingNovel.tsx
│ │ │ ├── MalLoading.tsx
│ │ │ ├── SourceScreenSkeletonLoading.tsx
│ │ │ └── TrackerLoading.tsx
│ │ ├── migration
│ │ │ ├── Migration.tsx
│ │ │ ├── MigrationNovelList.tsx
│ │ │ ├── MigrationNovels.tsx
│ │ │ └── MigrationSourceItem.tsx
│ │ └── settings
│ │ │ ├── BrowseSettings.tsx
│ │ │ └── modals
│ │ │ └── ConcurrentSearchesModal.tsx
│ ├── history
│ │ ├── HistoryScreen.tsx
│ │ └── components
│ │ │ ├── ClearHistoryDialog.tsx
│ │ │ ├── HistoryCard
│ │ │ └── HistoryCard.tsx
│ │ │ └── HistorySkeletonLoading.tsx
│ ├── library
│ │ ├── LibraryScreen.tsx
│ │ ├── components
│ │ │ ├── Banner.tsx
│ │ │ ├── LibraryBottomSheet
│ │ │ │ └── LibraryBottomSheet.tsx
│ │ │ └── LibraryListView.tsx
│ │ ├── constants
│ │ │ └── constants.ts
│ │ └── hooks
│ │ │ └── useLibrary.ts
│ ├── more
│ │ ├── About.tsx
│ │ ├── DownloadsScreen.tsx
│ │ ├── MoreScreen.tsx
│ │ ├── TaskQueueScreen.tsx
│ │ └── components
│ │ │ ├── MoreHeader.tsx
│ │ │ └── RemoveDownloadsDialog.tsx
│ ├── novel
│ │ ├── NovelContext.tsx
│ │ ├── NovelScreen.tsx
│ │ └── components
│ │ │ ├── Chapter
│ │ │ └── ChapterDownloadButtons.tsx
│ │ │ ├── ChapterItem.tsx
│ │ │ ├── ChooseEpubLocationModal.tsx
│ │ │ ├── DownloadCustomChapterModal.tsx
│ │ │ ├── EditInfoModal.tsx
│ │ │ ├── EpubIconButton.tsx
│ │ │ ├── Info
│ │ │ ├── NovelInfoComponents.tsx
│ │ │ ├── NovelInfoHeader.tsx
│ │ │ └── ReadButton.tsx
│ │ │ ├── JumpToChapterModal.tsx
│ │ │ ├── LoadingAnimation
│ │ │ └── NovelScreenLoading.tsx
│ │ │ ├── NovelAppbar.tsx
│ │ │ ├── NovelBottomSheet.tsx
│ │ │ ├── NovelDrawer.tsx
│ │ │ ├── NovelScreenButtonGroup
│ │ │ └── NovelScreenButtonGroup.tsx
│ │ │ ├── NovelScreenList.tsx
│ │ │ ├── NovelSummary
│ │ │ └── NovelSummary.tsx
│ │ │ ├── SetCategoriesModal.tsx
│ │ │ └── Tracker
│ │ │ ├── AniList.js
│ │ │ ├── MyAnimeList.js
│ │ │ ├── SetTrackChaptersDialog.js
│ │ │ ├── SetTrackScoreDialog.js
│ │ │ ├── SetTrackStatusDialog.js
│ │ │ ├── TrackSearchDialog.js
│ │ │ ├── TrackSheet.tsx
│ │ │ └── TrackerCards.js
│ ├── onboarding
│ │ ├── OnboardingScreen.tsx
│ │ └── PickThemeStep.tsx
│ ├── reader
│ │ ├── ChapterContext.tsx
│ │ ├── ChapterLoadingScreen
│ │ │ └── ChapterLoadingScreen.tsx
│ │ ├── ReaderScreen.tsx
│ │ ├── components
│ │ │ ├── ChapterDrawer
│ │ │ │ ├── RenderListChapter.tsx
│ │ │ │ └── index.tsx
│ │ │ ├── KeepScreenAwake.tsx
│ │ │ ├── ReaderAppbar.tsx
│ │ │ ├── ReaderBottomSheet
│ │ │ │ ├── ReaderBottomSheet.tsx
│ │ │ │ ├── ReaderFontPicker.tsx
│ │ │ │ ├── ReaderSheetPreferenceItem.tsx
│ │ │ │ ├── ReaderTextAlignSelector.tsx
│ │ │ │ ├── ReaderThemeSelector.tsx
│ │ │ │ ├── ReaderValueChange.tsx
│ │ │ │ └── TextSizeSlider.tsx
│ │ │ ├── ReaderFooter.tsx
│ │ │ ├── SkeletonLines.tsx
│ │ │ └── WebViewReader.tsx
│ │ ├── hooks
│ │ │ └── useChapter.ts
│ │ └── utils
│ │ │ └── sanitizeChapterText.ts
│ ├── settings
│ │ ├── SettingsAdvancedScreen.tsx
│ │ ├── SettingsAppearanceScreen.tsx
│ │ ├── SettingsBackupScreen
│ │ │ ├── Components
│ │ │ │ ├── GoogleDriveModal.tsx
│ │ │ │ └── SelfHostModal.tsx
│ │ │ └── index.tsx
│ │ ├── SettingsGeneralScreen
│ │ │ ├── SettingsGeneralScreen.tsx
│ │ │ └── modals
│ │ │ │ ├── DisplayModeModal.tsx
│ │ │ │ ├── GridSizeModal.tsx
│ │ │ │ ├── NovelBadgesModal.tsx
│ │ │ │ └── NovelSortModal.tsx
│ │ ├── SettingsLibraryScreen
│ │ │ ├── DefaultCategoryDialog.tsx
│ │ │ └── SettingsLibraryScreen.tsx
│ │ ├── SettingsReaderScreen
│ │ │ ├── Modals
│ │ │ │ ├── CustomFileModal.tsx
│ │ │ │ ├── FontPickerModal.tsx
│ │ │ │ └── VoicePickerModal.tsx
│ │ │ ├── ReaderTextSize.tsx
│ │ │ ├── Settings
│ │ │ │ ├── CustomCSSSettings.tsx
│ │ │ │ ├── CustomJSSettings.tsx
│ │ │ │ ├── DisplaySettings.tsx
│ │ │ │ ├── GeneralSettings.tsx
│ │ │ │ ├── ReaderThemeSettings.tsx
│ │ │ │ └── TextToSpeechSettings.tsx
│ │ │ ├── SettingsReaderScreen.tsx
│ │ │ └── utils.ts
│ │ ├── SettingsRepositoryScreen
│ │ │ ├── SettingsRepositoryScreen.tsx
│ │ │ └── components
│ │ │ │ ├── AddRepositoryModal.tsx
│ │ │ │ ├── DeleteRepositoryModal.tsx
│ │ │ │ └── RepositoryCard.tsx
│ │ ├── SettingsScreen.tsx
│ │ ├── SettingsTrackerScreen.tsx
│ │ └── components
│ │ │ ├── ConnectionModal.tsx
│ │ │ ├── DefaultChapterSortModal.tsx
│ │ │ └── SettingSwitch.tsx
│ └── updates
│ │ ├── UpdatesScreen.tsx
│ │ └── components
│ │ ├── UpdateNovelCard.tsx
│ │ └── UpdatesSkeletonLoading.tsx
├── services
│ ├── ServiceManager.ts
│ ├── Trackers
│ │ ├── aniList.ts
│ │ ├── index.ts
│ │ └── myAnimeList.ts
│ ├── backup
│ │ ├── drive
│ │ │ └── index.ts
│ │ ├── legacy
│ │ │ └── index.ts
│ │ ├── selfhost
│ │ │ └── index.ts
│ │ ├── types.ts
│ │ └── utils.ts
│ ├── download
│ │ └── downloadChapter.ts
│ ├── epub
│ │ └── import.ts
│ ├── migrate
│ │ └── migrateNovel.ts
│ ├── plugin
│ │ └── fetch.ts
│ └── updates
│ │ ├── LibraryUpdateQueries.ts
│ │ └── index.ts
├── theme
│ ├── colors.ts
│ ├── md3
│ │ ├── defaultTheme.ts
│ │ ├── index.ts
│ │ ├── lavender.ts
│ │ ├── mignightDusk.ts
│ │ ├── strawberry.ts
│ │ ├── tako.ts
│ │ ├── tealTurquoise.ts
│ │ └── yotsuba.ts
│ ├── types
│ │ └── index.ts
│ └── utils
│ │ └── setBarColor.ts
├── type
│ └── icon.ts
└── utils
│ ├── Storages.ts
│ ├── askForPostNoftificationsPermission.ts
│ ├── compareVersion.ts
│ ├── constants
│ ├── languages.ts
│ └── readerConstants.ts
│ ├── error.ts
│ ├── fetch
│ └── fetch.ts
│ ├── mmkv
│ └── mmkv.ts
│ ├── parseChapterNumber.ts
│ ├── showToast.ts
│ ├── sleep.ts
│ ├── translateEnum.ts
│ └── useLoadingColors.ts
├── strings
├── languages
│ ├── af_ZA
│ │ └── strings.json
│ ├── ar_SA
│ │ └── strings.json
│ ├── as_IN
│ │ └── strings.json
│ ├── ca_ES
│ │ └── strings.json
│ ├── cs_CZ
│ │ └── strings.json
│ ├── da_DK
│ │ └── strings.json
│ ├── de_DE
│ │ └── strings.json
│ ├── el_GR
│ │ └── strings.json
│ ├── en
│ │ └── strings.json
│ ├── es_ES
│ │ └── strings.json
│ ├── fi_FI
│ │ └── strings.json
│ ├── fr_FR
│ │ └── strings.json
│ ├── he_IL
│ │ └── strings.json
│ ├── hi_IN
│ │ └── strings.json
│ ├── hu_HU
│ │ └── strings.json
│ ├── id_ID
│ │ └── strings.json
│ ├── it_IT
│ │ └── strings.json
│ ├── ja_JP
│ │ └── strings.json
│ ├── ko_KR
│ │ └── strings.json
│ ├── nl_NL
│ │ └── strings.json
│ ├── no_NO
│ │ └── strings.json
│ ├── or_IN
│ │ └── strings.json
│ ├── pl_PL
│ │ └── strings.json
│ ├── pt_BR
│ │ └── strings.json
│ ├── pt_PT
│ │ └── strings.json
│ ├── ro_RO
│ │ └── strings.json
│ ├── ru_RU
│ │ └── strings.json
│ ├── sq_AL
│ │ └── strings.json
│ ├── sr_SP
│ │ └── strings.json
│ ├── sv_SE
│ │ └── strings.json
│ ├── tr_TR
│ │ └── strings.json
│ ├── uk_UA
│ │ └── strings.json
│ ├── vi_VN
│ │ └── strings.json
│ ├── zh_CN
│ │ └── strings.json
│ └── zh_TW
│ │ └── strings.json
├── translations.ts
└── types
│ └── index.ts
└── tsconfig.json
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | extends: '@react-native',
4 | overrides: [
5 | {
6 | files: ['*.js', '*.jsx', '*.ts', '*.tsx'],
7 | rules: {
8 | 'no-shadow': 'off',
9 | 'no-undef': 'off',
10 | 'no-console': 'error',
11 | '@typescript-eslint/no-shadow': 'warn',
12 | 'react-hooks/exhaustive-deps': 'warn',
13 | 'curly': ['error', 'multi-line', 'consistent'],
14 | 'no-useless-return': 'error',
15 | 'block-scoped-var': 'error',
16 | 'no-var': 'error',
17 | 'prefer-const': 'error',
18 | 'no-dupe-else-if': 'error',
19 | 'no-duplicate-imports': 'error',
20 | },
21 | },
22 | ],
23 | };
24 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/request_feature.yml:
--------------------------------------------------------------------------------
1 | # Cloned from tachiyomi
2 |
3 | name: Feature request
4 | description: Suggest a feature to improve LNReader
5 | labels: [Feature Request]
6 | body:
7 | - type: textarea
8 | id: feature-description
9 | attributes:
10 | label: Describe your suggested feature
11 | description: How can LNReader be improved?
12 | placeholder: |
13 | Example:
14 | "It should work like this..."
15 | validations:
16 | required: true
17 |
18 | - type: textarea
19 | id: other-details
20 | attributes:
21 | label: Other details
22 | placeholder: |
23 | Additional details and attachments.
24 |
25 | - type: checkboxes
26 | id: acknowledgements
27 | attributes:
28 | label: Acknowledgements
29 | description: Read this carefully, we will close and ignore your issue if you skimmed through this.
30 | options:
31 | - label: I have searched the existing issues and this is a new ticket, **NOT** a duplicate or related to another open or closed issue.
32 | required: true
33 | - label: I have written a short but informative title.
34 | required: true
35 | - label: If this is an issue with a source, I should be opening an issue in the [sources repository](https://github.com/LNReader/lnreader-sources/issues/new/choose).
36 | required: true
37 | - label: I have updated the app to version **[2.0.0](https://github.com/LNReader/lnreader/releases/latest)**.
38 | required: true
39 | - label: I will fill out all of the requested information in this form.
40 | required: true
41 |
--------------------------------------------------------------------------------
/.github/readme-images/icon_alt.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LNReader/lnreader/9e81f805c0b5c4b1e2ec3b1c2357fa4320bfb8cb/.github/readme-images/icon_alt.png
--------------------------------------------------------------------------------
/.github/readme-images/screenshots.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LNReader/lnreader/9e81f805c0b5c4b1e2ec3b1c2357fa4320bfb8cb/.github/readme-images/screenshots.png
--------------------------------------------------------------------------------
/.github/workflows/issue_moderator.yml:
--------------------------------------------------------------------------------
1 | # Cloned from tachiyomi
2 |
3 | name: Issue moderator
4 |
5 | on:
6 | issues:
7 | types: [opened, edited, reopened]
8 | issue_comment:
9 | types: [created]
10 |
11 | jobs:
12 | moderate:
13 | runs-on: ubuntu-latest
14 | steps:
15 | - name: Moderate issues
16 | uses: tachiyomiorg/issue-moderator-action@v1
17 | with:
18 | repo-token: ${{ secrets.GITHUB_TOKEN }}
19 | auto-close-rules: |
20 | [
21 | {
22 | "type": "body",
23 | "regex": ".*DELETE THIS SECTION IF YOU HAVE READ AND ACKNOWLEDGED IT.*",
24 | "message": "The acknowledgment section was not removed."
25 | },
26 | {
27 | "type": "body",
28 | "regex": ".*\\* (LNReader version|Android version|Device): \\?.*",
29 | "message": "Requested information in the template was not filled out."
30 | }
31 | ]
32 |
--------------------------------------------------------------------------------
/.github/workflows/lint.yml:
--------------------------------------------------------------------------------
1 | name: Lint
2 | on:
3 | push:
4 | branches:
5 | - master
6 | pull_request:
7 | branches:
8 | - master
9 |
10 | jobs:
11 | build:
12 | name: ESLint
13 | runs-on: ubuntu-latest
14 | steps:
15 | - uses: actions/checkout@v4
16 | - uses: actions/setup-node@v4
17 | with:
18 | node-version: '20'
19 | - name: Install modules
20 | run: npm install
21 | - name: Run ESLint
22 | run: npm run lint
23 |
--------------------------------------------------------------------------------
/.github/workflows/types.yml:
--------------------------------------------------------------------------------
1 | name: Typescript
2 | on:
3 | push:
4 | branches:
5 | - master
6 | pull_request:
7 | branches:
8 | - master
9 |
10 | jobs:
11 | build:
12 | name: Types Checking
13 | runs-on: ubuntu-latest
14 | steps:
15 | - uses: actions/checkout@v4
16 | - uses: actions/setup-node@v4
17 | with:
18 | node-version: '20'
19 | - name: Install modules
20 | run: npm install
21 | - name: Check Types
22 | run: npm run type-check
23 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/**/*
2 | .expo/*
3 | npm-debug.*
4 | *.jks
5 | *.p8
6 | *.p12
7 | *.key
8 | *.mobileprovision
9 | *.orig.*
10 | web-build/
11 | .env
12 | reader_playground/index.html
13 | # macOS
14 | .DS_Store
15 |
16 | # @generated expo-cli sync-2138f1e3e130677ea10ea873f6d498e3890e677b
17 | # The following patterns were generated by expo-cli
18 |
19 | # OSX
20 | #
21 | .DS_Store
22 |
23 | # Xcode
24 | #
25 | build/
26 | *.pbxuser
27 | !default.pbxuser
28 | *.mode1v3
29 | !default.mode1v3
30 | *.mode2v3
31 | !default.mode2v3
32 | *.perspectivev3
33 | !default.perspectivev3
34 | xcuserdata
35 | *.xccheckout
36 | *.moved-aside
37 | DerivedData
38 | *.hmap
39 | *.ipa
40 | *.xcuserstate
41 | **/.xcode.env.local
42 |
43 | # Android/IntelliJ
44 | #
45 | build/
46 | .idea
47 | .gradle
48 | local.properties
49 | *.iml
50 | *.hprof
51 | .cxx/
52 | *.keystore
53 | !debug.keystore
54 | .kotlin/
55 |
56 | # node.js
57 | #
58 | node_modules/
59 | npm-debug.log
60 | yarn-error.log
61 |
62 | # BUCK
63 | buck-out/
64 | \.buckd/
65 | *.keystore
66 | !debug.keystore
67 |
68 | # Bundle artifacts
69 | *.jsbundle
70 |
71 | # CocoaPods
72 | /ios/Pods/
73 | Gemfile.lock
74 |
75 | # Expo
76 | .expo/
77 | web-build/
78 |
79 | # @end expo-cli
80 |
81 | .vscode/
82 | src/sources/en/generators/
83 | *.lockb
84 | .tool-versions
--------------------------------------------------------------------------------
/.husky/.gitignore:
--------------------------------------------------------------------------------
1 | _
2 |
--------------------------------------------------------------------------------
/.husky/pre-commit:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | . "$(dirname "$0")/_/husky.sh"
3 |
4 | npx lint-staged
5 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | **/node_modules
2 | android/**/js/van.js
3 | android/**/js/text-vibe.js
4 |
--------------------------------------------------------------------------------
/.prettierrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | tabWidth: 2,
3 | bracketSpacing: true,
4 | useTabs: false,
5 | bracketSameLine: false,
6 | singleQuote: true,
7 | trailingComma: 'all',
8 | arrowParens: 'avoid',
9 | quoteProps: 'preserve',
10 | endOfLine: 'auto', // stop prettier from getting mad on windows
11 | };
12 |
--------------------------------------------------------------------------------
/App.tsx:
--------------------------------------------------------------------------------
1 | import 'react-native-url-polyfill/auto';
2 | import { enableFreeze } from 'react-native-screens';
3 |
4 | enableFreeze(true);
5 |
6 | import React from 'react';
7 | import { StatusBar, StyleSheet } from 'react-native';
8 | import { GestureHandlerRootView } from 'react-native-gesture-handler';
9 | import LottieSplashScreen from 'react-native-lottie-splash-screen';
10 | import { SafeAreaProvider } from 'react-native-safe-area-context';
11 | import { Provider as PaperProvider } from 'react-native-paper';
12 | import * as Notifications from 'expo-notifications';
13 |
14 | import { createTables } from '@database/db';
15 | import AppErrorBoundary from '@components/AppErrorBoundary/AppErrorBoundary';
16 |
17 | import Main from './src/navigators/Main';
18 | import { BottomSheetModalProvider } from '@gorhom/bottom-sheet';
19 |
20 | Notifications.setNotificationHandler({
21 | handleNotification: async () => {
22 | return {
23 | shouldPlaySound: true,
24 | shouldSetBadge: true,
25 | shouldShowBanner: true,
26 | shouldShowList: true,
27 | };
28 | },
29 | });
30 | createTables();
31 | LottieSplashScreen.hide();
32 |
33 | const App = () => {
34 | return (
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 | );
48 | };
49 |
50 | export default App;
51 |
52 | const styles = StyleSheet.create({
53 | flex: {
54 | flex: 1,
55 | },
56 | });
57 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing Guide
2 |
3 | Contributions are welcome and are greatly appreciated!
4 |
5 | ## Setting up your environment
6 |
7 | After forking to your own github org or account, do the following steps to get started:
8 |
9 | ```bash
10 | # prerequisites
11 | node --version >= 20 (for version management, get nvm [recommended])
12 | java sdk --version >= 17 (for version management, get jenv [optional])
13 | android sdk (https://developer.android.com/studio)
14 |
15 | # clone your fork to your local machine
16 | git clone https://github.com//lnreader.git
17 |
18 | # step into local repo
19 | cd lnreader
20 |
21 | # install dependencies
22 | npm install
23 |
24 | # build the apk (the built apk will be found in ~/lnreader/android/app/build/outputs/apk/release/)
25 | npm run buildRelease
26 | ```
27 |
28 | ### Developing on Android
29 |
30 | You will need an Android device or emulator connected to your computer as well as an IDE of your choice. (eg: vscode)
31 |
32 | ```bash
33 | # prerequisites
34 | adb (https://developer.android.com/studio/command-line/adb)
35 | IDE
36 |
37 | # check if android device/emulator is connected
38 | adb devices
39 |
40 | # run metro for development
41 | npm start
42 |
43 | # then to view on your android device (new terminal)
44 | npm run android
45 | ```
46 |
47 | ### Style & Linting
48 |
49 | This codebase's linting rules are enforced using [ESLint](http://eslint.org/).
50 |
51 | It is recommended that you install an eslint plugin for your editor of choice when working on this
52 | codebase, however you can always check to see if the source code is compliant by running:
53 |
54 | ```bash
55 | npm run lint
56 | ```
57 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source 'https://rubygems.org'
2 |
3 | # You may use http://rbenv.org/ or https://rvm.io/ to install and use this version
4 | ruby ">= 2.6.10"
5 |
6 | # Exclude problematic versions of cocoapods and activesupport that causes build failures.
7 | gem 'cocoapods', '>= 1.13', '!= 1.15.0', '!= 1.15.1'
8 | gem 'activesupport', '>= 6.1.7.5', '!= 7.1.0'
9 | gem 'xcodeproj', '< 1.26.0'
10 | gem 'concurrent-ruby', '< 1.3.4'
11 |
12 | # Ruby 3.4.0 has removed some libraries from the standard library.
13 | gem 'bigdecimal'
14 | gem 'logger'
15 | gem 'benchmark'
16 | gem 'mutex_m'
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Rajarshee Chatterjee
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/android/app/debug.keystore:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LNReader/lnreader/9e81f805c0b5c4b1e2ec3b1c2357fa4320bfb8cb/android/app/debug.keystore
--------------------------------------------------------------------------------
/android/app/google-services.json:
--------------------------------------------------------------------------------
1 | {
2 | "project_info": {
3 | "project_number": "523872485654",
4 | "project_id": "lnreader-backup",
5 | "storage_bucket": "lnreader-backup.appspot.com"
6 | },
7 | "client": [
8 | {
9 | "client_info": {
10 | "mobilesdk_app_id": "1:523872485654:android:169cd062458db962ce1037",
11 | "android_client_info": {
12 | "package_name": "com.rajarsheechatterjee.LNReader"
13 | }
14 | },
15 | "oauth_client": [
16 | {
17 | "client_id": "523872485654-14jtut8orr7dbrk2chea279d7k8889sr.apps.googleusercontent.com",
18 | "client_type": 1,
19 | "android_info": {
20 | "package_name": "com.rajarsheechatterjee.LNReader",
21 | "certificate_hash": "5e8f16062ea3cd2c4a0d547876baa6f38cabf625"
22 | }
23 | },
24 | {
25 | "client_id": "523872485654-liarmq8nl0g5an2cki3bpg9jc0d8a21j.apps.googleusercontent.com",
26 | "client_type": 3
27 | }
28 | ],
29 | "api_key": [
30 | {
31 | "current_key": "AIzaSyBH_j-0Jyo9aFJ_KV8Nr3te2hs_L_ZYhrE"
32 | }
33 | ],
34 | "services": {
35 | "appinvite_service": {
36 | "other_platform_oauth_client": [
37 | {
38 | "client_id": "523872485654-liarmq8nl0g5an2cki3bpg9jc0d8a21j.apps.googleusercontent.com",
39 | "client_type": 3
40 | }
41 | ]
42 | }
43 | }
44 | }
45 | ],
46 | "configuration_version": "1"
47 | }
48 |
--------------------------------------------------------------------------------
/android/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # By default, the flags in this file are appended to flags specified
3 | # in /usr/local/Cellar/android-sdk/24.3.3/tools/proguard/proguard-android.txt
4 | # You can edit the include path and order by changing the proguardFiles
5 | # directive in build.gradle.
6 | #
7 | # For more details, see
8 | # http://developer.android.com/guide/developing/tools/proguard.html
9 |
10 | # Add any project specific keep options here:
11 | -keep class com.swmansion.reanimated.** { *; }
12 | -keep class com.facebook.react.turbomodule.** { *; }
13 | -keep class com.facebook.hermes.unicode.** { *; }
14 | -keep class com.facebook.jni.** { *; }
15 |
--------------------------------------------------------------------------------
/android/app/src/debug/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
9 |
--------------------------------------------------------------------------------
/android/app/src/debug/res/values/string.xml:
--------------------------------------------------------------------------------
1 |
2 | LNReader debug
3 |
--------------------------------------------------------------------------------
/android/app/src/main/assets/css/pageReader.css:
--------------------------------------------------------------------------------
1 | body.page-reader {
2 | overflow: hidden;
3 | padding-bottom: unset;
4 | }
5 |
6 | body.page-reader > #LNReader-chapter {
7 | height: calc(90vh);
8 | column-width: calc(100vw - var(--readerSettings-padding) * 2);
9 | column-gap: calc(var(--readerSettings-padding) * 2);
10 | transition: 200ms;
11 | }
12 | .transition-chapter {
13 | height: 100vh;
14 | width: 100vw;
15 | max-width: 100vw;
16 |
17 | position: fixed;
18 | left: 100vw;
19 | top: 0;
20 |
21 | box-sizing: border-box;
22 | padding: var(--readerSettings-padding);
23 | padding-top: 30vh;
24 |
25 | text-align: center;
26 | font-size: 2rem;
27 | z-index: 888888;
28 | background-color: var(--readerSettings-theme);
29 | }
30 |
--------------------------------------------------------------------------------
/android/app/src/main/assets/css/tts.css:
--------------------------------------------------------------------------------
1 | #TTS-Controller {
2 | position: fixed;
3 | top: 50%;
4 | left: 20px;
5 | opacity: 0.5;
6 | }
7 |
8 | #TTS-Controller button {
9 | background: var(--theme-surface-0-9);
10 | outline: none;
11 | border-width: 0;
12 | display: flex;
13 | justify-items: center;
14 | align-items: center;
15 | padding: 4px;
16 | border-radius: 100%;
17 | transition: 0.5s;
18 | }
19 |
20 | #TTS-Controller.active {
21 | opacity: 1;
22 | }
23 |
24 | #TTS-Controller.active button {
25 | padding: 16px;
26 | }
27 |
28 | #TTS-Controller svg {
29 | fill: var(--theme-onSurface);
30 | width: 20px;
31 | height: 20px;
32 | }
33 |
34 | #TTS-Controller.active svg {
35 | width: 24px;
36 | height: 24px;
37 | }
38 |
--------------------------------------------------------------------------------
/android/app/src/main/assets/fonts/OpenDyslexic3-Regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LNReader/lnreader/9e81f805c0b5c4b1e2ec3b1c2357fa4320bfb8cb/android/app/src/main/assets/fonts/OpenDyslexic3-Regular.ttf
--------------------------------------------------------------------------------
/android/app/src/main/assets/fonts/arbutus-slab.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LNReader/lnreader/9e81f805c0b5c4b1e2ec3b1c2357fa4320bfb8cb/android/app/src/main/assets/fonts/arbutus-slab.ttf
--------------------------------------------------------------------------------
/android/app/src/main/assets/fonts/domine.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LNReader/lnreader/9e81f805c0b5c4b1e2ec3b1c2357fa4320bfb8cb/android/app/src/main/assets/fonts/domine.ttf
--------------------------------------------------------------------------------
/android/app/src/main/assets/fonts/lato.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LNReader/lnreader/9e81f805c0b5c4b1e2ec3b1c2357fa4320bfb8cb/android/app/src/main/assets/fonts/lato.ttf
--------------------------------------------------------------------------------
/android/app/src/main/assets/fonts/lora.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LNReader/lnreader/9e81f805c0b5c4b1e2ec3b1c2357fa4320bfb8cb/android/app/src/main/assets/fonts/lora.ttf
--------------------------------------------------------------------------------
/android/app/src/main/assets/fonts/noto-sans.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LNReader/lnreader/9e81f805c0b5c4b1e2ec3b1c2357fa4320bfb8cb/android/app/src/main/assets/fonts/noto-sans.ttf
--------------------------------------------------------------------------------
/android/app/src/main/assets/fonts/nunito.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LNReader/lnreader/9e81f805c0b5c4b1e2ec3b1c2357fa4320bfb8cb/android/app/src/main/assets/fonts/nunito.ttf
--------------------------------------------------------------------------------
/android/app/src/main/assets/fonts/open-sans.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LNReader/lnreader/9e81f805c0b5c4b1e2ec3b1c2357fa4320bfb8cb/android/app/src/main/assets/fonts/open-sans.ttf
--------------------------------------------------------------------------------
/android/app/src/main/assets/fonts/pt-sans-bold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LNReader/lnreader/9e81f805c0b5c4b1e2ec3b1c2357fa4320bfb8cb/android/app/src/main/assets/fonts/pt-sans-bold.ttf
--------------------------------------------------------------------------------
/android/app/src/main/assets/fonts/pt-serif.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LNReader/lnreader/9e81f805c0b5c4b1e2ec3b1c2357fa4320bfb8cb/android/app/src/main/assets/fonts/pt-serif.ttf
--------------------------------------------------------------------------------
/android/app/src/main/assets/js/icons.js:
--------------------------------------------------------------------------------
1 | const volumnIcon =
2 | '';
3 | const pauseIcon =
4 | '';
5 | const resumeIcon =
6 | '';
7 |
--------------------------------------------------------------------------------
/android/app/src/main/assets/js/index.d.ts:
--------------------------------------------------------------------------------
1 | import { ChapterInfo, NovelInfo } from '@database/types';
2 | import {
3 | ChapterGeneralSettings,
4 | ChapterReaderSettings,
5 | } from '@hooks/persisted/useSettings';
6 | import { State } from './van';
7 |
8 | export interface Reader {
9 | // element
10 | chapterElement: HTMLElement;
11 | viewport: HTMLMetaElement;
12 | selection: Selection;
13 |
14 | // state
15 | hidden: State;
16 | generalSettings: State;
17 | readerSettings: State;
18 | batteryLevel: State;
19 |
20 | novel: NovelInfo;
21 | chapter: ChapterInfo;
22 | nextChapter?: ChapterInfo;
23 | autoSaveInterval: number;
24 | rawHTML: string;
25 | strings: {
26 | finished: string;
27 | nextChapter: string;
28 | noNextChapter: string;
29 | };
30 |
31 | //layout props
32 | paddingTop: number;
33 | layoutHeight: number;
34 | layoutWidth: number;
35 | chapterHeight: number;
36 | chapterWidth: number;
37 |
38 | post: (obj: Record) => void;
39 | refresh: () => void;
40 | }
41 |
42 | interface PageReader {
43 | page: State;
44 | totalPages: State;
45 | movePage: (page: number) => void;
46 | }
47 |
48 | interface TTS {
49 | started: boolean;
50 | reading: boolean;
51 | start: (element?: HTMLElement) => void;
52 | resume: () => void;
53 | stop: () => void;
54 | pause: () => void;
55 | readable: (element?: HTMLElement) => void;
56 | }
57 |
58 | declare global {
59 | const reader: Reader;
60 | const tts: TTS;
61 | const pageReader: PageReader;
62 | }
63 |
--------------------------------------------------------------------------------
/android/app/src/main/assets/js/text-vibe.js:
--------------------------------------------------------------------------------
1 | var textVide = function(L){'use strict'; const f = t=>t == null || t === '',h = (t,n)=>Object.keys(t).reduce((e,s)=>(n(e[s]) && delete e[s],e),t),I = (t,n)=>({...n,...h(t,f)}),_ = ['',''],y = t=>I(t,{sep:_,fixationPoint:1,ignoreHtmlTag:!0,ignoreHtmlEntity:!0}),u = [[0,4,12,17,24,29,35,42,48],[1,2,7,10,13,14,19,22,25,28,31,34,37,40,43,46,49],[1,2,5,7,9,11,13,15,17,19,21,23,25,27,29,31,33,35,37,39,41,43,45,47,49],[0,2,4,5,6,8,9,11,14,15,17,18,20,0,21,23,24,26,27,29,30,32,33,35,36,38,39,41,42,44,45,47,48],[0,2,3,5,6,7,8,10,11,12,14,15,17,19,20,21,23,24,25,26,28,29,30,32,33,34,35,37,38,39,41,42,43,44,46,47,48]],A = (t,n)=>{const {length:i} = t,e = u[n - 1] ?? u[0],s = e.findIndex(c=>i <= c); let o = i - s; return s === -1 && (o = i - e.length),Math.max(o,0);},R = (t,n)=>typeof n === 'string' ? `${n}${t}${n}` : `${n[0]}${t}${n[1]}`,m = t=>Array.from(t).map(n=>{const i = n.index,[e] = n,{length:s} = e; return [i,i + s - 1];}),H = /()|(<[^>]*>)/g,N = t=>{const n = t.matchAll(H),e = m(n).reverse(); return s=>{const o = s.index,c = e.find(([r])=>o > r); if (!c) {return !1;} const [,a] = c; return o < a;};},O = /(&[\w#]+;)/g,M = t=>{const n = t.matchAll(O),e = m(n).reverse(); return s=>{const o = s.index,c = e.find(([r])=>o > r); if (!c) {return !1;} const [,a] = c; return o < a;};},x = /(\p{L}|\p{Nd})*\p{L}(\p{L}|\p{Nd})*/gu,F = (t,n = {})=>{if (!(t != null && t.length)) {return '';} const {fixationPoint:i,sep:e,ignoreHtmlTag:s,ignoreHtmlEntity:o} = y(n),c = Array.from(t.matchAll(x)); let a = '',l = 0,r; s && (r = N(t)); let d; o && (d = M(t)); for (const g of c){if ((r == null ? void 0 : r(g)) || (d == null ? void 0 : d(g))) {continue;} const [D] = g,T = g.index,E = T + A(D,i),U = t.slice(l,T); a += U,T !== E && (a += R(t.slice(T,E),e)),l = E;} const G = t.slice(l); return a + G;}; return L.textVide = F,Object.defineProperty(L,Symbol.toStringTag,{value:'Module'}),L;}({});
2 |
--------------------------------------------------------------------------------
/android/app/src/main/assets/js/van.d.ts:
--------------------------------------------------------------------------------
1 | import 'typescript';
2 | import 'typescript/lib/lib.dom';
3 | export interface State {
4 | val: T;
5 | readonly oldVal: T;
6 | readonly rawVal: T;
7 | }
8 |
9 | // Defining readonly view of State for covariance.
10 | // Basically we want StateView to implement StateView
11 | export type StateView = Readonly>;
12 |
13 | export type Val = State | T;
14 |
15 | export type Primitive = string | number | boolean | bigint;
16 |
17 | export type PropValue = Primitive | ((e: any) => void) | null;
18 |
19 | export type PropValueOrDerived =
20 | | PropValue
21 | | StateView
22 | | (() => PropValue);
23 |
24 | export type Props = Record & {
25 | class?: PropValueOrDerived;
26 | };
27 |
28 | export type PropsWithKnownKeys = Partial<{
29 | [K in keyof ElementType]: PropValueOrDerived;
30 | }>;
31 |
32 | export type ValidChildDomValue = Primitive | Node | null | undefined;
33 |
34 | export type BindingFunc =
35 | | ((dom?: Node) => ValidChildDomValue)
36 | | ((dom?: Element) => Element);
37 |
38 | export type ChildDom =
39 | | ValidChildDomValue
40 | | StateView
41 | | BindingFunc
42 | | readonly ChildDom[];
43 |
44 | export type TagFunc = (
45 | first?: (Props & PropsWithKnownKeys) | ChildDom,
46 | ...rest: readonly ChildDom[]
47 | ) => Result;
48 |
49 | type Tags = Readonly>> & {
50 | [K in keyof HTMLElementTagNameMap]: TagFunc;
51 | };
52 |
53 | declare function state(): State;
54 | declare function state(initVal: T): State;
55 |
56 | export interface Van {
57 | readonly state: typeof state;
58 | readonly derive: (f: () => T) => State;
59 | readonly add: (dom: Element, ...children: readonly ChildDom[]) => Element;
60 | readonly tags: Tags &
61 | ((namespaceURI: string) => Readonly>>);
62 | readonly hydrate: (
63 | dom: T,
64 | f: (dom: T) => T | null | undefined,
65 | ) => T;
66 | }
67 |
68 | declare global {
69 | const van: Van;
70 | }
71 |
--------------------------------------------------------------------------------
/android/app/src/main/java/com/rajarsheechatterjee/NativeFile/NativePackage.kt:
--------------------------------------------------------------------------------
1 | package com.rajarsheechatterjee.NativeFile
2 |
3 | import com.facebook.react.BaseReactPackage
4 | import com.facebook.react.bridge.NativeModule
5 | import com.facebook.react.bridge.ReactApplicationContext
6 | import com.facebook.react.module.model.ReactModuleInfo
7 | import com.facebook.react.module.model.ReactModuleInfoProvider
8 | import com.lnreader.spec.NativeFileSpec
9 |
10 | class NativePackage : BaseReactPackage() {
11 | override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? =
12 | if (name == NativeFileSpec.NAME) {
13 | NativeFile(reactContext)
14 | } else {
15 | null
16 | }
17 |
18 | override fun getReactModuleInfoProvider() = ReactModuleInfoProvider {
19 | mapOf(
20 | NativeFileSpec.NAME to ReactModuleInfo(
21 | NativeFileSpec.NAME,
22 | NativeFileSpec.NAME,
23 | canOverrideExistingModule = false,
24 | needsEagerInit = false,
25 | isCxxModule = false,
26 | isTurboModule = true
27 | )
28 | )
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/android/app/src/main/java/com/rajarsheechatterjee/NativeVolumeButtonListener/NativeVolumeButtonListener.kt:
--------------------------------------------------------------------------------
1 | package com.rajarsheechatterjee.NativeVolumeButtonListener
2 |
3 | import com.facebook.react.bridge.ReactApplicationContext
4 | import com.facebook.react.modules.core.DeviceEventManagerModule
5 | import com.lnreader.spec.NativeVolumeButtonListenerSpec
6 |
7 | class NativeVolumeButtonListener(appContext: ReactApplicationContext) :
8 | NativeVolumeButtonListenerSpec(appContext) {
9 | init {
10 | NativeVolumeButtonListener.appContext = appContext
11 | }
12 |
13 | override fun addListener(eventName: String?) {
14 | isActive = true
15 | }
16 |
17 | override fun removeListeners(count: Double) {
18 | isActive = false
19 | }
20 |
21 | companion object {
22 | lateinit var appContext: ReactApplicationContext
23 | var isActive = false
24 |
25 | fun sendEvent(up: Boolean) {
26 | if (isActive) {
27 | appContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
28 | .emit(if (up) "VolumeUp" else "VolumeDown", null)
29 | }
30 | }
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/android/app/src/main/java/com/rajarsheechatterjee/NativeVolumeButtonListener/NativeVolumeButtonListenerPackage.kt:
--------------------------------------------------------------------------------
1 | package com.rajarsheechatterjee.NativeVolumeButtonListener
2 |
3 | import com.facebook.react.BaseReactPackage
4 | import com.facebook.react.bridge.NativeModule
5 | import com.facebook.react.bridge.ReactApplicationContext
6 | import com.facebook.react.module.model.ReactModuleInfo
7 | import com.facebook.react.module.model.ReactModuleInfoProvider
8 | import com.lnreader.spec.NativeVolumeButtonListenerSpec
9 |
10 | class NativeVolumeButtonListenerPackage : BaseReactPackage() {
11 | override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? =
12 | if (name == NativeVolumeButtonListenerSpec.NAME) {
13 | NativeVolumeButtonListener(reactContext)
14 | } else {
15 | null
16 | }
17 |
18 | override fun getReactModuleInfoProvider() = ReactModuleInfoProvider {
19 | mapOf(
20 | NativeVolumeButtonListenerSpec.NAME to ReactModuleInfo(
21 | NativeVolumeButtonListenerSpec.NAME,
22 | NativeVolumeButtonListenerSpec.NAME,
23 | canOverrideExistingModule = false,
24 | needsEagerInit = false,
25 | isCxxModule = false,
26 | isTurboModule = true
27 | )
28 | )
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/android/app/src/main/java/com/rajarsheechatterjee/NativeZipArchive/NativeZipArchivePackage.kt:
--------------------------------------------------------------------------------
1 | package com.rajarsheechatterjee.NativeZipArchive
2 |
3 | import com.facebook.react.BaseReactPackage
4 | import com.facebook.react.bridge.NativeModule
5 | import com.facebook.react.bridge.ReactApplicationContext
6 | import com.facebook.react.module.model.ReactModuleInfo
7 | import com.facebook.react.module.model.ReactModuleInfoProvider
8 | import com.lnreader.spec.NativeZipArchiveSpec
9 |
10 | class NativeZipArchivePackage : BaseReactPackage() {
11 | override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? =
12 | if (name == NativeZipArchiveSpec.NAME) {
13 | NativeZipArchive(reactContext)
14 | } else {
15 | null
16 | }
17 |
18 | override fun getReactModuleInfoProvider() = ReactModuleInfoProvider {
19 | mapOf(
20 | NativeZipArchiveSpec.NAME to ReactModuleInfo(
21 | NativeZipArchiveSpec.NAME,
22 | NativeZipArchiveSpec.NAME,
23 | canOverrideExistingModule = false,
24 | needsEagerInit = false,
25 | isCxxModule = false,
26 | isTurboModule = true
27 | )
28 | )
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/android/app/src/main/jni/CMakeLists.txt:
--------------------------------------------------------------------------------
1 | cmake_minimum_required(VERSION 3.13)
2 |
3 | # Define the library name here.
4 | project(appmodules)
5 |
6 | # This file includes all the necessary to let you build your React Native application
7 | include(${REACT_ANDROID_DIR}/cmake-utils/ReactNative-application.cmake)
8 |
9 | # Define where the additional source code lives. We need to crawl back the jni, main, src, app, android folders
10 | target_sources(${CMAKE_PROJECT_NAME} PRIVATE
11 | ../../../../../shared/NativeEpub.cpp
12 | ../../../../../shared/Epub.cpp
13 | ../../../../../shared/pugixml.cpp
14 | )
15 |
16 | # Define where CMake can find the additional header files. We need to crawl back the jni, main, src, app, android folders
17 | target_include_directories(${CMAKE_PROJECT_NAME} PUBLIC ../../../../../shared)
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable-hdpi/ic_action_name.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LNReader/lnreader/9e81f805c0b5c4b1e2ec3b1c2357fa4320bfb8cb/android/app/src/main/res/drawable-hdpi/ic_action_name.png
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable-hdpi/notification_icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LNReader/lnreader/9e81f805c0b5c4b1e2ec3b1c2357fa4320bfb8cb/android/app/src/main/res/drawable-hdpi/notification_icon.png
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable-mdpi/ic_action_name.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LNReader/lnreader/9e81f805c0b5c4b1e2ec3b1c2357fa4320bfb8cb/android/app/src/main/res/drawable-mdpi/ic_action_name.png
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable-mdpi/notification_icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LNReader/lnreader/9e81f805c0b5c4b1e2ec3b1c2357fa4320bfb8cb/android/app/src/main/res/drawable-mdpi/notification_icon.png
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable-xhdpi/ic_action_name.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LNReader/lnreader/9e81f805c0b5c4b1e2ec3b1c2357fa4320bfb8cb/android/app/src/main/res/drawable-xhdpi/ic_action_name.png
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable-xhdpi/notification_icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LNReader/lnreader/9e81f805c0b5c4b1e2ec3b1c2357fa4320bfb8cb/android/app/src/main/res/drawable-xhdpi/notification_icon.png
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable-xxhdpi/ic_action_name.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LNReader/lnreader/9e81f805c0b5c4b1e2ec3b1c2357fa4320bfb8cb/android/app/src/main/res/drawable-xxhdpi/ic_action_name.png
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable-xxhdpi/notification_icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LNReader/lnreader/9e81f805c0b5c4b1e2ec3b1c2357fa4320bfb8cb/android/app/src/main/res/drawable-xxhdpi/notification_icon.png
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable-xxxhdpi/ic_action_name.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LNReader/lnreader/9e81f805c0b5c4b1e2ec3b1c2357fa4320bfb8cb/android/app/src/main/res/drawable-xxxhdpi/ic_action_name.png
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable-xxxhdpi/notification_icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LNReader/lnreader/9e81f805c0b5c4b1e2ec3b1c2357fa4320bfb8cb/android/app/src/main/res/drawable-xxxhdpi/notification_icon.png
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable/rn_edit_text_material.xml:
--------------------------------------------------------------------------------
1 |
2 |
13 |
18 |
19 |
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable/splashscreen.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/android/app/src/main/res/layout/launch_screen.xml:
--------------------------------------------------------------------------------
1 |
2 |
14 |
23 |
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-hdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LNReader/lnreader/9e81f805c0b5c4b1e2ec3b1c2357fa4320bfb8cb/android/app/src/main/res/mipmap-hdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-hdpi/ic_launcher_background.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LNReader/lnreader/9e81f805c0b5c4b1e2ec3b1c2357fa4320bfb8cb/android/app/src/main/res/mipmap-hdpi/ic_launcher_background.webp
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LNReader/lnreader/9e81f805c0b5c4b1e2ec3b1c2357fa4320bfb8cb/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-hdpi/ic_launcher_monochrome.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LNReader/lnreader/9e81f805c0b5c4b1e2ec3b1c2357fa4320bfb8cb/android/app/src/main/res/mipmap-hdpi/ic_launcher_monochrome.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LNReader/lnreader/9e81f805c0b5c4b1e2ec3b1c2357fa4320bfb8cb/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-mdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LNReader/lnreader/9e81f805c0b5c4b1e2ec3b1c2357fa4320bfb8cb/android/app/src/main/res/mipmap-mdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-mdpi/ic_launcher_background.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LNReader/lnreader/9e81f805c0b5c4b1e2ec3b1c2357fa4320bfb8cb/android/app/src/main/res/mipmap-mdpi/ic_launcher_background.webp
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LNReader/lnreader/9e81f805c0b5c4b1e2ec3b1c2357fa4320bfb8cb/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-mdpi/ic_launcher_monochrome.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LNReader/lnreader/9e81f805c0b5c4b1e2ec3b1c2357fa4320bfb8cb/android/app/src/main/res/mipmap-mdpi/ic_launcher_monochrome.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LNReader/lnreader/9e81f805c0b5c4b1e2ec3b1c2357fa4320bfb8cb/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LNReader/lnreader/9e81f805c0b5c4b1e2ec3b1c2357fa4320bfb8cb/android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xhdpi/ic_launcher_background.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LNReader/lnreader/9e81f805c0b5c4b1e2ec3b1c2357fa4320bfb8cb/android/app/src/main/res/mipmap-xhdpi/ic_launcher_background.webp
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LNReader/lnreader/9e81f805c0b5c4b1e2ec3b1c2357fa4320bfb8cb/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xhdpi/ic_launcher_monochrome.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LNReader/lnreader/9e81f805c0b5c4b1e2ec3b1c2357fa4320bfb8cb/android/app/src/main/res/mipmap-xhdpi/ic_launcher_monochrome.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LNReader/lnreader/9e81f805c0b5c4b1e2ec3b1c2357fa4320bfb8cb/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LNReader/lnreader/9e81f805c0b5c4b1e2ec3b1c2357fa4320bfb8cb/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LNReader/lnreader/9e81f805c0b5c4b1e2ec3b1c2357fa4320bfb8cb/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.webp
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LNReader/lnreader/9e81f805c0b5c4b1e2ec3b1c2357fa4320bfb8cb/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_monochrome.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LNReader/lnreader/9e81f805c0b5c4b1e2ec3b1c2357fa4320bfb8cb/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_monochrome.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LNReader/lnreader/9e81f805c0b5c4b1e2ec3b1c2357fa4320bfb8cb/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LNReader/lnreader/9e81f805c0b5c4b1e2ec3b1c2357fa4320bfb8cb/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LNReader/lnreader/9e81f805c0b5c4b1e2ec3b1c2357fa4320bfb8cb/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.webp
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LNReader/lnreader/9e81f805c0b5c4b1e2ec3b1c2357fa4320bfb8cb/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_monochrome.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LNReader/lnreader/9e81f805c0b5c4b1e2ec3b1c2357fa4320bfb8cb/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_monochrome.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LNReader/lnreader/9e81f805c0b5c4b1e2ec3b1c2357fa4320bfb8cb/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/android/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #FFFFFF
4 | #1F2024
5 | #023c69
6 | #202125
7 | #202125
8 | #00adb5
9 |
--------------------------------------------------------------------------------
/android/app/src/main/res/values/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #132C33
4 |
--------------------------------------------------------------------------------
/android/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | LNReader
4 |
5 |
--------------------------------------------------------------------------------
/android/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
16 |
17 |
24 |
33 |
34 |
--------------------------------------------------------------------------------
/android/build.gradle:
--------------------------------------------------------------------------------
1 | buildscript {
2 | ext {
3 | buildToolsVersion = "35.0.0"
4 | minSdkVersion = 24
5 | compileSdkVersion = 35
6 | targetSdkVersion = 35
7 | ndkVersion = "27.1.12297006"
8 | kotlinVersion = "2.0.21"
9 | }
10 | repositories {
11 | google()
12 | mavenCentral()
13 | }
14 | dependencies {
15 | classpath("com.android.tools.build:gradle")
16 | classpath("com.facebook.react:react-native-gradle-plugin")
17 | classpath("org.jetbrains.kotlin:kotlin-gradle-plugin")
18 | }
19 | }
20 |
21 | apply plugin: "com.facebook.react.rootproject"
--------------------------------------------------------------------------------
/android/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LNReader/lnreader/9e81f805c0b5c4b1e2ec3b1c2357fa4320bfb8cb/android/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/android/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionBase=GRADLE_USER_HOME
2 | distributionPath=wrapper/dists
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
4 | networkTimeout=10000
5 | validateDistributionUrl=true
6 | zipStoreBase=GRADLE_USER_HOME
7 | zipStorePath=wrapper/dists
8 |
--------------------------------------------------------------------------------
/android/settings.gradle:
--------------------------------------------------------------------------------
1 | pluginManagement { includeBuild("../node_modules/@react-native/gradle-plugin") }
2 | plugins { id("com.facebook.react.settings") }
3 |
4 | extensions.configure(com.facebook.react.ReactSettingsExtension){ ex -> ex.autolinkLibrariesFromCommand() }
5 | rootProject.name = 'LNReader'
6 |
7 | include ':app'
8 | includeBuild('../node_modules/@react-native/gradle-plugin')
9 |
10 | apply from: new File(["node", "--print", "require.resolve('expo/package.json')"].execute(null, rootDir).text.trim(), "../scripts/autolinking.gradle")
11 | useExpoModules()
12 |
--------------------------------------------------------------------------------
/app.json:
--------------------------------------------------------------------------------
1 | {
2 | "expo": {
3 | "name": "LNReader",
4 | "slug": "LNReader",
5 | "scheme": "lnreader",
6 | "plugins": [
7 | "expo-localization",
8 | "react-native-edge-to-edge",
9 | [
10 | "expo-sqlite",
11 | {
12 | "enableFTS": false,
13 | "useSQLCipher": false
14 | }
15 | ],
16 | "expo-web-browser"
17 | ]
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/assets/anilist.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LNReader/lnreader/9e81f805c0b5c4b1e2ec3b1c2357fa4320bfb8cb/assets/anilist.png
--------------------------------------------------------------------------------
/assets/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LNReader/lnreader/9e81f805c0b5c4b1e2ec3b1c2357fa4320bfb8cb/assets/logo.png
--------------------------------------------------------------------------------
/assets/mal.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LNReader/lnreader/9e81f805c0b5c4b1e2ec3b1c2357fa4320bfb8cb/assets/mal.png
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | const ReactCompilerConfig = {
2 | target: '19',
3 | };
4 |
5 | module.exports = function (api) {
6 | api.cache(true);
7 | return {
8 | presets: ['module:@react-native/babel-preset'],
9 | plugins: [
10 | 'module:@babel/plugin-transform-export-namespace-from',
11 | ['babel-plugin-react-compiler', ReactCompilerConfig],
12 | [
13 | 'module-resolver',
14 | {
15 | alias: {
16 | '@components': './src/components',
17 | '@database': './src/database',
18 | '@hooks': './src/hooks',
19 | '@screens': './src/screens',
20 | '@strings': './strings',
21 | '@services': './src/services',
22 | '@plugins': './src/plugins',
23 | '@utils': './src/utils',
24 | '@theme': './src/theme',
25 | '@navigators': './src/navigators',
26 | '@api': './src/api',
27 | '@type': './src/type',
28 | '@specs': './specs',
29 | 'react-native-vector-icons/MaterialCommunityIcons':
30 | '@react-native-vector-icons/material-design-icons',
31 | },
32 | },
33 | ],
34 | 'react-native-reanimated/plugin',
35 | [
36 | 'module:react-native-dotenv',
37 | {
38 | envName: 'APP_ENV',
39 | moduleName: '@env',
40 | path: '.env',
41 | },
42 | ],
43 | ],
44 | };
45 | };
46 |
--------------------------------------------------------------------------------
/crowdin.yml:
--------------------------------------------------------------------------------
1 | pull_request_title: 'chore: Update Translations'
2 | commit_message: '[ci skip]'
3 | files:
4 | - source: /strings/languages/en/strings.json
5 | translation: /strings/languages/%locale_with_underscore%/strings.json
6 |
--------------------------------------------------------------------------------
/env.d.ts:
--------------------------------------------------------------------------------
1 | declare module '@env' {
2 | export const MYANIMELIST_CLIENT_ID: string;
3 | export const ANILIST_CLIENT_ID: string;
4 | export const GIT_HASH: string;
5 | export const RELEASE_DATE: string;
6 | export const BUILD_TYPE: 'Debug' | 'Release' | 'Beta' | 'Github Action';
7 | }
8 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | import 'react-native-gesture-handler';
2 | import { registerRootComponent } from 'expo';
3 |
4 | import App from './App';
5 |
6 | // registerRootComponent calls AppRegistry.registerComponent('main', () => App);
7 | // It also ensures that whether you load the app in Expo Go or in a native build,
8 | // the environment is set up appropriately
9 | registerRootComponent(App);
10 |
--------------------------------------------------------------------------------
/ios/.xcode.env:
--------------------------------------------------------------------------------
1 | export NODE_BINARY=$(command -v node)
2 |
--------------------------------------------------------------------------------
/ios/Dynamic.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 | import Foundation
3 | import Lottie
4 |
5 | @objc class Dynamic: NSObject {
6 |
7 | @objc func createAnimationView(rootView: UIView, lottieName: String) -> AnimationView {
8 | let animationView = AnimationView(name: lottieName)
9 | animationView.frame = rootView.frame
10 | animationView.center = rootView.center
11 | animationView.backgroundColor = UIColor.white;
12 | return animationView;
13 | }
14 |
15 | @objc func play(animationView: AnimationView) {
16 | animationView.play(
17 | completion: { (success) in
18 | RNSplashScreen.setAnimationFinished(true)
19 | }
20 | );
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/ios/LNReader.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/ios/LNReader.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/ios/LNReader/Images.xcassets/AppIcon.appiconset/1024.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LNReader/lnreader/9e81f805c0b5c4b1e2ec3b1c2357fa4320bfb8cb/ios/LNReader/Images.xcassets/AppIcon.appiconset/1024.png
--------------------------------------------------------------------------------
/ios/LNReader/Images.xcassets/AppIcon.appiconset/114.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LNReader/lnreader/9e81f805c0b5c4b1e2ec3b1c2357fa4320bfb8cb/ios/LNReader/Images.xcassets/AppIcon.appiconset/114.png
--------------------------------------------------------------------------------
/ios/LNReader/Images.xcassets/AppIcon.appiconset/120.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LNReader/lnreader/9e81f805c0b5c4b1e2ec3b1c2357fa4320bfb8cb/ios/LNReader/Images.xcassets/AppIcon.appiconset/120.png
--------------------------------------------------------------------------------
/ios/LNReader/Images.xcassets/AppIcon.appiconset/180.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LNReader/lnreader/9e81f805c0b5c4b1e2ec3b1c2357fa4320bfb8cb/ios/LNReader/Images.xcassets/AppIcon.appiconset/180.png
--------------------------------------------------------------------------------
/ios/LNReader/Images.xcassets/AppIcon.appiconset/29.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LNReader/lnreader/9e81f805c0b5c4b1e2ec3b1c2357fa4320bfb8cb/ios/LNReader/Images.xcassets/AppIcon.appiconset/29.png
--------------------------------------------------------------------------------
/ios/LNReader/Images.xcassets/AppIcon.appiconset/40.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LNReader/lnreader/9e81f805c0b5c4b1e2ec3b1c2357fa4320bfb8cb/ios/LNReader/Images.xcassets/AppIcon.appiconset/40.png
--------------------------------------------------------------------------------
/ios/LNReader/Images.xcassets/AppIcon.appiconset/57.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LNReader/lnreader/9e81f805c0b5c4b1e2ec3b1c2357fa4320bfb8cb/ios/LNReader/Images.xcassets/AppIcon.appiconset/57.png
--------------------------------------------------------------------------------
/ios/LNReader/Images.xcassets/AppIcon.appiconset/58.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LNReader/lnreader/9e81f805c0b5c4b1e2ec3b1c2357fa4320bfb8cb/ios/LNReader/Images.xcassets/AppIcon.appiconset/58.png
--------------------------------------------------------------------------------
/ios/LNReader/Images.xcassets/AppIcon.appiconset/60.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LNReader/lnreader/9e81f805c0b5c4b1e2ec3b1c2357fa4320bfb8cb/ios/LNReader/Images.xcassets/AppIcon.appiconset/60.png
--------------------------------------------------------------------------------
/ios/LNReader/Images.xcassets/AppIcon.appiconset/80.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LNReader/lnreader/9e81f805c0b5c4b1e2ec3b1c2357fa4320bfb8cb/ios/LNReader/Images.xcassets/AppIcon.appiconset/80.png
--------------------------------------------------------------------------------
/ios/LNReader/Images.xcassets/AppIcon.appiconset/87.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LNReader/lnreader/9e81f805c0b5c4b1e2ec3b1c2357fa4320bfb8cb/ios/LNReader/Images.xcassets/AppIcon.appiconset/87.png
--------------------------------------------------------------------------------
/ios/LNReader/Images.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {"images":[{"size":"60x60","expected-size":"180","filename":"180.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"3x"},{"size":"40x40","expected-size":"80","filename":"80.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"40x40","expected-size":"120","filename":"120.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"3x"},{"size":"60x60","expected-size":"120","filename":"120.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"57x57","expected-size":"57","filename":"57.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"1x"},{"size":"29x29","expected-size":"58","filename":"58.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"29x29","expected-size":"29","filename":"29.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"1x"},{"size":"29x29","expected-size":"87","filename":"87.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"3x"},{"size":"57x57","expected-size":"114","filename":"114.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"20x20","expected-size":"40","filename":"40.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"20x20","expected-size":"60","filename":"60.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"3x"},{"size":"1024x1024","filename":"1024.png","expected-size":"1024","idiom":"ios-marketing","folder":"Assets.xcassets/AppIcon.appiconset/","scale":"1x"}]}
--------------------------------------------------------------------------------
/ios/LNReader/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | en
7 | CFBundleDisplayName
8 | LNReader
9 | CFBundleExecutable
10 | $(EXECUTABLE_NAME)
11 | CFBundleIdentifier
12 | $(PRODUCT_BUNDLE_IDENTIFIER)
13 | CFBundleInfoDictionaryVersion
14 | 6.0
15 | CFBundleName
16 | $(PRODUCT_NAME)
17 | CFBundlePackageType
18 | APPL
19 | CFBundleShortVersionString
20 | $(MARKETING_VERSION)
21 | CFBundleSignature
22 | ????
23 | CFBundleURLTypes
24 |
25 |
26 | CFBundleTypeRole
27 | Editor
28 | CFBundleURLIconFile
29 |
30 | CFBundleURLName
31 | LNReader
32 | CFBundleURLSchemes
33 |
34 | lnreader
35 |
36 |
37 |
38 | CFBundleVersion
39 | $(CURRENT_PROJECT_VERSION)
40 | LSRequiresIPhoneOS
41 |
42 | NSAppTransportSecurity
43 |
44 | NSAllowsArbitraryLoads
45 |
46 | NSAllowsLocalNetworking
47 |
48 |
49 | NSLocationWhenInUseUsageDescription
50 |
51 | UILaunchStoryboardName
52 | LaunchScreen
53 | UIRequiredDeviceCapabilities
54 |
55 | arm64
56 |
57 | UISupportedInterfaceOrientations
58 |
59 | UIInterfaceOrientationPortrait
60 | UIInterfaceOrientationLandscapeLeft
61 | UIInterfaceOrientationLandscapeRight
62 |
63 | UIViewControllerBasedStatusBarAppearance
64 |
65 |
66 |
67 |
--------------------------------------------------------------------------------
/ios/LNReader/LNReader-Bridging-Header.h:
--------------------------------------------------------------------------------
1 | //
2 | // LNReader-Bridging-Header.h
3 | // LNReader
4 | //
5 | // Created by QUAN on 17/5/25.
6 | //
7 |
8 | #ifndef LNReader_Bridging_Header_h
9 | #define LNReader_Bridging_Header_h
10 |
11 | #import "RNSplashScreen.h"
12 | #import
13 |
14 | #endif /* LNReader_Bridging_Header_h */
15 |
--------------------------------------------------------------------------------
/ios/LNReader/LaunchScreen.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/ios/LNReader/PrivacyInfo.xcprivacy:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | NSPrivacyAccessedAPITypes
6 |
7 |
8 | NSPrivacyAccessedAPIType
9 | NSPrivacyAccessedAPICategoryFileTimestamp
10 | NSPrivacyAccessedAPITypeReasons
11 |
12 | C617.1
13 | 0A2A.1
14 | 3B52.1
15 |
16 |
17 |
18 | NSPrivacyAccessedAPIType
19 | NSPrivacyAccessedAPICategoryUserDefaults
20 | NSPrivacyAccessedAPITypeReasons
21 |
22 | CA92.1
23 |
24 |
25 |
26 | NSPrivacyAccessedAPIType
27 | NSPrivacyAccessedAPICategoryDiskSpace
28 | NSPrivacyAccessedAPITypeReasons
29 |
30 | E174.1
31 | 85F4.1
32 |
33 |
34 |
35 | NSPrivacyAccessedAPIType
36 | NSPrivacyAccessedAPICategorySystemBootTime
37 | NSPrivacyAccessedAPITypeReasons
38 |
39 | 35F9.1
40 |
41 |
42 |
43 | NSPrivacyCollectedDataTypes
44 |
45 | NSPrivacyTracking
46 |
47 |
48 |
49 |
--------------------------------------------------------------------------------
/ios/NativeEpubUtil/RCTNativeEpubUtil.h:
--------------------------------------------------------------------------------
1 | #import
2 | #import
3 |
4 | NS_ASSUME_NONNULL_BEGIN
5 |
6 | @interface RCTNativeEpubUtil : NSObject
7 |
8 | @end
9 |
10 | NS_ASSUME_NONNULL_END
11 |
--------------------------------------------------------------------------------
/ios/NativeEpubUtil/RCTNativeEpubUtil.mm:
--------------------------------------------------------------------------------
1 | //
2 |
3 | #import "RCTNativeEpubUtil.h"
4 |
5 | @implementation RCTNativeEpubUtil
6 |
7 |
8 | + (NSString *)moduleName {
9 | return @"NativeEpubUtil";
10 | }
11 |
12 | - (std::shared_ptr)getTurboModule:(const facebook::react::ObjCTurboModule::InitParams &)params {
13 | return std::make_shared(params);
14 | }
15 |
16 | - (NSDictionary * _Nullable)parseNovelAndChapters:(nonnull NSString *)epubDirPath {
17 | NSMutableDictionary * res = [NSMutableDictionary dictionary];
18 | // TODO: implement parse epub
19 | return res;
20 | }
21 |
22 | @end
23 |
--------------------------------------------------------------------------------
/ios/NativeFile/RCTNativeFile.h:
--------------------------------------------------------------------------------
1 | #import
2 | #import
3 |
4 | NS_ASSUME_NONNULL_BEGIN
5 |
6 | @interface RCTNativeFile : NSObject
7 |
8 | @end
9 |
10 | NS_ASSUME_NONNULL_END
11 |
--------------------------------------------------------------------------------
/ios/NativeVolumeButtonListener/RCTNativeVolumeButtonListener.h:
--------------------------------------------------------------------------------
1 | #import
2 | #import
3 |
4 | NS_ASSUME_NONNULL_BEGIN
5 |
6 | @interface RCTNativeVolumeButtonListener : NSObject
7 |
8 | @end
9 |
10 | NS_ASSUME_NONNULL_END
11 |
--------------------------------------------------------------------------------
/ios/NativeVolumeButtonListener/RCTNativeVolumeButtonListener.mm:
--------------------------------------------------------------------------------
1 | #import "RCTNativeVolumeButtonListener.h"
2 |
3 | @implementation RCTNativeVolumeButtonListener
4 |
5 | + (NSString *)moduleName {
6 | return @"NativeVolumeButtonListener";
7 | }
8 |
9 | - (std::shared_ptr)getTurboModule:(const facebook::react::ObjCTurboModule::InitParams &)params {
10 | return std::make_shared(params);
11 | }
12 |
13 | - (void)addListener:(nonnull NSString *)eventName {
14 | // TODO: implement addlistener
15 | }
16 |
17 | - (void)removeListeners:(double)count {
18 | // TODO: implement count listeners
19 | }
20 |
21 | @end
22 |
--------------------------------------------------------------------------------
/ios/NativeZipArchive/RCTNativeZipArchive.h:
--------------------------------------------------------------------------------
1 | //
2 | // RCTNativeZipArchive.h
3 | // LNReader
4 | //
5 | // Created by QUAN on 18/5/25.
6 | //
7 |
8 | #import
9 | #import
10 |
11 | NS_ASSUME_NONNULL_BEGIN
12 |
13 | @interface RCTNativeZipArchive : NSObject
14 |
15 | @end
16 |
17 | NS_ASSUME_NONNULL_END
18 |
--------------------------------------------------------------------------------
/ios/NativeZipArchive/RCTNativeZipArchive.mm:
--------------------------------------------------------------------------------
1 | //
2 | // RCTNativeZipArchive.m
3 | // LNReader
4 | //
5 | // Created by QUAN on 18/5/25.
6 | //
7 |
8 | #import "RCTNativeZipArchive.h"
9 |
10 | @implementation RCTNativeZipArchive
11 |
12 | + (NSString *)moduleName {
13 | return @"NativeZipArchive";
14 | }
15 |
16 | - (std::shared_ptr)getTurboModule:(const facebook::react::ObjCTurboModule::InitParams &)params {
17 | return std::make_shared(params);
18 | }
19 |
20 | - (void)remoteUnzip:(nonnull NSString *)distDirPath url:(nonnull NSString *)url headers:(nonnull NSDictionary *)headers resolve:(nonnull RCTPromiseResolveBlock)resolve reject:(nonnull RCTPromiseRejectBlock)reject {
21 | // TODO: implement remoteUnzip
22 | }
23 |
24 | - (void)remoteZip:(nonnull NSString *)sourceDirPath url:(nonnull NSString *)url headers:(nonnull NSDictionary *)headers resolve:(nonnull RCTPromiseResolveBlock)resolve reject:(nonnull RCTPromiseRejectBlock)reject {
25 | // TODO: implement remoteZip
26 | }
27 |
28 | - (void)unzip:(nonnull NSString *)sourceFilePath distDirPath:(nonnull NSString *)distDirPath resolve:(nonnull RCTPromiseResolveBlock)resolve reject:(nonnull RCTPromiseRejectBlock)reject {
29 | // TODO: implement unzip
30 | }
31 |
32 | @end
33 |
--------------------------------------------------------------------------------
/ios/Podfile:
--------------------------------------------------------------------------------
1 | require File.join(File.dirname(`node --print "require.resolve('expo/package.json')"`), "scripts/autolinking")
2 | # Resolve react_native_pods.rb with node to allow for hoisting
3 | require Pod::Executable.execute_command('node', ['-p',
4 | 'require.resolve(
5 | "react-native/scripts/react_native_pods.rb",
6 | {paths: [process.argv[1]]},
7 | )', __dir__]).strip
8 |
9 | platform :ios, 15.5
10 | prepare_react_native_project!
11 |
12 | linkage = ENV['USE_FRAMEWORKS']
13 | if linkage != nil
14 | Pod::UI.puts "Configuring Pod with #{linkage}ally linked Frameworks".green
15 | use_frameworks! :linkage => linkage.to_sym
16 | end
17 |
18 | target 'LNReader' do
19 | use_expo_modules!
20 |
21 | if ENV['EXPO_USE_COMMUNITY_AUTOLINKING'] == '1'
22 | config_command = ['node', '-e', "process.argv=['', '', 'config'];require('@react-native-community/cli').run()"];
23 | else
24 | config_command = [
25 | 'node',
26 | '--no-warnings',
27 | '--eval',
28 | 'require(require.resolve(\'expo-modules-autolinking\', { paths: [require.resolve(\'expo/package.json\')] }))(process.argv.slice(1))',
29 | 'react-native-config',
30 | '--json',
31 | '--platform',
32 | 'ios'
33 | ]
34 | end
35 |
36 | config = use_native_modules!(config_command)
37 |
38 | use_react_native!(
39 | :path => config[:reactNativePath],
40 | # An absolute path to your application root.
41 | :app_path => "#{Pod::Config.instance.installation_root}/.."
42 | )
43 |
44 | post_install do |installer|
45 | # https://github.com/facebook/react-native/blob/main/packages/react-native/scripts/react_native_pods.rb#L197-L202
46 | react_native_post_install(
47 | installer,
48 | config[:reactNativePath],
49 | :mac_catalyst_enabled => false,
50 | # :ccache_enabled => true
51 | )
52 | end
53 | end
54 |
--------------------------------------------------------------------------------
/metro.config.js:
--------------------------------------------------------------------------------
1 | // Learn more https://docs.expo.io/guides/customizing-metro
2 |
3 | /**
4 | * Metro configuration
5 | * https://reactnative.dev/docs/metro
6 | *
7 | * @type {import('@react-native/metro-config').MetroConfig}
8 | */
9 |
10 | const path = require('path');
11 | const fs = require('fs');
12 | const { mergeConfig } = require('metro-config');
13 | const { getDefaultConfig } = require('@react-native/metro-config');
14 | const defaultConfig = getDefaultConfig(__dirname);
15 |
16 | const map = {
17 | '.ico': 'image/x-icon',
18 | '.html': 'text/html',
19 | '.js': 'text/javascript',
20 | '.json': 'application/json',
21 | '.css': 'text/css',
22 | '.png': 'image/png',
23 | '.jpg': 'image/jpeg',
24 | };
25 | const customConfig = {
26 | server: {
27 | port: 8081,
28 | enhanceMiddleware: (metroMiddleware, metroServer) => {
29 | return (request, res, next) => {
30 | const filePath = path.join(
31 | __dirname,
32 | 'android/app/src/main',
33 | request._parsedUrl.path || '',
34 | );
35 | const ext = path.parse(filePath).ext;
36 | if (fs.existsSync(filePath)) {
37 | try {
38 | const data = fs.readFileSync(filePath);
39 | res.setHeader('Content-type', map[ext] || 'text/plain');
40 | res.end(data);
41 | } catch (err) {
42 | res.statusCode = 500;
43 | res.end(`Error getting the file: ${err}.`);
44 | }
45 | } else {
46 | return metroMiddleware(request, res, next);
47 | }
48 | };
49 | },
50 | },
51 | };
52 | module.exports = mergeConfig(defaultConfig, customConfig);
53 |
--------------------------------------------------------------------------------
/scripts/setEnvFile.cjs:
--------------------------------------------------------------------------------
1 | const fs = require('fs');
2 | const os = require('os');
3 | const path = require('path');
4 | const { execSync } = require('child_process');
5 |
6 | const formattedDate = new Date().getTime();
7 |
8 | const commitHash = execSync('git rev-parse --short HEAD').toString().trim();
9 |
10 | const buildType = process.argv[2] || 'Beta';
11 |
12 | let data =
13 | `BUILD_TYPE=${buildType}` +
14 | os.EOL +
15 | `GIT_HASH=${commitHash}` +
16 | os.EOL +
17 | `RELEASE_DATE=${formattedDate}` +
18 | os.EOL +
19 | `NODE_ENV=${buildType === 'Release' ? 'production' : 'development'}`;
20 | let existingEnvData = '';
21 |
22 | fs.readFile(path.join(__dirname, '..', '.env'), 'utf8', (err, existingData) => {
23 | if (err) return;
24 |
25 | existingEnvData = existingData
26 | .split(os.EOL)
27 | .filter(line => {
28 | return (
29 | !line.startsWith('BUILD_TYPE=') &&
30 | !line.startsWith('GIT_HASH=') &&
31 | !line.startsWith('RELEASE_DATE=')
32 | );
33 | })
34 | .join(os.EOL);
35 | });
36 |
37 | if (existingEnvData) {
38 | data += os.EOL + existingEnvData;
39 | }
40 |
41 | fs.writeFile(path.join(__dirname, '..', '.env'), data, 'utf8', err => {
42 | if (err) {
43 | console.log(err);
44 | }
45 | });
46 |
--------------------------------------------------------------------------------
/scripts/stringTypes.cjs:
--------------------------------------------------------------------------------
1 | const fs = require('fs');
2 | const path = require('path');
3 | const prettier = require('prettier');
4 | const prettierConfig = require('../.prettierrc');
5 |
6 | const flatten = (obj, target, prefix) => {
7 | (target = target || {}), (prefix = prefix || '');
8 |
9 | Object.keys(obj).forEach(function (key) {
10 | if (typeof obj[key] === 'object' && obj[key] !== null) {
11 | flatten(obj[key], target, prefix + key + '.');
12 | } else {
13 | return (target[prefix + key] = 'string');
14 | }
15 | });
16 |
17 | return target;
18 | };
19 |
20 | const strings = fs.readFileSync(
21 | path.resolve(process.cwd(), 'strings/languages/en/strings.json'),
22 | 'utf8',
23 | );
24 |
25 | const stringTypes = `/**\n * This file is auto-generated\n */\n\n
26 |
27 | export interface StringMap ${JSON.stringify(flatten(JSON.parse(strings)))}
28 |
29 | `;
30 |
31 | const formatContent = prettier.format(stringTypes, {
32 | parser: 'typescript',
33 | ...prettierConfig,
34 | });
35 |
36 | fs.writeFile(
37 | path.resolve(process.cwd(), 'strings/types/index.ts'),
38 | formatContent,
39 | err => {
40 | if (err) {
41 | console.log(err);
42 | }
43 | },
44 | );
45 |
--------------------------------------------------------------------------------
/shared/Epub.hpp:
--------------------------------------------------------------------------------
1 | #include
2 | #include
3 |
4 | #ifndef Epub_H
5 | #define Epub_H
6 |
7 | struct Chapter
8 | {
9 | std::string name;
10 | std::string path;
11 | };
12 |
13 | struct EpubMetadata
14 | {
15 | std::string name;
16 | std::string path;
17 | std::string cover;
18 | std::string summary;
19 | std::string author;
20 | std::string artist;
21 | std::vector chapters;
22 | std::vector cssPaths;
23 | std::vector imagePaths;
24 | };
25 |
26 | EpubMetadata parseEpub(const std::string epub_path);
27 |
28 | #endif
29 |
--------------------------------------------------------------------------------
/shared/NativeEpub.cpp:
--------------------------------------------------------------------------------
1 | #include
2 |
3 | namespace facebook::react
4 | {
5 |
6 | NativeEpub::NativeEpub(std::shared_ptr jsInvoker)
7 | : NativeEpubCxxSpec(std::move(jsInvoker)) {}
8 |
9 | jsi::Object NativeEpub::parseNovelAndChapters(jsi::Runtime &rt, jsi::String epubDirPath)
10 | {
11 | jsi::Object novel(rt);
12 | EpubMetadata metadata = parseEpub(epubDirPath.utf8(rt).c_str());
13 | novel.setProperty(rt, "name", metadata.name);
14 | novel.setProperty(rt, "author", metadata.author);
15 | novel.setProperty(rt, "artist", metadata.artist);
16 | novel.setProperty(rt, "summary", metadata.summary);
17 | novel.setProperty(rt, "cover", metadata.cover);
18 |
19 | jsi::Array chapters(rt, metadata.chapters.size());
20 | for (int i = 0; i < metadata.chapters.size(); i++)
21 | {
22 | jsi::Object chapter(rt);
23 | chapter.setProperty(rt, "name", metadata.chapters[i].name);
24 | chapter.setProperty(rt, "path", metadata.chapters[i].path);
25 | chapters.setValueAtIndex(rt, i, chapter);
26 | }
27 | novel.setProperty(rt, "chapters", chapters);
28 | jsi::Array cssPaths(rt, metadata.cssPaths.size());
29 | for (int i = 0; i < metadata.cssPaths.size(); i++)
30 | {
31 | cssPaths.setValueAtIndex(rt, i, metadata.cssPaths[i]);
32 | }
33 | novel.setProperty(rt, "cssPaths", cssPaths);
34 |
35 | jsi::Array imagePaths(rt, metadata.imagePaths.size());
36 | for (int i = 0; i < metadata.imagePaths.size(); i++)
37 | {
38 | imagePaths.setValueAtIndex(rt, i, metadata.imagePaths[i]);
39 | }
40 | novel.setProperty(rt, "imagePaths", imagePaths);
41 |
42 | return novel;
43 | }
44 |
45 | } // namespace facebook::react
--------------------------------------------------------------------------------
/shared/NativeEpub.hpp:
--------------------------------------------------------------------------------
1 | #pragma once
2 |
3 | #include
4 | #include
5 | #include
6 |
7 | namespace facebook::react
8 | {
9 |
10 | class NativeEpub : public NativeEpubCxxSpec
11 | {
12 | public:
13 | NativeEpub(std::shared_ptr jsInvoker);
14 |
15 | jsi::Object parseNovelAndChapters(jsi::Runtime &rt, jsi::String epubDirPath);
16 | };
17 |
18 | } // namespace facebook::react
19 |
--------------------------------------------------------------------------------
/specs/NativeEpub.ts:
--------------------------------------------------------------------------------
1 | import { TurboModule, TurboModuleRegistry } from 'react-native';
2 |
3 | interface EpubChapter {
4 | name: string;
5 | path: string;
6 | }
7 | interface EpubNovel {
8 | name: string;
9 | cover: string | null;
10 | summary: string | null;
11 | author: string | null;
12 | artist: string | null;
13 | chapters: EpubChapter[];
14 | cssPaths: string[];
15 | imagePaths: string[];
16 | }
17 |
18 | export interface Spec extends TurboModule {
19 | parseNovelAndChapters: (epubDirPath: string) => EpubNovel;
20 | }
21 |
22 | export default TurboModuleRegistry.getEnforcing('NativeEpub');
23 |
--------------------------------------------------------------------------------
/specs/NativeFile.ts:
--------------------------------------------------------------------------------
1 | import { TurboModule, TurboModuleRegistry } from 'react-native';
2 |
3 | interface ReadDirResult {
4 | name: string;
5 | path: string;
6 | isDirectory: boolean; // int
7 | }
8 |
9 | export interface Spec extends TurboModule {
10 | writeFile: (path: string, content: string) => void;
11 | readFile: (path: string) => string;
12 | copyFile: (sourcePath: string, destPath: string) => void;
13 | moveFile: (sourcePath: string, destPath: string) => void;
14 | exists: (filePath: string) => boolean;
15 | /**
16 | * @description create parents, and do nothing if exists;
17 | */
18 | mkdir: (filePath: string) => void;
19 | /**
20 | * @description remove recursively
21 | */
22 | unlink: (filePath: string) => void;
23 | readDir: (dirPath: string) => ReadDirResult[];
24 | downloadFile: (
25 | url: string,
26 | destPath: string,
27 | method: string,
28 | headers: { [key: string]: string } | Headers,
29 | body?: string,
30 | ) => Promise;
31 | getConstants: () => {
32 | ExternalDirectoryPath: string;
33 | ExternalCachesDirectoryPath: string;
34 | };
35 | }
36 |
37 | export default TurboModuleRegistry.getEnforcing('NativeFile');
38 |
--------------------------------------------------------------------------------
/specs/NativeVolumeButtonListener.ts:
--------------------------------------------------------------------------------
1 | import { TurboModule, TurboModuleRegistry } from 'react-native';
2 |
3 | export interface Spec extends TurboModule {
4 | addListener: (eventName: string) => void;
5 | removeListeners: (count: number) => void;
6 | }
7 |
8 | export default TurboModuleRegistry.getEnforcing(
9 | 'NativeVolumeButtonListener',
10 | );
11 |
--------------------------------------------------------------------------------
/specs/NativeZipArchive.ts:
--------------------------------------------------------------------------------
1 | import { TurboModule, TurboModuleRegistry } from 'react-native';
2 |
3 | export interface Spec extends TurboModule {
4 | unzip: (sourceFilePath: string, distDirPath: string) => Promise;
5 | remoteUnzip: (
6 | distDirPath: string,
7 | url: string,
8 | headers: { [key: string]: string },
9 | ) => Promise;
10 | remoteZip: (
11 | sourceDirPath: string,
12 | url: string,
13 | headers: { [key: string]: string },
14 | ) => Promise; // return response as text
15 | }
16 |
17 | export default TurboModuleRegistry.getEnforcing('NativeZipArchive');
18 |
--------------------------------------------------------------------------------
/src/api/constants.ts:
--------------------------------------------------------------------------------
1 | export const PATH_SEPARATOR = '&&';
2 |
--------------------------------------------------------------------------------
/src/api/drive/types.ts:
--------------------------------------------------------------------------------
1 | export interface DriveFile {
2 | kind: string;
3 | mimeType?: string;
4 | parents: string[];
5 | id: string;
6 | name: string;
7 | description?: string;
8 | createdTime?: string;
9 | }
10 |
11 | export interface DriveReponse {
12 | nextPageToken?: string;
13 | kind: string;
14 | incompleteSearch: boolean;
15 | files: DriveFile[];
16 | }
17 |
18 | export interface DriveRequestParams {
19 | q?: string;
20 | orderBy?: string;
21 | pageSize?: number;
22 | fields?: string;
23 | pageToken?: string;
24 | uploadType?: string; // only for upload
25 | addParents?: string;
26 | removeParents?: string;
27 | }
28 |
29 | export interface DriveCreateRequestData {
30 | metadata: {
31 | name: string;
32 | mimeType: string;
33 | description?: string;
34 | parents?: string[];
35 | };
36 | content?: string; // uri if upload file
37 | }
38 |
--------------------------------------------------------------------------------
/src/api/remote/index.ts:
--------------------------------------------------------------------------------
1 | import { PATH_SEPARATOR } from '@api/constants';
2 | import NativeZipArchive from '@specs/NativeZipArchive';
3 | import { fetchTimeout } from '@utils/fetch/fetch';
4 |
5 | const commonHeaders = {
6 | 'Connection': 'keep-alive',
7 | 'Accept':
8 | 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7',
9 | 'Accept-Encoding': 'gzip, deflate, br',
10 | };
11 |
12 | export const upload = (
13 | host: string,
14 | backupFolder: string,
15 | filename: string,
16 | sourceDirPath: string,
17 | ) => {
18 | const url = `${host}/upload/${backupFolder}${PATH_SEPARATOR}${filename}`;
19 | return NativeZipArchive.remoteZip(sourceDirPath, url, {});
20 | };
21 |
22 | export const list = (host: string): Promise => {
23 | const url = host + '/list';
24 | return fetchTimeout(url, { headers: commonHeaders }).then(res => res.json());
25 | };
26 |
27 | export const download = async (
28 | host: string,
29 | backupFolder: string,
30 | filename: string,
31 | distDirPath: string,
32 | ) => {
33 | const url = `${host}/download/${backupFolder}${PATH_SEPARATOR}${filename}`;
34 | return NativeZipArchive.remoteUnzip(distDirPath, url, {});
35 | };
36 |
--------------------------------------------------------------------------------
/src/components/Appbar/Appbar.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { StatusBar } from 'react-native';
3 |
4 | import { Appbar as PaperAppbar } from 'react-native-paper';
5 | import { ThemeColors } from '../../theme/types';
6 |
7 | interface AppbarProps {
8 | title: string;
9 | handleGoBack?: () => void;
10 | theme: ThemeColors;
11 | mode?: 'small' | 'medium' | 'large' | 'center-aligned';
12 | children?: React.ReactNode;
13 | }
14 |
15 | const Appbar: React.FC = ({
16 | title,
17 | handleGoBack,
18 | theme,
19 | mode = 'large',
20 | children,
21 | }) => (
22 |
27 | {handleGoBack && (
28 |
32 | )}
33 |
37 | {children}
38 |
39 | );
40 |
41 | export default Appbar;
42 |
--------------------------------------------------------------------------------
/src/components/BottomSheet/BottomSheetBackdrop.tsx:
--------------------------------------------------------------------------------
1 | import React, { memo } from 'react';
2 | import { Pressable, StyleSheet } from 'react-native';
3 | import Animated, { FadeIn, FadeOut } from 'react-native-reanimated';
4 | import { useBottomSheet } from '@gorhom/bottom-sheet';
5 | import { BottomSheetBackdropProps } from '@gorhom/bottom-sheet/lib/typescript/components/bottomSheetBackdrop/types';
6 |
7 | const styles = StyleSheet.create({
8 | container: {
9 | backgroundColor: 'rgba(0, 0, 0, 0.4)',
10 | },
11 | });
12 |
13 | const AnimatedPressable = Animated.createAnimatedComponent(Pressable);
14 |
15 | const CustomBackdrop = ({ style }: BottomSheetBackdropProps) => {
16 | const { close } = useBottomSheet();
17 | return (
18 | close()}
20 | entering={FadeIn.duration(100)}
21 | exiting={FadeOut.duration(100)}
22 | style={[styles.container, style]}
23 | />
24 | );
25 | };
26 | export default memo(CustomBackdrop);
27 |
--------------------------------------------------------------------------------
/src/components/Button/Button.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {
3 | Button as PaperButton,
4 | ButtonProps as PaperButtonProps,
5 | } from 'react-native-paper';
6 |
7 | import { useTheme } from '@hooks/persisted';
8 | import { ThemeProp } from 'react-native-paper/lib/typescript/types';
9 |
10 | interface ButtonProps extends Partial {
11 | title?: string;
12 | }
13 |
14 | const Button: React.FC = props => {
15 | const t = useTheme();
16 | const theme: ThemeProp = React.useMemo(() => ({ colors: t }), [t]);
17 | const Children = React.useMemo(
18 | () => props.title || props.children,
19 | [props.children, props.title],
20 | );
21 |
22 | return ;
23 | };
24 |
25 | export default React.memo(Button);
26 |
--------------------------------------------------------------------------------
/src/components/Chip/Chip.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Pressable, StyleSheet, Text, View } from 'react-native';
3 |
4 | import { ThemeColors } from '../../theme/types';
5 | import { overlay } from 'react-native-paper';
6 |
7 | interface ChipProps {
8 | label: string;
9 | theme: ThemeColors;
10 | }
11 |
12 | const Chip: React.FC = ({ label, theme }) => (
13 |
23 |
27 |
35 | {label}
36 |
37 |
38 |
39 | );
40 |
41 | export default Chip;
42 |
43 | const styles = StyleSheet.create({
44 | chipContainer: {
45 | borderRadius: 8,
46 | height: 32,
47 | marginRight: 8,
48 | overflow: 'hidden',
49 | },
50 | label: {
51 | fontSize: 12,
52 | },
53 | pressable: {
54 | alignItems: 'center',
55 | flex: 1,
56 | justifyContent: 'center',
57 | paddingHorizontal: 16,
58 | },
59 | });
60 |
--------------------------------------------------------------------------------
/src/components/ColorPreferenceItem/ColorPreferenceItem.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Pressable, StyleSheet, Text, View } from 'react-native';
3 |
4 | import { ThemeColors } from '../../theme/types';
5 |
6 | interface ColorPreferenceItemProps {
7 | label: string;
8 | description?: string;
9 | onPress: () => void;
10 | theme: ThemeColors;
11 | }
12 |
13 | const ColorPreferenceItem: React.FC = ({
14 | label,
15 | description,
16 | theme,
17 | onPress,
18 | }) => (
19 |
24 |
25 | {label}
26 |
27 | {description?.toUpperCase?.()}
28 |
29 |
30 |
31 |
32 | );
33 |
34 | export default ColorPreferenceItem;
35 |
36 | const styles = StyleSheet.create({
37 | colorPreview: {
38 | borderRadius: 50,
39 | height: 24,
40 | marginRight: 16,
41 | width: 24,
42 | },
43 | container: {
44 | alignItems: 'center',
45 | flexDirection: 'row',
46 | justifyContent: 'space-between',
47 | padding: 16,
48 | },
49 | label: {
50 | fontSize: 16,
51 | },
52 | });
53 |
--------------------------------------------------------------------------------
/src/components/Common.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { View, StyleSheet } from 'react-native';
3 |
4 | const Row = ({
5 | children,
6 | style = {},
7 | }: {
8 | children?: React.ReactNode;
9 | style?: any;
10 | }) => {children};
11 |
12 | export { Row };
13 |
14 | const styles = StyleSheet.create({
15 | row: {
16 | alignItems: 'center',
17 | flexDirection: 'row',
18 | },
19 | });
20 |
--------------------------------------------------------------------------------
/src/components/ConfirmationDialog/ConfirmationDialog.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { StyleSheet, Text, View } from 'react-native';
3 |
4 | import { getString } from '@strings/translations';
5 |
6 | import { Dialog, Portal } from 'react-native-paper';
7 | import { ThemeColors } from '../../theme/types';
8 | import Button from '../Button/Button';
9 |
10 | interface ConfirmationDialogProps {
11 | title?: string;
12 | message?: string;
13 | visible: boolean;
14 | theme: ThemeColors;
15 | onSubmit: () => void;
16 | onDismiss: () => void;
17 | }
18 |
19 | const ConfirmationDialog: React.FC = ({
20 | title = getString('common.warning'),
21 | message,
22 | visible,
23 | onDismiss,
24 | theme,
25 | onSubmit,
26 | }) => {
27 | const handleOnSubmit = () => {
28 | onSubmit();
29 | onDismiss();
30 | };
31 |
32 | return (
33 |
34 |
50 |
51 | );
52 | };
53 |
54 | export default ConfirmationDialog;
55 |
56 | const styles = StyleSheet.create({
57 | buttonCtn: {
58 | flexDirection: 'row-reverse',
59 | padding: 16,
60 | },
61 | container: {
62 | borderRadius: 28,
63 | shadowColor: 'transparent',
64 | },
65 | content: {
66 | fontSize: 16,
67 | letterSpacing: 0,
68 | },
69 | });
70 |
--------------------------------------------------------------------------------
/src/components/Context/LibraryContext.tsx:
--------------------------------------------------------------------------------
1 | import React, { createContext, useContext } from 'react';
2 | import {
3 | useLibrary,
4 | UseLibraryReturnType,
5 | } from '@screens/library/hooks/useLibrary';
6 | import { useLibrarySettings } from '@hooks/persisted';
7 | import { LibrarySettings } from '@hooks/persisted/useSettings';
8 |
9 | // type Library = Category & { novels: LibraryNovelInfo[] };
10 |
11 | type LibraryContextType = UseLibraryReturnType & {
12 | settings: LibrarySettings;
13 | };
14 |
15 | const defaultValue = {} as LibraryContextType;
16 | const LibraryContext = createContext(defaultValue);
17 |
18 | export function LibraryContextProvider({
19 | children,
20 | }: {
21 | children: React.ReactNode;
22 | }) {
23 | const useLibraryParams = useLibrary();
24 | const settings = useLibrarySettings();
25 |
26 | return (
27 |
28 | {children}
29 |
30 | );
31 | }
32 |
33 | export const useLibraryContext = (): LibraryContextType => {
34 | return useContext(LibraryContext);
35 | };
36 |
--------------------------------------------------------------------------------
/src/components/Context/UpdateContext.tsx:
--------------------------------------------------------------------------------
1 | import { useUpdates } from '@hooks/persisted';
2 | import React, { createContext, useContext, useMemo } from 'react';
3 |
4 | type UpdateContextType = ReturnType;
5 |
6 | const defaultValue = {} as UpdateContextType;
7 | const UpdateContext = createContext(defaultValue);
8 |
9 | export function UpdateContextProvider({
10 | children,
11 | }: {
12 | children: React.ReactNode;
13 | }) {
14 | const useUpdateParams = useUpdates();
15 | const contextValue = useMemo(
16 | () => ({
17 | ...useUpdateParams,
18 | }),
19 | [useUpdateParams],
20 | );
21 |
22 | return (
23 |
24 | {children}
25 |
26 | );
27 | }
28 |
29 | export const useUpdateContext = (): UpdateContextType => {
30 | return useContext(UpdateContext);
31 | };
32 |
--------------------------------------------------------------------------------
/src/components/EmptyView.tsx:
--------------------------------------------------------------------------------
1 | import { useTheme } from '@hooks/persisted';
2 | import React from 'react';
3 | import { StyleSheet, Text, View } from 'react-native';
4 |
5 | interface EmptyViewProps {
6 | icon: string;
7 | description: string;
8 | style?: any;
9 | children?: React.ReactNode;
10 | iconStyle?: any;
11 | }
12 |
13 | const EmptyView = ({
14 | icon,
15 | description,
16 | style,
17 | children,
18 | iconStyle,
19 | }: EmptyViewProps) => {
20 | const theme = useTheme();
21 |
22 | return (
23 |
24 |
32 | {icon}
33 |
34 |
35 | {description}
36 |
37 | {children}
38 |
39 | );
40 | };
41 |
42 | export default EmptyView;
43 |
44 | const styles = StyleSheet.create({
45 | emptyViewContainer: {
46 | alignItems: 'center',
47 | flex: 1,
48 | justifyContent: 'center',
49 | },
50 | emptyViewIcon: {
51 | fontSize: 45,
52 | },
53 | emptyViewText: {
54 | fontWeight: 'bold',
55 | marginTop: 10,
56 | paddingHorizontal: 30,
57 | textAlign: 'center',
58 | },
59 | });
60 |
--------------------------------------------------------------------------------
/src/components/EmptyView/EmptyView.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { StyleSheet, View, Text } from 'react-native';
3 |
4 | import { ThemeColors } from '../../theme/types';
5 | import { Button } from 'react-native-paper';
6 |
7 | interface EmptyViewProps {
8 | icon?: string;
9 | description: string;
10 | theme: ThemeColors;
11 | actions?: Array<{
12 | iconName: string;
13 | title: string;
14 | onPress: () => void;
15 | }>;
16 | }
17 |
18 | const EmptyView: React.FC = ({
19 | icon,
20 | description,
21 | theme,
22 | actions,
23 | }) => (
24 |
25 | {icon ? (
26 | {icon}
27 | ) : null}
28 | {description}
29 | {actions?.length ? (
30 |
31 | {actions.map(action => (
32 |
33 |
42 |
43 | ))}
44 |
45 | ) : null}
46 |
47 | );
48 |
49 | export default EmptyView;
50 |
51 | const styles = StyleSheet.create({
52 | actionsCtn: {
53 | flexDirection: 'row',
54 | marginTop: 20,
55 | },
56 | buttonWrapper: {
57 | flexDirection: 'row',
58 | marginHorizontal: 4,
59 | },
60 | container: {
61 | alignItems: 'center',
62 | flex: 1,
63 | justifyContent: 'center',
64 | padding: 16,
65 | },
66 | icon: {
67 | fontSize: 40,
68 | fontWeight: 'bold',
69 | },
70 | text: {
71 | marginTop: 16,
72 | textAlign: 'center',
73 | },
74 | });
75 |
--------------------------------------------------------------------------------
/src/components/IconButtonV2/IconButtonV2.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Pressable, StyleSheet, View, ViewStyle } from 'react-native';
3 | import MaterialCommunityIcons from '@react-native-vector-icons/material-design-icons';
4 | import Color from 'color';
5 |
6 | import { ThemeColors } from '../../theme/types';
7 | import { MaterialDesignIconName } from '@type/icon';
8 |
9 | type Props = {
10 | name: MaterialDesignIconName;
11 | color?: string;
12 | size?: number;
13 | disabled?: boolean;
14 | padding?: number;
15 | onPress?: () => void;
16 | theme: ThemeColors;
17 | style?: ViewStyle;
18 | };
19 |
20 | const IconButton: React.FC = ({
21 | name,
22 | color,
23 | size = 24,
24 | padding = 8,
25 | onPress,
26 | disabled,
27 | theme,
28 | style,
29 | }) => (
30 |
31 |
41 |
46 |
47 |
48 | );
49 |
50 | export default React.memo(IconButton);
51 |
52 | const styles = StyleSheet.create({
53 | container: {
54 | borderRadius: 50,
55 | overflow: 'hidden',
56 | },
57 | pressable: {
58 | padding: 8,
59 | },
60 | });
61 |
--------------------------------------------------------------------------------
/src/components/LoadingMoreIndicator/LoadingMoreIndicator.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { ActivityIndicator, StyleSheet } from 'react-native';
3 | import { ThemeColors } from '../../theme/types';
4 |
5 | interface Props {
6 | theme: ThemeColors;
7 | }
8 |
9 | const LoadingMoreIndicator: React.FC = ({ theme }) => (
10 |
11 | );
12 |
13 | export default LoadingMoreIndicator;
14 |
15 | const styles = StyleSheet.create({
16 | indicator: {
17 | alignItems: 'center',
18 | flex: 1,
19 | justifyContent: 'center',
20 | padding: 32,
21 | },
22 | });
23 |
--------------------------------------------------------------------------------
/src/components/LoadingScreenV2/LoadingScreenV2.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { StyleSheet, ActivityIndicator } from 'react-native';
3 |
4 | import { ThemeColors } from '../../theme/types';
5 |
6 | const LoadingScreen: React.FC<{ theme: ThemeColors }> = ({ theme }) => (
7 |
8 | );
9 |
10 | export default LoadingScreen;
11 |
12 | const styles = StyleSheet.create({
13 | indicator: {
14 | alignItems: 'center',
15 | flex: 1,
16 | justifyContent: 'center',
17 | },
18 | });
19 |
--------------------------------------------------------------------------------
/src/components/Modal/Modal.tsx:
--------------------------------------------------------------------------------
1 | import SafeAreaView from '@components/SafeAreaView/SafeAreaView';
2 | import { useTheme } from '@hooks/persisted';
3 | import React from 'react';
4 | import { StyleSheet } from 'react-native';
5 | import { ModalProps, overlay, Modal as PaperModal } from 'react-native-paper';
6 |
7 | const Modal: React.FC = ({
8 | children,
9 | visible,
10 | onDismiss,
11 | contentContainerStyle,
12 | ...props
13 | }) => {
14 | const theme = useTheme();
15 | return (
16 |
17 |
27 | {children}
28 |
29 |
30 | );
31 | };
32 |
33 | const styles = StyleSheet.create({
34 | modalContainer: {
35 | borderRadius: 28,
36 | margin: 30,
37 | padding: 24,
38 | shadowColor: 'transparent', // Modal weird shadow fix
39 | },
40 | });
41 |
42 | export default Modal;
43 |
--------------------------------------------------------------------------------
/src/components/RadioButton.tsx:
--------------------------------------------------------------------------------
1 | import { ThemeColors } from '@theme/types';
2 | import React from 'react';
3 | import { StyleSheet, View, Text } from 'react-native';
4 | import { RadioButton as MaterialRadioButton } from 'react-native-paper';
5 |
6 | interface RadioButtonGroupProps {
7 | children?: React.ReactNode;
8 | value: string;
9 | onValueChange: () => void;
10 | }
11 |
12 | interface RadioButtonProps {
13 | value: string;
14 | label: string;
15 | theme: ThemeColors;
16 | labelStyle: StyleSheet.AbsoluteFillStyle;
17 | }
18 |
19 | export const RadioButtonGroup = ({
20 | children,
21 | value,
22 | onValueChange,
23 | }: RadioButtonGroupProps) => (
24 |
25 | {children}
26 |
27 | );
28 |
29 | export const RadioButton = ({
30 | value,
31 | label,
32 | theme,
33 | labelStyle,
34 | }: RadioButtonProps) => (
35 |
36 |
41 |
44 | {label}
45 |
46 |
47 | );
48 |
49 | const styles = StyleSheet.create({
50 | radioButtonContainer: {
51 | alignItems: 'center',
52 | flexDirection: 'row',
53 | paddingVertical: 8,
54 | },
55 | radioButtonLabel: {
56 | fontSize: 16,
57 | marginLeft: 16,
58 | },
59 | });
60 |
--------------------------------------------------------------------------------
/src/components/RadioButton/RadioButton.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {
3 | Pressable,
4 | StyleProp,
5 | StyleSheet,
6 | Text,
7 | TextStyle,
8 | ViewStyle,
9 | } from 'react-native';
10 | import { RadioButton as PaperRadioButton } from 'react-native-paper';
11 | import { ThemeColors } from '../../theme/types';
12 |
13 | interface Props {
14 | label: string;
15 | status: boolean;
16 | onPress?: () => void;
17 | style?: StyleProp;
18 | labelStyle?: StyleProp;
19 | theme: ThemeColors;
20 | }
21 |
22 | export const RadioButton: React.FC = ({
23 | label,
24 | status,
25 | onPress,
26 | style,
27 | labelStyle,
28 | theme,
29 | }) => (
30 |
35 |
42 |
43 | {label}
44 |
45 |
46 | );
47 |
48 | const styles = StyleSheet.create({
49 | icon: {
50 | alignSelf: 'center',
51 | left: 24,
52 | position: 'absolute',
53 | },
54 | label: {
55 | marginLeft: 12,
56 | },
57 | pressable: {
58 | alignItems: 'center',
59 | flexDirection: 'row',
60 | paddingHorizontal: 16,
61 | paddingVertical: 6,
62 | },
63 | });
64 |
--------------------------------------------------------------------------------
/src/components/SafeAreaView/SafeAreaView.tsx:
--------------------------------------------------------------------------------
1 | import React, { memo } from 'react';
2 | import { StyleProp, StyleSheet, View, ViewStyle } from 'react-native';
3 | import { useSafeAreaInsets } from 'react-native-safe-area-context';
4 |
5 | interface SafeAreaViewProps {
6 | children: React.ReactNode;
7 | excludeTop?: boolean;
8 | excludeBottom?: boolean;
9 | style?: StyleProp;
10 | }
11 |
12 | const SafeAreaView: React.FC = ({
13 | children,
14 | style,
15 | excludeTop,
16 | excludeBottom,
17 | }) => {
18 | const { bottom, top, right, left } = useSafeAreaInsets();
19 | const styles = StyleSheet.create({
20 | container: {
21 | flex: 1,
22 | },
23 | padding: {
24 | paddingBottom: excludeBottom ? 0 : bottom,
25 | paddingLeft: left,
26 | paddingRight: right,
27 | paddingTop: excludeTop ? 0 : top,
28 | },
29 | });
30 | return (
31 | {children}
32 | );
33 | };
34 |
35 | export default memo(SafeAreaView);
36 |
--------------------------------------------------------------------------------
/src/components/Skeleton/useLoadingColors.tsx:
--------------------------------------------------------------------------------
1 | import { ThemeColors } from '@theme/types';
2 | import color from 'color';
3 |
4 | const useLoadingColors = (theme: ThemeColors) => {
5 | const highlightColor = color(theme.primary).alpha(0.08).string();
6 | const backgroundColor = color(theme.surface);
7 |
8 | let adjustedBackgroundColor;
9 |
10 | if (backgroundColor.isDark()) {
11 | adjustedBackgroundColor =
12 | backgroundColor.luminosity() !== 0
13 | ? backgroundColor.lighten(0.1).toString()
14 | : backgroundColor.negate().darken(0.98).toString();
15 | } else {
16 | adjustedBackgroundColor = backgroundColor.darken(0.04).toString();
17 | }
18 |
19 | return [highlightColor, adjustedBackgroundColor];
20 | };
21 | export default useLoadingColors;
22 |
--------------------------------------------------------------------------------
/src/components/Switch/SwitchItem.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {
3 | Pressable,
4 | StyleSheet,
5 | View,
6 | Text,
7 | ViewStyle,
8 | StyleProp,
9 | } from 'react-native';
10 | import Switch from './Switch';
11 | import { ThemeColors } from '../../theme/types';
12 |
13 | interface SwitchItemProps {
14 | value: boolean;
15 | label: string;
16 | description?: string;
17 | onPress: () => void;
18 | theme: ThemeColors;
19 | size?: number;
20 | style?: StyleProp;
21 | }
22 |
23 | const SwitchItem: React.FC = ({
24 | label,
25 | description,
26 | onPress,
27 | theme,
28 | value,
29 | size,
30 | style,
31 | }) => (
32 |
37 |
38 | {label}
39 | {description ? (
40 |
41 | {description}
42 |
43 | ) : null}
44 |
45 |
51 |
52 | );
53 |
54 | export default SwitchItem;
55 |
56 | const styles = StyleSheet.create({
57 | container: {
58 | alignItems: 'center',
59 | flexDirection: 'row',
60 | justifyContent: 'space-between',
61 | paddingVertical: 12,
62 | },
63 | description: {
64 | fontSize: 12,
65 | lineHeight: 20,
66 | },
67 | label: {
68 | fontSize: 16,
69 | },
70 | labelContainer: {
71 | flex: 1,
72 | justifyContent: 'center',
73 | },
74 | switch: {
75 | marginLeft: 8,
76 | },
77 | });
78 |
--------------------------------------------------------------------------------
/src/components/index.ts:
--------------------------------------------------------------------------------
1 | export { default as IconButtonV2 } from './IconButtonV2/IconButtonV2';
2 | export { default as SearchbarV2 } from './SearchbarV2/SearchbarV2';
3 | export { default as LoadingScreenV2 } from './LoadingScreenV2/LoadingScreenV2';
4 | export { default as ErrorScreenV2 } from './ErrorScreenV2/ErrorScreenV2';
5 | export { default as EmptyView } from './EmptyView/EmptyView';
6 | export { default as Chip } from './Chip/Chip';
7 | export { default as Button } from './Button/Button';
8 | export { default as Appbar } from './Appbar/Appbar';
9 | export { default as SwitchItem } from './Switch/SwitchItem';
10 | export { default as List } from './List/List';
11 | export { default as ColorPreferenceItem } from './ColorPreferenceItem/ColorPreferenceItem';
12 | export { default as LoadingMoreIndicator } from './LoadingMoreIndicator/LoadingMoreIndicator';
13 | export { Checkbox } from './Checkbox/Checkbox';
14 | export { RadioButton } from './RadioButton/RadioButton';
15 | export { default as ConfirmationDialog } from './ConfirmationDialog/ConfirmationDialog';
16 | export { default as SafeAreaView } from './SafeAreaView/SafeAreaView';
17 | export { default as Modal } from './Modal/Modal';
18 |
--------------------------------------------------------------------------------
/src/database/queries/HistoryQueries.ts:
--------------------------------------------------------------------------------
1 | import { History } from '@database/types';
2 |
3 | import { showToast } from '@utils/showToast';
4 | import { getString } from '@strings/translations';
5 | import { db } from '@database/db';
6 |
7 | export const getHistoryFromDb = () =>
8 | db.getAllAsync(`
9 | SELECT
10 | Chapter.*, Novel.pluginId, Novel.name as novelName, Novel.path as novelPath, Novel.cover as novelCover, Novel.id as novelId
11 | FROM Chapter
12 | JOIN Novel
13 | ON Chapter.novelId = Novel.id AND Chapter.readTime IS NOT NULL
14 | GROUP BY novelId
15 | HAVING readTime = MAX(readTime)
16 | ORDER BY readTime DESC
17 | `);
18 |
19 | export const insertHistory = async (chapterId: number) =>
20 | db.runAsync(
21 | "UPDATE Chapter SET readTime = datetime('now','localtime') WHERE id = ?",
22 | chapterId,
23 | );
24 |
25 | export const deleteChapterHistory = (chapterId: number) =>
26 | db.runAsync('UPDATE Chapter SET readTime = NULL WHERE id = ?', chapterId);
27 |
28 | export const deleteAllHistory = async () => {
29 | await db.execAsync('UPDATE Chapter SET readTime = NULL');
30 | showToast(getString('historyScreen.deleted'));
31 | };
32 |
--------------------------------------------------------------------------------
/src/database/queries/RepositoryQueries.ts:
--------------------------------------------------------------------------------
1 | import { Repository } from '@database/types';
2 | import { db } from '@database/db';
3 |
4 | export const getRepositoriesFromDb = () =>
5 | db.getAllSync('SELECT * FROM Repository');
6 |
7 | export const isRepoUrlDuplicated = (repoUrl: string) =>
8 | (db.getFirstSync<{ isDuplicated: number }>(
9 | 'SELECT COUNT(*) as isDuplicated FROM Repository WHERE url = ?',
10 | repoUrl,
11 | )?.isDuplicated || 0) > 0;
12 |
13 | export const createRepository = (repoUrl: string) =>
14 | db.runSync('INSERT INTO Repository (url) VALUES (?)', repoUrl);
15 |
16 | export const deleteRepositoryById = (id: number) =>
17 | db.runSync('DELETE FROM Repository WHERE id = ?', id);
18 |
19 | export const updateRepository = (id: number, url: string) =>
20 | db.runSync('UPDATE Repository SET url = ? WHERE id = ?', url, id);
21 |
--------------------------------------------------------------------------------
/src/database/tables/CategoryTable.ts:
--------------------------------------------------------------------------------
1 | import { getString } from '@strings/translations';
2 |
3 | export const createCategoriesTableQuery = `
4 | CREATE TABLE IF NOT EXISTS Category (
5 | id INTEGER PRIMARY KEY AUTOINCREMENT,
6 | name TEXT NOT NULL UNIQUE,
7 | sort INTEGER
8 | );
9 | `;
10 |
11 | export const createCategoryTriggerQuery = `
12 | CREATE TRIGGER IF NOT EXISTS add_category AFTER INSERT ON Category
13 | BEGIN
14 | UPDATE Category SET sort = (SELECT IFNULL(sort, new.id)) WHERE id = new.id;
15 | END;
16 | `;
17 |
18 | // if category with id = 2 exists, nothing in db.ts file is executed
19 | export const createCategoryDefaultQuery = `
20 | INSERT INTO Category (id, name, sort) VALUES
21 | (1, "${getString('categories.default')}", 1),
22 | (2, "${getString('categories.local')}", 2)
23 | `;
24 |
--------------------------------------------------------------------------------
/src/database/tables/ChapterTable.ts:
--------------------------------------------------------------------------------
1 | export const createChapterTableQuery = `
2 | CREATE TABLE IF NOT EXISTS Chapter (
3 | id INTEGER PRIMARY KEY AUTOINCREMENT,
4 | novelId INTEGER NOT NULL,
5 | path TEXT NOT NULL,
6 | name TEXT NOT NULL,
7 | releaseTime TEXT,
8 | bookmark INTEGER DEFAULT 0,
9 | unread INTEGER DEFAULT 1,
10 | readTime TEXT,
11 | isDownloaded INTEGER DEFAULT 0,
12 | updatedTime TEXT,
13 | chapterNumber REAL NULL,
14 | page TEXT DEFAULT "1",
15 | position INTEGER DEFAULT 0,
16 | progress INTEGER,
17 | UNIQUE(path, novelId),
18 | FOREIGN KEY (novelId) REFERENCES Novel(id) ON DELETE CASCADE
19 | )
20 | `;
21 |
22 | export const createChapterIndexQuery = `
23 | CREATE INDEX
24 | IF NOT EXISTS
25 | chapterNovelIdIndex ON Chapter(novelId, position,page, id)
26 | `;
27 |
28 | export const dropChapterIndexQuery = `
29 | DROP INDEX IF EXISTS chapterNovelIdIndex;
30 | `;
31 |
--------------------------------------------------------------------------------
/src/database/tables/NovelCategoryTable.ts:
--------------------------------------------------------------------------------
1 | export const createNovelCategoryTableQuery = `
2 | CREATE TABLE IF NOT EXISTS NovelCategory (
3 | id INTEGER PRIMARY KEY AUTOINCREMENT,
4 | novelId INTEGER NOT NULL,
5 | categoryId INTEGER NOT NULL,
6 | UNIQUE(novelId, categoryId),
7 | FOREIGN KEY (novelId) REFERENCES Novel(id) ON DELETE CASCADE,
8 | FOREIGN KEY (categoryId) REFERENCES Category(id) ON DELETE CASCADE
9 | );
10 | `;
11 |
--------------------------------------------------------------------------------
/src/database/tables/RepositoryTable.ts:
--------------------------------------------------------------------------------
1 | export const createRepositoryTableQuery = `
2 | CREATE TABLE IF NOT EXISTS Repository (
3 | id INTEGER PRIMARY KEY AUTOINCREMENT,
4 | url TEXT NOT NULL,
5 | UNIQUE(url)
6 | );
7 | `;
8 |
--------------------------------------------------------------------------------
/src/database/utils/convertDateToISOString.ts:
--------------------------------------------------------------------------------
1 | export const convertDateToISOString = (SQLiteDate: string) => {
2 | const dateParts: string[] = SQLiteDate.split('-');
3 |
4 | const JSDate = new Date(
5 | Number(dateParts[0]),
6 | Number(dateParts[1]) - 1,
7 | Number(dateParts[2].substring(0, 2)),
8 | );
9 |
10 | const date = JSDate.toISOString();
11 |
12 | return date;
13 | };
14 |
--------------------------------------------------------------------------------
/src/hooks/common/githubUpdateChecker.ts:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from 'react';
2 | import { version } from '../../../package.json';
3 | import { newer } from '@utils/compareVersion';
4 |
5 | interface GithubUpdate {
6 | isNewVersion: boolean;
7 | latestRelease: any;
8 | }
9 |
10 | export const useGithubUpdateChecker = (): GithubUpdate => {
11 | const latestReleaseUrl =
12 | 'https://api.github.com/repos/rajarsheechatterjee/lnreader/releases/latest';
13 |
14 | const [checking, setChecking] = useState(true);
15 | const [latestRelease, setLatestRelease] = useState();
16 |
17 | const checkForRelease = async () => {
18 | const res = await fetch(latestReleaseUrl);
19 | const data = await res.json();
20 |
21 | const release = {
22 | tag_name: data.tag_name,
23 | body: data.body,
24 | downloadUrl: data.assets[0].browser_download_url,
25 | };
26 |
27 | setLatestRelease(release);
28 | setChecking(false);
29 | };
30 |
31 | const isNewVersion = (versionTag: string) => {
32 | const currentVersion = `${version}`;
33 | const regex = /[^\\d.]/;
34 |
35 | const newVersion = versionTag.replace(regex, '');
36 |
37 | return newer(newVersion, currentVersion);
38 | };
39 |
40 | useEffect(() => {
41 | checkForRelease();
42 | // .catch(e => {
43 | // showToast(`Could not connect to github:\n${`${e}`.split(':')[1].trim()}`);
44 | // // console.error(e);
45 | // });
46 | }, []);
47 |
48 | if (!checking) {
49 | const data = {
50 | latestRelease,
51 | isNewVersion: isNewVersion(latestRelease.tag_name),
52 | };
53 |
54 | return data;
55 | } else {
56 | return {
57 | latestRelease: undefined,
58 | isNewVersion: false,
59 | };
60 | }
61 | };
62 |
--------------------------------------------------------------------------------
/src/hooks/common/useBackHandler.ts:
--------------------------------------------------------------------------------
1 | import { useEffect } from 'react';
2 | import { BackHandler } from 'react-native';
3 |
4 | export function useBackHandler(handler: () => boolean) {
5 | useEffect(() => {
6 | const backHandler = BackHandler.addEventListener(
7 | 'hardwareBackPress',
8 | handler,
9 | );
10 |
11 | return () => backHandler.remove();
12 | }, [handler]);
13 | }
14 |
--------------------------------------------------------------------------------
/src/hooks/common/useBoolean.ts:
--------------------------------------------------------------------------------
1 | import { Dispatch, SetStateAction, useCallback, useState } from 'react';
2 |
3 | // have no idea about the name
4 | export interface UseBooleanReturnType {
5 | value: boolean;
6 | setValue: Dispatch>;
7 | setTrue: () => void;
8 | setFalse: () => void;
9 | toggle: () => void;
10 | }
11 |
12 | const useBoolean = (defaultValue?: boolean): UseBooleanReturnType => {
13 | const [value, setValue] = useState(!!defaultValue);
14 |
15 | const setTrue = useCallback(() => setValue(true), []);
16 | const setFalse = useCallback(() => setValue(false), []);
17 | const toggle = useCallback(() => setValue(x => !x), []);
18 |
19 | return { value, setValue, setTrue, setFalse, toggle };
20 | };
21 |
22 | export default useBoolean;
23 |
--------------------------------------------------------------------------------
/src/hooks/common/useDeviceOrientation.ts:
--------------------------------------------------------------------------------
1 | import { useWindowDimensions } from 'react-native';
2 |
3 | export const useDeviceOrientation = () => {
4 | const window = useWindowDimensions();
5 |
6 | if (window.width > window.height) {
7 | return 'landscape';
8 | } else {
9 | return 'potrait';
10 | }
11 | };
12 |
--------------------------------------------------------------------------------
/src/hooks/common/usePreviousRouteName.ts:
--------------------------------------------------------------------------------
1 | import { useNavigationState } from '@react-navigation/native';
2 |
3 | export const usePreviousRouteName = () => {
4 | return useNavigationState(state =>
5 | state.routes[state.index - 1]?.name
6 | ? state.routes[state.index - 1].name
7 | : 'None',
8 | );
9 | };
10 |
--------------------------------------------------------------------------------
/src/hooks/common/useSearch.ts:
--------------------------------------------------------------------------------
1 | import { useIsFocused } from '@react-navigation/native';
2 | import { useCallback, useEffect, useMemo, useState } from 'react';
3 |
4 | const useSearch = (defaultSearchText?: string, clearSearchOnUnfocus = true) => {
5 | const isFocused = useIsFocused();
6 |
7 | const [searchText, setSearchText] = useState(defaultSearchText || '');
8 |
9 | const clearSearchbar = useCallback(() => setSearchText(''), []);
10 |
11 | useEffect(() => {
12 | if (clearSearchOnUnfocus) {
13 | if (!isFocused) {
14 | clearSearchbar();
15 | }
16 | }
17 | }, [clearSearchbar, clearSearchOnUnfocus, isFocused]);
18 |
19 | return useMemo(
20 | () => ({ searchText, setSearchText, clearSearchbar }),
21 | [searchText, setSearchText, clearSearchbar],
22 | );
23 | };
24 |
25 | export default useSearch;
26 |
--------------------------------------------------------------------------------
/src/hooks/index.ts:
--------------------------------------------------------------------------------
1 | export { default as useSearch } from './common/useSearch';
2 | export { default as useFullscreenMode } from './common/useFullscreenMode';
3 | export { default as useBoolean } from './common/useBoolean';
4 | export { usePreviousRouteName } from './common/usePreviousRouteName';
5 | export { useBackHandler } from './common/useBackHandler';
6 | export { useDeviceOrientation } from './common/useDeviceOrientation';
7 |
8 | // hook types
9 | export type { UseBooleanReturnType } from './common/useBoolean';
10 |
--------------------------------------------------------------------------------
/src/hooks/persisted/index.ts:
--------------------------------------------------------------------------------
1 | export { useTheme } from './useTheme';
2 | export { useUpdates, useLastUpdate } from './useUpdates';
3 | export { default as useCategories } from './useCategories';
4 | export { default as useHistory } from './useHistory';
5 | export {
6 | useAppSettings,
7 | useBrowseSettings,
8 | useLibrarySettings,
9 | useChapterGeneralSettings,
10 | useChapterReaderSettings,
11 | } from './useSettings';
12 | export { default as usePlugins } from './usePlugins';
13 | export { getTracker, useTracker } from './useTracker';
14 | export { useTrackedNovel, useNovel } from './useNovel';
15 | export { default as useDownload } from './useDownload';
16 | export { default as useUserAgent } from './useUserAgent';
17 |
--------------------------------------------------------------------------------
/src/hooks/persisted/useCategories.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react';
2 |
3 | import { getCategoriesFromDb } from '@database/queries/CategoryQueries';
4 | import { Category } from '@database/types';
5 |
6 | const useCategories = () => {
7 | const [isLoading, setIsLoading] = useState(true);
8 | const [categories, setCategories] = useState([]);
9 | const [error, setError] = useState();
10 |
11 | const getCategories = async () => {
12 | try {
13 | const res = await getCategoriesFromDb();
14 | setCategories(res);
15 | } catch (err) {
16 | if (err instanceof Error) {
17 | setError(err.message);
18 | }
19 | } finally {
20 | setIsLoading(false);
21 | }
22 | };
23 |
24 | useEffect(() => {
25 | getCategories();
26 | }, []);
27 |
28 | return {
29 | isLoading,
30 | categories,
31 | error,
32 | };
33 | };
34 |
35 | export default useCategories;
36 |
--------------------------------------------------------------------------------
/src/hooks/persisted/useDownload.ts:
--------------------------------------------------------------------------------
1 | import { ChapterInfo, NovelInfo } from '@database/types';
2 | import ServiceManager, {
3 | BackgroundTaskMetadata,
4 | DownloadChapterTask,
5 | QueuedBackgroundTask,
6 | } from '@services/ServiceManager';
7 | import { useMemo } from 'react';
8 | import { useMMKVObject } from 'react-native-mmkv';
9 |
10 | export const DOWNLOAD_QUEUE = 'DOWNLOAD';
11 | export const CHAPTER_DOWNLOADING = 'CHAPTER_DOWNLOADING';
12 |
13 | export default function useDownload() {
14 | const [queue] = useMMKVObject(
15 | ServiceManager.manager.STORE_KEY,
16 | );
17 |
18 | const downloadQueue = useMemo(
19 | () => queue?.filter(t => t.task?.name === 'DOWNLOAD_CHAPTER') || [],
20 | [queue],
21 | ) as { task: DownloadChapterTask; meta: BackgroundTaskMetadata }[];
22 |
23 | const downloadChapter = (novel: NovelInfo, chapter: ChapterInfo) =>
24 | ServiceManager.manager.addTask({
25 | name: 'DOWNLOAD_CHAPTER',
26 | data: {
27 | chapterId: chapter.id,
28 | novelName: novel.name,
29 | chapterName: chapter.name,
30 | },
31 | });
32 | const downloadChapters = (novel: NovelInfo, chapters: ChapterInfo[]) =>
33 | ServiceManager.manager.addTask(
34 | chapters.map(chapter => ({
35 | name: 'DOWNLOAD_CHAPTER',
36 | data: {
37 | chapterId: chapter.id,
38 | novelName: novel.name,
39 | chapterName: chapter.name,
40 | },
41 | })),
42 | );
43 | const resumeDowndload = () => ServiceManager.manager.resume();
44 |
45 | const pauseDownload = () => ServiceManager.manager.pause();
46 |
47 | const cancelDownload = () =>
48 | ServiceManager.manager.removeTasksByName('DOWNLOAD_CHAPTER');
49 |
50 | return {
51 | downloadQueue,
52 | resumeDowndload,
53 | downloadChapter,
54 | downloadChapters,
55 | pauseDownload,
56 | cancelDownload,
57 | };
58 | }
59 |
--------------------------------------------------------------------------------
/src/hooks/persisted/useHistory.ts:
--------------------------------------------------------------------------------
1 | import { useCallback, useState } from 'react';
2 | import { useFocusEffect } from '@react-navigation/native';
3 |
4 | import { History } from '@database/types';
5 |
6 | import {
7 | deleteAllHistory,
8 | deleteChapterHistory,
9 | getHistoryFromDb,
10 | } from '@database/queries/HistoryQueries';
11 | import dayjs from 'dayjs';
12 | import { parseChapterNumber } from '@utils/parseChapterNumber';
13 |
14 | const useHistory = () => {
15 | const [isLoading, setIsLoading] = useState(true);
16 | const [history, setHistory] = useState([]);
17 | const [error, setError] = useState();
18 |
19 | const getHistory = () =>
20 | getHistoryFromDb()
21 | .then(res =>
22 | setHistory(
23 | res.map(localHistory => {
24 | const parsedTime = dayjs(localHistory.releaseTime);
25 | return {
26 | ...localHistory,
27 | releaseTime: parsedTime.isValid()
28 | ? parsedTime.format('LL')
29 | : localHistory.releaseTime,
30 | chapterNumber: localHistory.chapterNumber
31 | ? localHistory.chapterNumber
32 | : parseChapterNumber(localHistory.novelName, localHistory.name),
33 | };
34 | }),
35 | ),
36 | )
37 | .catch((err: Error) => setError(err.message))
38 | .finally(() => setIsLoading(false));
39 |
40 | const clearAllHistory = () => {
41 | deleteAllHistory();
42 | getHistory();
43 | };
44 |
45 | const removeChapterFromHistory = async (chapterId: number) => {
46 | deleteChapterHistory(chapterId);
47 | getHistory();
48 | };
49 |
50 | useFocusEffect(
51 | useCallback(() => {
52 | getHistory();
53 | }, []),
54 | );
55 |
56 | return {
57 | isLoading,
58 | history,
59 | error,
60 | removeChapterFromHistory,
61 | clearAllHistory,
62 | };
63 | };
64 |
65 | export default useHistory;
66 |
--------------------------------------------------------------------------------
/src/hooks/persisted/useImport.ts:
--------------------------------------------------------------------------------
1 | import { useLibraryContext } from '@components/Context/LibraryContext';
2 | import ServiceManager, { BackgroundTask } from '@services/ServiceManager';
3 | import { DocumentPickerResult } from 'expo-document-picker';
4 | import { useCallback, useEffect, useMemo } from 'react';
5 | import { useMMKVObject } from 'react-native-mmkv';
6 |
7 | export default function useImport() {
8 | const { refetchLibrary } = useLibraryContext();
9 | const [queue] = useMMKVObject(
10 | ServiceManager.manager.STORE_KEY,
11 | );
12 | const importQueue = useMemo(
13 | () => queue?.filter(t => t.name === 'IMPORT_EPUB') || [],
14 | [queue],
15 | );
16 |
17 | useEffect(() => {
18 | refetchLibrary();
19 | }, [importQueue, refetchLibrary]);
20 |
21 | const importNovel = useCallback((pickedNovel: DocumentPickerResult) => {
22 | if (pickedNovel.canceled) return;
23 | ServiceManager.manager.addTask(
24 | pickedNovel.assets.map(asset => ({
25 | name: 'IMPORT_EPUB',
26 | data: {
27 | filename: asset.name,
28 | uri: asset.uri,
29 | },
30 | })),
31 | );
32 | }, []);
33 | const resumeImport = () => ServiceManager.manager.resume();
34 |
35 | const pauseImport = () => ServiceManager.manager.pause();
36 |
37 | const cancelImport = () =>
38 | ServiceManager.manager.removeTasksByName('IMPORT_EPUB');
39 |
40 | return {
41 | importQueue,
42 | importNovel,
43 | resumeImport,
44 | pauseImport,
45 | cancelImport,
46 | };
47 | }
48 |
--------------------------------------------------------------------------------
/src/hooks/persisted/useSelfHost.ts:
--------------------------------------------------------------------------------
1 | import { useMMKVString } from 'react-native-mmkv';
2 |
3 | export const SELF_HOST_BACKUP = 'SELF_HOST_BACKUP';
4 |
5 | export const useSelfHost = () => {
6 | const [host = '', setHost] = useMMKVString(SELF_HOST_BACKUP);
7 | return {
8 | host,
9 | setHost,
10 | };
11 | };
12 |
--------------------------------------------------------------------------------
/src/hooks/persisted/useTheme.ts:
--------------------------------------------------------------------------------
1 | import { useMemo } from 'react';
2 | import { Appearance } from 'react-native';
3 | import {
4 | useMMKVBoolean,
5 | useMMKVObject,
6 | useMMKVString,
7 | } from 'react-native-mmkv';
8 | import { overlay } from 'react-native-paper';
9 | import Color from 'color';
10 |
11 | import { defaultTheme } from '@theme/md3/defaultTheme';
12 | import { ThemeColors } from '@theme/types';
13 |
14 | const getElevationColor = (colors: ThemeColors, elevation: number) => {
15 | return Color(colors.surface)
16 | .mix(Color(colors.primary), elevation)
17 | .rgb()
18 | .string();
19 | };
20 |
21 | export const useTheme = (): ThemeColors => {
22 | const [appTheme] = useMMKVObject('APP_THEME');
23 | const [isAmoledBlack] = useMMKVBoolean('AMOLED_BLACK');
24 | const [customAccent] = useMMKVString('CUSTOM_ACCENT_COLOR');
25 |
26 | const theme: ThemeColors = useMemo(() => {
27 | const isDeviveColorSchemeDark = Appearance.getColorScheme() === 'dark';
28 |
29 | let colors: ThemeColors =
30 | appTheme ||
31 | (isDeviveColorSchemeDark ? defaultTheme.dark : defaultTheme.light);
32 |
33 | if (isAmoledBlack && colors.isDark) {
34 | colors = {
35 | ...colors,
36 | background: '#000000',
37 | surface: '#000000',
38 | };
39 | }
40 |
41 | if (customAccent) {
42 | colors = {
43 | ...colors,
44 | primary: customAccent,
45 | secondary: customAccent,
46 | };
47 | }
48 |
49 | colors = {
50 | ...colors,
51 | surface2: getElevationColor(colors, 0.08),
52 | overlay3: overlay(3, colors.surface),
53 | rippleColor: Color(colors.primary).alpha(0.12).toString(),
54 | surfaceReader: Color(colors.surface).alpha(0.9).toString(),
55 | };
56 |
57 | return colors;
58 | }, [appTheme, isAmoledBlack, customAccent]);
59 |
60 | return theme;
61 | };
62 |
--------------------------------------------------------------------------------
/src/hooks/persisted/useTracker.ts:
--------------------------------------------------------------------------------
1 | import { AuthenticationResult, Tracker, TrackerName } from '@services/Trackers';
2 | import { aniListTracker } from '@services/Trackers/aniList';
3 | import { myAnimeListTracker } from '@services/Trackers/myAnimeList';
4 | import { useMMKVObject } from 'react-native-mmkv';
5 |
6 | export const TRACKER = 'TRACKER';
7 | export const TRACKED_NOVELS = 'TRACKED_NOVELS';
8 |
9 | export type TrackerMetadata = {
10 | name: TrackerName;
11 | auth: AuthenticationResult;
12 | };
13 |
14 | const trackers: Record = {
15 | AniList: aniListTracker,
16 | MyAnimeList: myAnimeListTracker,
17 | };
18 |
19 | export const getTracker = (name: TrackerName) => {
20 | return trackers[name];
21 | };
22 |
23 | export function useTracker() {
24 | const [tracker, setValue] = useMMKVObject(TRACKER);
25 | const setTracker = (name: TrackerName, auth: AuthenticationResult) =>
26 | setValue({ name, auth });
27 | const removeTracker = () => setValue(undefined);
28 | return {
29 | tracker,
30 | setTracker,
31 | removeTracker,
32 | };
33 | }
34 |
--------------------------------------------------------------------------------
/src/hooks/persisted/useUserAgent.ts:
--------------------------------------------------------------------------------
1 | import { MMKVStorage } from '@utils/mmkv/mmkv';
2 | import { useMMKVString } from 'react-native-mmkv';
3 | import { getUserAgentSync } from 'react-native-device-info';
4 |
5 | export const USER_AGENT = 'USER_AGENT';
6 |
7 | export const getUserAgent = () => {
8 | return MMKVStorage.getString(USER_AGENT) || getUserAgentSync();
9 | };
10 |
11 | export default function useUserAgent() {
12 | const [userAgent = getUserAgentSync(), setUserAgent] =
13 | useMMKVString(USER_AGENT);
14 |
15 | return {
16 | userAgent,
17 | setUserAgent,
18 | };
19 | }
20 |
--------------------------------------------------------------------------------
/src/navigators/ReaderStack.tsx:
--------------------------------------------------------------------------------
1 | import React, { useRef } from 'react';
2 |
3 | import { createNativeStackNavigator } from '@react-navigation/native-stack';
4 |
5 | // Screens
6 | import Novel from '../screens/novel/NovelScreen';
7 | import Reader from '../screens/reader/ReaderScreen';
8 |
9 | import {
10 | ChapterScreenProps,
11 | NovelScreenProps,
12 | ReaderStackParamList,
13 | } from './types';
14 | import { NovelContextProvider } from '@screens/novel/NovelContext';
15 |
16 | const Stack = createNativeStackNavigator();
17 |
18 | const stackNavigatorConfig = { headerShown: false };
19 |
20 | // @ts-ignore
21 | const ReaderStack = ({ route }) => {
22 | const params = useRef(route?.params);
23 |
24 | return (
25 |
32 |
33 |
34 |
35 |
36 |
37 | );
38 | };
39 |
40 | export default ReaderStack;
41 |
--------------------------------------------------------------------------------
/src/plugins/helpers/constants.ts:
--------------------------------------------------------------------------------
1 | export const defaultCover =
2 | 'https://github.com/LNReader/lnreader-plugins/blob/master/public/static/coverNotAvailable.webp?raw=true';
3 |
--------------------------------------------------------------------------------
/src/plugins/helpers/isAbsoluteUrl.ts:
--------------------------------------------------------------------------------
1 | export const isUrlAbsolute = (url: string) => {
2 | if (url) {
3 | if (url.indexOf('//') === 0) {
4 | return true;
5 | } // URL is protocol-relative (= absolute)
6 | if (url.indexOf('://') === -1) {
7 | return false;
8 | } // URL has no protocol (= relative)
9 | if (url.indexOf('.') === -1) {
10 | return false;
11 | } // URL does not contain a dot, i.e. no TLD (= relative, possibly REST)
12 | if (url.indexOf('/') === -1) {
13 | return false;
14 | } // URL does not contain a single slash (= relative)
15 | if (url.indexOf(':') > url.indexOf('/')) {
16 | return false;
17 | } // The first colon comes after the first slash (= relative)
18 | if (url.indexOf('://') < url.indexOf('.')) {
19 | return true;
20 | } // Protocol is defined before first dot (= absolute)
21 | }
22 | return false; // Anything else must be relative
23 | };
24 |
--------------------------------------------------------------------------------
/src/screens/BrowseSourceScreen/components/filterUtils.ts:
--------------------------------------------------------------------------------
1 | import {
2 | FilterToValues,
3 | FilterTypes,
4 | Filters,
5 | isCheckboxValue,
6 | isPickerValue,
7 | isSwitchValue,
8 | isTextValue,
9 | ValueOfFilter,
10 | isXCheckboxValue,
11 | } from '@plugins/types/filterTypes';
12 |
13 | export const getValueFor = (
14 | filter: Filters[string],
15 | value: FilterToValues[string],
16 | ): ValueOfFilter => {
17 | switch (filter.type) {
18 | case FilterTypes.CheckboxGroup:
19 | return (
20 | isCheckboxValue(value) ? value.value : filter.value
21 | ) as ValueOfFilter;
22 | case FilterTypes.Picker:
23 | return (
24 | isPickerValue(value) ? (value.value as ValueOfFilter) : filter.value
25 | ) as ValueOfFilter;
26 | case FilterTypes.Switch:
27 | return (
28 | isSwitchValue(value) ? value.value : filter.value
29 | ) as ValueOfFilter;
30 | case FilterTypes.TextInput:
31 | return (
32 | isTextValue(value) ? value.value : filter.value
33 | ) as ValueOfFilter;
34 | case FilterTypes.ExcludableCheckboxGroup:
35 | return (
36 | isXCheckboxValue(value) ? value.value : filter.value
37 | ) as ValueOfFilter;
38 | default:
39 | throw 'Invalid filter type!';
40 | }
41 | };
42 |
--------------------------------------------------------------------------------
/src/screens/Categories/components/CategorySkeletonLoading.tsx:
--------------------------------------------------------------------------------
1 | import React, { memo } from 'react';
2 | import { StyleSheet, View } from 'react-native';
3 | import { createShimmerPlaceholder } from 'react-native-shimmer-placeholder';
4 | import { LinearGradient } from 'expo-linear-gradient';
5 | import { ThemeColors } from '@theme/types';
6 | import useLoadingColors from '@utils/useLoadingColors';
7 | import { useAppSettings } from '@hooks/persisted/index';
8 |
9 | interface Props {
10 | width: number;
11 | height: number;
12 | theme: ThemeColors;
13 | }
14 |
15 | const CategorySkeletonLoading: React.FC = ({ height, width, theme }) => {
16 | const { disableLoadingAnimations } = useAppSettings();
17 | const ShimmerPlaceHolder = createShimmerPlaceholder(LinearGradient);
18 |
19 | const [highlightColor, backgroundColor] = useLoadingColors(theme);
20 |
21 | const renderLoadingCard = (item: number, index: number) => {
22 | return (
23 |
24 |
31 |
32 | );
33 | };
34 |
35 | const items = [];
36 | for (let index = 0; index < Math.random() * 6 + 3; index++) {
37 | items.push(0);
38 | }
39 |
40 | return {items.map(renderLoadingCard)};
41 | };
42 |
43 | const styles = StyleSheet.create({
44 | categoryCard: {
45 | borderRadius: 12,
46 | marginBottom: 8,
47 | marginHorizontal: 16,
48 | },
49 | contentCtn: {
50 | paddingBottom: 100,
51 | paddingVertical: 16,
52 | },
53 | });
54 |
55 | export default memo(CategorySkeletonLoading);
56 |
--------------------------------------------------------------------------------
/src/screens/Categories/components/DeleteCategoryModal.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { StyleSheet, Text, View } from 'react-native';
3 | import { Portal } from 'react-native-paper';
4 |
5 | import { Button, Modal } from '@components/index';
6 |
7 | import { Category } from '@database/types';
8 | import { deleteCategoryById } from '@database/queries/CategoryQueries';
9 | import { useTheme } from '@hooks/persisted';
10 |
11 | import { getString } from '@strings/translations';
12 |
13 | interface DeleteCategoryModalProps {
14 | category: Category;
15 | visible: boolean;
16 | closeModal: () => void;
17 | onSuccess: () => Promise;
18 | }
19 |
20 | const DeleteCategoryModal: React.FC = ({
21 | category,
22 | closeModal,
23 | visible,
24 | onSuccess,
25 | }) => {
26 | const theme = useTheme();
27 | return (
28 |
29 |
30 |
31 | {getString('categories.deleteModal.header')}
32 |
33 |
34 | {getString('categories.deleteModal.desc')}
35 | {` "${category.name}"?`}
36 |
37 |
38 |
48 |
49 |
50 | );
51 | };
52 |
53 | export default DeleteCategoryModal;
54 |
55 | const styles = StyleSheet.create({
56 | btnContainer: {
57 | flexDirection: 'row-reverse',
58 | marginTop: 24,
59 | },
60 | modalDesc: {},
61 | modalTitle: {
62 | fontSize: 24,
63 | marginBottom: 16,
64 | },
65 | });
66 |
--------------------------------------------------------------------------------
/src/screens/GlobalSearchScreen/GlobalSearchScreen.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { ProgressBar } from 'react-native-paper';
4 |
5 | import { EmptyView, SafeAreaView, SearchbarV2 } from '@components/index';
6 | import GlobalSearchResultsList from './components/GlobalSearchResultsList';
7 |
8 | import { useSearch } from '@hooks';
9 | import { useTheme } from '@hooks/persisted';
10 |
11 | import { getString } from '@strings/translations';
12 | import { useGlobalSearch } from './hooks/useGlobalSearch';
13 |
14 | interface Props {
15 | route?: {
16 | params?: {
17 | searchText?: string;
18 | };
19 | };
20 | }
21 |
22 | const GlobalSearchScreen = (props: Props) => {
23 | const theme = useTheme();
24 | const { searchText, setSearchText, clearSearchbar } = useSearch(
25 | props?.route?.params?.searchText,
26 | false,
27 | );
28 | const onChangeText = (text: string) => setSearchText(text);
29 | const onSubmitEditing = () => globalSearch(searchText);
30 |
31 | const { searchResults, globalSearch, progress } = useGlobalSearch({
32 | defaultSearchText: searchText,
33 | });
34 |
35 | return (
36 |
37 |
46 | {progress ? (
47 |
51 | ) : null}
52 |
62 | }
63 | />
64 |
65 | );
66 | };
67 |
68 | export default GlobalSearchScreen;
69 |
--------------------------------------------------------------------------------
/src/screens/browse/SourceNovels.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { StyleSheet, View, FlatList, Text, FlatListProps } from 'react-native';
3 | import { useTheme } from '@hooks/persisted';
4 |
5 | import ListView from '../../components/ListView';
6 | import { Appbar } from '@components';
7 | import { SourceNovelsScreenProps } from '@navigators/types';
8 | import { NovelInfo } from '@database/types';
9 | import { getString } from '@strings/translations';
10 | import { useLibraryContext } from '@components/Context/LibraryContext';
11 |
12 | const SourceNovels = ({ navigation, route }: SourceNovelsScreenProps) => {
13 | const pluginId = route.params.pluginId;
14 | const theme = useTheme();
15 | const { library } = useLibraryContext();
16 |
17 | const sourceNovels = library.filter(novel => novel.pluginId === pluginId);
18 |
19 | const renderItem: FlatListProps['renderItem'] = ({ item }) => (
20 |
24 | navigation.navigate('MigrateNovel', {
25 | novel: item,
26 | })
27 | }
28 | />
29 | );
30 |
31 | return (
32 |
33 |
38 | 'migrateFrom' + item.id}
41 | renderItem={renderItem}
42 | ListEmptyComponent={
43 |
51 | {getString('browseScreen.noSource')}
52 |
53 | }
54 | />
55 |
56 | );
57 | };
58 |
59 | export default SourceNovels;
60 |
61 | const styles = StyleSheet.create({
62 | container: { flex: 1 },
63 | text: { padding: 20, textAlign: 'center' },
64 | });
65 |
--------------------------------------------------------------------------------
/src/screens/browse/discover/TrackerCard.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {
3 | StyleSheet,
4 | Text,
5 | View,
6 | Image,
7 | Pressable,
8 | ImageSourcePropType,
9 | } from 'react-native';
10 | import { getString } from '@strings/translations';
11 | import { Button } from '@components';
12 |
13 | import { ThemeColors } from '@theme/types';
14 |
15 | interface Props {
16 | trackerName: string;
17 | icon: ImageSourcePropType;
18 | onPress: () => void;
19 | theme: ThemeColors;
20 | }
21 |
22 | const TrackerCard: React.FC = ({
23 | theme,
24 | icon,
25 | trackerName,
26 | onPress,
27 | }) => {
28 | return (
29 |
34 |
35 |
36 |
37 | {trackerName}
38 |
39 |
40 |
41 |
46 |
47 |
48 | );
49 | };
50 |
51 | export default TrackerCard;
52 |
53 | const styles = StyleSheet.create({
54 | container: {
55 | alignItems: 'center',
56 | flexDirection: 'row',
57 | justifyContent: 'space-between',
58 | padding: 16,
59 | paddingVertical: 12,
60 | },
61 | details: {
62 | marginLeft: 16,
63 | },
64 | flexRow: {
65 | alignItems: 'center',
66 | flexDirection: 'row',
67 | },
68 | icon: {
69 | borderRadius: 4,
70 | height: 40,
71 | width: 40,
72 | },
73 | });
74 |
--------------------------------------------------------------------------------
/src/screens/browse/globalsearch/GlobalSearchNovelCover.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { StyleSheet, View, Text, Image, Pressable } from 'react-native';
3 | import { coverPlaceholderColor } from '@theme/colors';
4 | import { ThemeColors } from '@theme/types';
5 | import { NovelItem } from '@plugins/types';
6 |
7 | interface GlobalSearchNovelCoverProps {
8 | novel: NovelItem;
9 | theme: ThemeColors;
10 | onPress: () => void;
11 | inLibrary: boolean;
12 | onLongPress: () => void;
13 | }
14 |
15 | const GlobalSearchNovelCover = ({
16 | novel,
17 | theme,
18 | onPress,
19 | inLibrary,
20 | onLongPress,
21 | }: GlobalSearchNovelCoverProps) => {
22 | const { name, cover } = novel;
23 |
24 | const uri = cover;
25 |
26 | const opacity = inLibrary ? 0.5 : 1;
27 |
28 | return (
29 |
30 |
36 |
41 |
45 | {name}
46 |
47 |
48 |
49 | );
50 | };
51 |
52 | export default GlobalSearchNovelCover;
53 |
54 | const styles = StyleSheet.create({
55 | container: {
56 | borderRadius: 6,
57 | flex: 1,
58 | overflow: 'hidden',
59 | },
60 | novelCover: {
61 | backgroundColor: coverPlaceholderColor,
62 | borderRadius: 4,
63 | height: 150,
64 | width: 115,
65 | },
66 | pressable: {
67 | borderRadius: 4,
68 | flex: 1,
69 | paddingHorizontal: 5,
70 | paddingVertical: 4,
71 | },
72 | title: {
73 | flexWrap: 'wrap',
74 | fontFamily: 'pt-sans-bold',
75 | fontSize: 14,
76 | padding: 4,
77 | width: 115,
78 | },
79 | });
80 |
--------------------------------------------------------------------------------
/src/screens/browse/loadingAnimation/GlobalSearchSkeletonLoading.tsx:
--------------------------------------------------------------------------------
1 | import React, { memo } from 'react';
2 | import { View, StyleSheet } from 'react-native';
3 | import { ThemeColors } from '@theme/types';
4 | import LoadingNovel from './LoadingNovel';
5 | import useLoadingColors from '@utils/useLoadingColors';
6 | import { DisplayModes } from '@screens/library/constants/constants';
7 |
8 | interface Props {
9 | theme: ThemeColors;
10 | }
11 |
12 | const GlobalSearchSkeletonLoading: React.FC = ({ theme }) => {
13 | const styles = createStyleSheet();
14 |
15 | const [highlightColor, backgroundColor] = useLoadingColors(theme);
16 |
17 | const items: Array = [1, 2, 3, 4];
18 | return (
19 |
20 | {items.map((item: number, index: number) => {
21 | return (
22 |
30 | );
31 | })}
32 |
33 | );
34 | };
35 |
36 | const createStyleSheet = () => {
37 | return StyleSheet.create({
38 | container: {
39 | marginBottom: 6,
40 | marginHorizontal: 4,
41 | marginTop: 6,
42 | overflow: 'hidden',
43 | },
44 | row: {
45 | flexDirection: 'row',
46 | paddingHorizontal: 3,
47 | },
48 | });
49 | };
50 |
51 | export default memo(GlobalSearchSkeletonLoading);
52 |
--------------------------------------------------------------------------------
/src/screens/browse/settings/modals/ConcurrentSearchesModal.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Text, StyleSheet } from 'react-native';
3 |
4 | import { Portal } from 'react-native-paper';
5 |
6 | import { RadioButton } from '@components/RadioButton/RadioButton';
7 | import { ThemeColors } from '@theme/types';
8 | import { getString } from '@strings/translations';
9 | import { useBrowseSettings } from '@hooks/persisted/index';
10 | import { Modal } from '@components';
11 |
12 | interface DisplayModeModalProps {
13 | globalSearchConcurrency: number;
14 | modalVisible: boolean;
15 | hideModal: () => void;
16 | theme: ThemeColors;
17 | }
18 |
19 | const ConcurrentSearchesModal: React.FC = ({
20 | theme,
21 | globalSearchConcurrency,
22 | hideModal,
23 | modalVisible,
24 | }) => {
25 | const { setBrowseSettings } = useBrowseSettings();
26 |
27 | return (
28 |
29 |
30 |
31 | {getString('browseSettingsScreen.concurrentSearches')}
32 |
33 | {[1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map(concurrency => (
34 |
38 | setBrowseSettings({ globalSearchConcurrency: concurrency })
39 | }
40 | label={concurrency.toString()}
41 | theme={theme}
42 | />
43 | ))}
44 |
45 |
46 | );
47 | };
48 |
49 | export default ConcurrentSearchesModal;
50 |
51 | const styles = StyleSheet.create({
52 | modalHeader: {
53 | fontSize: 24,
54 | marginBottom: 10,
55 | paddingHorizontal: 24,
56 | },
57 | });
58 |
--------------------------------------------------------------------------------
/src/screens/history/components/ClearHistoryDialog.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { StyleSheet } from 'react-native';
3 | import { Dialog, Portal } from 'react-native-paper';
4 |
5 | import { Button } from '@components';
6 | import { getString } from '@strings/translations';
7 | import { ThemeColors } from '@theme/types';
8 |
9 | interface ClearHistoryDialogProps {
10 | visible: boolean;
11 | theme: ThemeColors;
12 | onSubmit: () => void;
13 | onDismiss: () => void;
14 | }
15 |
16 | const ClearHistoryDialog: React.FC = ({
17 | visible,
18 | onDismiss,
19 | theme,
20 | onSubmit,
21 | }) => {
22 | const handleOnSubmit = () => {
23 | onSubmit();
24 | onDismiss();
25 | };
26 |
27 | return (
28 |
29 |
42 |
43 | );
44 | };
45 |
46 | export default ClearHistoryDialog;
47 |
48 | const styles = StyleSheet.create({
49 | button: {
50 | marginLeft: 4,
51 | },
52 | container: {
53 | borderRadius: 28,
54 | margin: 20,
55 | },
56 | title: {
57 | fontSize: 16,
58 | letterSpacing: 0,
59 | },
60 | });
61 |
--------------------------------------------------------------------------------
/src/screens/library/components/Banner.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { StyleSheet, Text, View } from 'react-native';
3 |
4 | import MaterialCommunityIcons from '@react-native-vector-icons/material-design-icons';
5 | import { ThemeColors } from '../../../theme/types';
6 | import { MaterialDesignIconName } from '@type/icon';
7 |
8 | interface Props {
9 | label: string;
10 | icon?: MaterialDesignIconName;
11 | backgroundColor?: string;
12 | textColor?: string;
13 | theme: ThemeColors;
14 | }
15 |
16 | export const Banner: React.FC = ({
17 | label,
18 | icon,
19 | theme,
20 | backgroundColor = theme.primary,
21 | textColor = theme.onPrimary,
22 | }) => (
23 |
24 | {icon ? (
25 |
31 | ) : null}
32 | {label}
33 |
34 | );
35 |
36 | const styles = StyleSheet.create({
37 | bannerText: {
38 | fontSize: 12,
39 | fontWeight: 'bold',
40 | },
41 | container: {
42 | alignItems: 'center',
43 | flexDirection: 'row',
44 | justifyContent: 'center',
45 | paddingVertical: 4,
46 | },
47 | icon: {
48 | marginRight: 8,
49 | },
50 | });
51 |
--------------------------------------------------------------------------------
/src/screens/more/components/MoreHeader.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Image, StyleSheet, View } from 'react-native';
3 |
4 | import { Appbar, List } from '@components';
5 | import { AboutScreenProps, MoreStackScreenProps } from '@navigators/types';
6 | import { ThemeColors } from '@theme/types';
7 |
8 | interface MoreHeaderProps {
9 | title: string;
10 | navigation:
11 | | AboutScreenProps['navigation']
12 | | MoreStackScreenProps['navigation'];
13 | theme: ThemeColors;
14 | goBack?: boolean;
15 | }
16 |
17 | export const MoreHeader = ({
18 | title,
19 | navigation,
20 | theme,
21 | goBack,
22 | }: MoreHeaderProps) => (
23 | <>
24 |
30 |
31 |
32 |
36 |
37 |
38 |
39 | >
40 | );
41 |
42 | const styles = StyleSheet.create({
43 | logo: {
44 | height: 90,
45 | width: 90,
46 | },
47 | logoContainer: {
48 | alignItems: 'center',
49 | paddingBottom: 24,
50 | paddingTop: 4,
51 | },
52 | overflow: {
53 | overflow: 'hidden',
54 | paddingBottom: 4,
55 | },
56 | });
57 |
--------------------------------------------------------------------------------
/src/screens/more/components/RemoveDownloadsDialog.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { Button } from '@components';
4 | import { Dialog, overlay, Portal } from 'react-native-paper';
5 | import { ThemeColors } from '@theme/types';
6 | import { getString } from '@strings/translations';
7 | import { StyleSheet } from 'react-native';
8 |
9 | interface RemoveDownloadsDialogProps {
10 | dialogVisible: boolean;
11 | hideDialog: () => void;
12 | theme: ThemeColors;
13 | onSubmit: () => void;
14 | }
15 |
16 | const RemoveDownloadsDialog = ({
17 | dialogVisible,
18 | hideDialog,
19 | theme,
20 | onSubmit,
21 | }: RemoveDownloadsDialogProps) => {
22 | return (
23 |
24 |
49 |
50 | );
51 | };
52 |
53 | export default RemoveDownloadsDialog;
54 |
55 | const styles = StyleSheet.create({
56 | fontSize: {
57 | letterSpacing: 0,
58 | fontSize: 16,
59 | },
60 | borderRadius: { borderRadius: 6 },
61 | });
62 |
--------------------------------------------------------------------------------
/src/screens/novel/components/Info/ReadButton.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { Button } from '@components';
4 | import { getString } from '@strings/translations';
5 | import { ChapterInfo } from '@database/types';
6 | import { useAppSettings } from '@hooks/persisted';
7 | import Animated, { ZoomIn } from 'react-native-reanimated';
8 | import { StyleSheet } from 'react-native';
9 |
10 | interface ReadButtonProps {
11 | chapters: ChapterInfo[];
12 | lastRead?: ChapterInfo;
13 | navigateToChapter: (chapter: ChapterInfo) => void;
14 | }
15 |
16 | const ReadButton = ({
17 | chapters,
18 | lastRead,
19 | navigateToChapter,
20 | }: ReadButtonProps) => {
21 | const { useFabForContinueReading = false } = useAppSettings();
22 |
23 | const navigateToLastReadChapter = () => {
24 | if (lastRead) {
25 | navigateToChapter(lastRead);
26 | } else if (chapters.length) {
27 | navigateToChapter(chapters[0]);
28 | }
29 | };
30 |
31 | if (!useFabForContinueReading) {
32 | return chapters.length > 0 || lastRead ? (
33 |
34 |
46 |
47 | ) : null;
48 | } else {
49 | return null;
50 | }
51 | };
52 |
53 | export default ReadButton;
54 |
55 | const styles = StyleSheet.create({
56 | margin: { margin: 16 },
57 | });
58 |
--------------------------------------------------------------------------------
/src/screens/novel/components/Tracker/MyAnimeList.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {
3 | RadioButton,
4 | RadioButtonGroup,
5 | } from '../../../../components/RadioButton';
6 |
7 | export const MyAnimeListScoreSelector = ({
8 | trackItem,
9 | updateTrackScore,
10 | theme,
11 | }) => {
12 | const getScoreLabel = score => {
13 | const myanimeListScores = {
14 | 0: 'No Score',
15 | 1: 'Apalling',
16 | 2: 'Horrible',
17 | 3: 'Very Bad',
18 | 4: 'Bad',
19 | 5: 'Average',
20 | 6: 'Fine',
21 | 7: 'Good',
22 | 8: 'Very Good',
23 | 9: 'Great',
24 | 10: 'Masterpiece',
25 | };
26 |
27 | return `(${score}) ${myanimeListScores[score]}`;
28 | };
29 |
30 | return (
31 |
32 | {[...Array(11).keys()].map((item, index) => (
33 |
39 | ))}
40 |
41 | );
42 | };
43 |
--------------------------------------------------------------------------------
/src/screens/novel/components/Tracker/SetTrackChaptersDialog.js:
--------------------------------------------------------------------------------
1 | import { Modal } from '@components';
2 | import React, { useState } from 'react';
3 | import { StyleSheet, Text } from 'react-native';
4 | import { TextInput } from 'react-native-paper';
5 |
6 | const SetTrackChaptersDialog = ({
7 | trackItem,
8 | trackChaptersDialog,
9 | setTrackChaptersDialog,
10 | updateTrackChapters,
11 | theme,
12 | }) => {
13 | const [trackChapters, setTrackChapters] = useState(trackItem.progress);
14 |
15 | return (
16 | setTrackChaptersDialog(false)}
19 | >
20 |
21 | Chapters
22 |
23 | setTrackChapters(text ? +text : '')}
26 | onSubmitEditing={() => updateTrackChapters(trackChapters)}
27 | mode="outlined"
28 | keyboardType="numeric"
29 | theme={{
30 | colors: {
31 | primary: theme.primary,
32 | placeholder: theme.outline,
33 | text: theme.onSurface,
34 | background: 'transparent',
35 | },
36 | }}
37 | underlineColor={theme.outline}
38 | />
39 |
40 | );
41 | };
42 |
43 | export default SetTrackChaptersDialog;
44 |
45 | const styles = StyleSheet.create({
46 | dialogTitle: {
47 | fontSize: 24,
48 | marginBottom: 16,
49 | },
50 | });
51 |
--------------------------------------------------------------------------------
/src/screens/novel/components/Tracker/SetTrackScoreDialog.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { StyleSheet, Text } from 'react-native';
3 | import { MyAnimeListScoreSelector } from './MyAnimeList';
4 | import { AniListScoreSelector } from './AniList';
5 | import { Modal } from '@components';
6 |
7 | const SetTrackScoreDialog = ({
8 | tracker,
9 | trackItem,
10 | trackScoreDialog,
11 | setTrackScoreDialog,
12 | updateTrackScore,
13 | theme,
14 | }) => {
15 | return (
16 | setTrackScoreDialog(false)}
19 | >
20 |
21 | Score
22 |
23 | {tracker.name === 'MyAnimeList' ? (
24 |
29 | ) : (
30 |
36 | )}
37 |
38 | );
39 | };
40 |
41 | export default SetTrackScoreDialog;
42 |
43 | const styles = StyleSheet.create({
44 | dialogTitle: {
45 | fontSize: 24,
46 | marginBottom: 16,
47 | },
48 | });
49 |
--------------------------------------------------------------------------------
/src/screens/novel/components/Tracker/SetTrackStatusDialog.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { StyleSheet, Text } from 'react-native';
3 | import {
4 | RadioButton,
5 | RadioButtonGroup,
6 | } from '../../../../components/RadioButton';
7 | import { Modal } from '@components';
8 |
9 | const SetTrackStatusDialog = ({
10 | trackItem,
11 | trackStatusDialog,
12 | setTrackStatusDialog,
13 | updateTrackStatus,
14 | theme,
15 | }) => {
16 | /** @type {Record} */
17 | const statusTypes = {
18 | CURRENT: 'Reading',
19 | PLANNING: 'Plan to read',
20 | COMPLETED: 'Completed',
21 | DROPPED: 'Dropped',
22 | PAUSED: 'On Hold',
23 | REPEATING: 'Rereading',
24 | };
25 |
26 | return (
27 | setTrackStatusDialog(false)}
30 | >
31 |
32 | Status
33 |
34 |
38 | {Object.keys(statusTypes).map((key, index) => (
39 |
45 | ))}
46 |
47 |
48 | );
49 | };
50 |
51 | export default SetTrackStatusDialog;
52 |
53 | const styles = StyleSheet.create({
54 | dialogTitle: {
55 | fontSize: 24,
56 | marginBottom: 16,
57 | },
58 | });
59 |
--------------------------------------------------------------------------------
/src/screens/reader/ChapterContext.tsx:
--------------------------------------------------------------------------------
1 | import React, { createContext, useContext, useMemo, useRef } from 'react';
2 | import { ChapterInfo, NovelInfo } from '@database/types';
3 | import WebView from 'react-native-webview';
4 | import useChapter from './hooks/useChapter';
5 |
6 | type ChapterContextType = ReturnType & {
7 | novel: NovelInfo;
8 | webViewRef: React.RefObject | null>;
9 | };
10 |
11 | const defaultValue = {} as ChapterContextType;
12 |
13 | const ChapterContext = createContext(defaultValue);
14 |
15 | export function ChapterContextProvider({
16 | children,
17 | novel,
18 | initialChapter,
19 | }: {
20 | children: React.JSX.Element;
21 | novel: NovelInfo;
22 | initialChapter: ChapterInfo;
23 | }) {
24 | const webViewRef = useRef(null);
25 | const chapterHookContent = useChapter(webViewRef, initialChapter, novel);
26 |
27 | const contextValue = useMemo(
28 | () => ({
29 | novel,
30 | webViewRef,
31 | ...chapterHookContent,
32 | }),
33 | [novel, webViewRef, chapterHookContent],
34 | );
35 |
36 | return (
37 |
38 | {children}
39 |
40 | );
41 | }
42 |
43 | export const useChapterContext = () => {
44 | return useContext(ChapterContext);
45 | };
46 |
--------------------------------------------------------------------------------
/src/screens/reader/ChapterLoadingScreen/ChapterLoadingScreen.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { View } from 'react-native';
3 | import color from 'color';
4 |
5 | import SkeletonLines from '../components/SkeletonLines';
6 | import { useChapterReaderSettings } from '@hooks/persisted';
7 |
8 | const ChapterLoadingScreen = () => {
9 | const {
10 | theme: backgroundColor,
11 | padding,
12 | textSize,
13 | lineHeight,
14 | } = useChapterReaderSettings();
15 |
16 | return (
17 |
18 |
39 |
40 | );
41 | };
42 |
43 | export default ChapterLoadingScreen;
44 |
--------------------------------------------------------------------------------
/src/screens/reader/components/ChapterDrawer/RenderListChapter.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { View, Pressable, TextStyle, StyleProp, ViewStyle } from 'react-native';
3 | import { Text } from 'react-native-paper';
4 | import color from 'color';
5 | import { ChapterInfo } from '@database/types';
6 | import { ThemeColors } from '@theme/types';
7 |
8 | type Styles = {
9 | chapterCtn: StyleProp;
10 | drawerElementContainer: StyleProp;
11 | chapterNameCtn: StyleProp;
12 | releaseDateCtn: StyleProp;
13 | };
14 |
15 | type Props = {
16 | item: ChapterInfo;
17 | styles: Styles;
18 | theme: ThemeColors;
19 | chapterId: number;
20 | onPress: () => void;
21 | };
22 |
23 | const renderListChapter = ({
24 | item,
25 | styles,
26 | theme,
27 | onPress,
28 | chapterId,
29 | }: Props) => {
30 | return (
31 |
39 |
44 |
51 | {item.name}
52 |
53 | {item.releaseTime ? (
54 |
60 | {item.releaseTime}
61 |
62 | ) : null}
63 |
64 |
65 | );
66 | };
67 | export default renderListChapter;
68 |
--------------------------------------------------------------------------------
/src/screens/reader/components/KeepScreenAwake.tsx:
--------------------------------------------------------------------------------
1 | import { useKeepAwake } from 'expo-keep-awake';
2 |
3 | const KeepScreenAwake = () => {
4 | useKeepAwake();
5 |
6 | return null;
7 | };
8 |
9 | export default KeepScreenAwake;
10 |
--------------------------------------------------------------------------------
/src/screens/reader/components/ReaderBottomSheet/ReaderSheetPreferenceItem.tsx:
--------------------------------------------------------------------------------
1 | import React, { Suspense } from 'react';
2 | import { Pressable, StyleSheet, Text, View } from 'react-native';
3 | import { ThemeColors } from '../../../../theme/types';
4 | import Switch from '@components/Switch/Switch';
5 |
6 | interface ReaderSheetPreferenceItemProps {
7 | label: string;
8 | value: boolean;
9 | onPress: () => void;
10 | theme: ThemeColors;
11 | }
12 |
13 | const ReaderSheetPreferenceItem: React.FC = ({
14 | label,
15 | value,
16 | onPress,
17 | theme,
18 | }) => {
19 | const size = 20;
20 | return (
21 |
26 |
27 | {label}
28 |
29 |
38 | }
39 | >
40 |
41 |
42 |
43 | );
44 | };
45 |
46 | export default ReaderSheetPreferenceItem;
47 |
48 | const styles = StyleSheet.create({
49 | container: {
50 | alignItems: 'center',
51 | flexDirection: 'row',
52 | justifyContent: 'space-between',
53 | paddingHorizontal: 16,
54 | paddingVertical: 12,
55 | },
56 | label: {
57 | flex: 1,
58 | paddingRight: 16,
59 | },
60 | });
61 |
--------------------------------------------------------------------------------
/src/screens/reader/components/ReaderBottomSheet/ReaderTextAlignSelector.tsx:
--------------------------------------------------------------------------------
1 | import { StyleSheet, Text, TextStyle, View } from 'react-native';
2 | import React from 'react';
3 |
4 | import { useChapterReaderSettings, useTheme } from '@hooks/persisted';
5 | import { textAlignments } from '@utils/constants/readerConstants';
6 | import { ToggleButton } from '@components/Common/ToggleButton';
7 | import { getString } from '@strings/translations';
8 |
9 | interface ReaderTextAlignSelectorProps {
10 | labelStyle?: TextStyle | TextStyle[];
11 | }
12 |
13 | const ReaderTextAlignSelector: React.FC = ({
14 | labelStyle,
15 | }) => {
16 | const theme = useTheme();
17 | const { textAlign, setChapterReaderSettings } = useChapterReaderSettings();
18 |
19 | return (
20 |
21 |
22 | {getString('readerScreen.bottomSheet.textAlign')}
23 |
24 |
25 | {textAlignments.map(item => (
26 | setChapterReaderSettings({ textAlign: item.value })}
32 | />
33 | ))}
34 |
35 |
36 | );
37 | };
38 |
39 | export default ReaderTextAlignSelector;
40 |
41 | const styles = StyleSheet.create({
42 | buttonContainer: {
43 | flexDirection: 'row',
44 | },
45 | container: {
46 | alignItems: 'center',
47 | flexDirection: 'row',
48 | justifyContent: 'space-between',
49 | marginVertical: 6,
50 | paddingHorizontal: 16,
51 | },
52 | });
53 |
--------------------------------------------------------------------------------
/src/screens/reader/components/ReaderBottomSheet/TextSizeSlider.tsx:
--------------------------------------------------------------------------------
1 | import { StyleSheet, Text, View } from 'react-native';
2 | import React from 'react';
3 |
4 | import { useChapterReaderSettings, useTheme } from '@hooks/persisted';
5 | import Slider from '@react-native-community/slider';
6 | import { getString } from '@strings/translations';
7 |
8 | const TRACK_TINT_COLOR = '#000000';
9 |
10 | const TextSizeSlider: React.FC = () => {
11 | const theme = useTheme();
12 |
13 | const { textSize, setChapterReaderSettings } = useChapterReaderSettings();
14 |
15 | return (
16 |
17 |
18 | {getString('readerScreen.bottomSheet.textSize')}
19 |
20 |
30 | setChapterReaderSettings({ textSize: value })
31 | }
32 | />
33 |
34 | );
35 | };
36 |
37 | export default TextSizeSlider;
38 |
39 | const styles = StyleSheet.create({
40 | container: {
41 | alignItems: 'center',
42 | flexDirection: 'row',
43 | justifyContent: 'space-between',
44 | },
45 | label: {
46 | paddingHorizontal: 16,
47 | textAlign: 'center',
48 | },
49 | slider: {
50 | flex: 1,
51 | height: 40,
52 | },
53 | });
54 |
--------------------------------------------------------------------------------
/src/screens/reader/utils/sanitizeChapterText.ts:
--------------------------------------------------------------------------------
1 | import { getString } from '@strings/translations';
2 | import sanitizeHtml from 'sanitize-html';
3 |
4 | export const sanitizeChapterText = (
5 | pluginId: string,
6 | novelName: string,
7 | chapterName: string,
8 | html: string,
9 | ): string => {
10 | const text = sanitizeHtml(html, {
11 | allowedTags: sanitizeHtml.defaults.allowedTags.concat([
12 | 'img',
13 | 'i',
14 | 'em',
15 | 'b',
16 | 'a',
17 | 'div',
18 | 'ol',
19 | 'li',
20 | 'title',
21 | ]),
22 | allowedAttributes: {
23 | a: ['href', 'class', 'id'],
24 | div: ['class', 'id'],
25 | img: ['src', 'class', 'id'],
26 | ol: ['reversed', 'start', 'type'],
27 | p: ['class', 'id'],
28 | span: ['class', 'id'],
29 | },
30 | allowedSchemes: ['data', 'http', 'https', 'file'],
31 | });
32 |
33 | // Return the sanitized and updated HTML or an error message
34 | return (
35 | text ||
36 | getString('readerScreen.emptyChapterMessage', {
37 | pluginId,
38 | novelName,
39 | chapterName,
40 | })
41 | );
42 | };
43 |
--------------------------------------------------------------------------------
/src/screens/settings/SettingsGeneralScreen/modals/DisplayModeModal.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | DisplayModes,
3 | displayModesList,
4 | } from '@screens/library/constants/constants';
5 | import React from 'react';
6 | import { Text, StyleSheet } from 'react-native';
7 |
8 | import { Portal } from 'react-native-paper';
9 |
10 | import { RadioButton } from '@components/RadioButton/RadioButton';
11 | import { ThemeColors } from '@theme/types';
12 | import { useLibrarySettings } from '@hooks/persisted';
13 | import { getString } from '@strings/translations';
14 | import { Modal } from '@components';
15 |
16 | interface DisplayModeModalProps {
17 | displayMode: DisplayModes;
18 | displayModalVisible: boolean;
19 | hideDisplayModal: () => void;
20 | theme: ThemeColors;
21 | }
22 |
23 | const DisplayModeModal: React.FC = ({
24 | theme,
25 | displayMode,
26 | hideDisplayModal,
27 | displayModalVisible,
28 | }) => {
29 | const { setLibrarySettings } = useLibrarySettings();
30 |
31 | return (
32 |
33 |
34 |
35 | {getString('generalSettingsScreen.displayMode')}
36 |
37 | {displayModesList.map(mode => (
38 | setLibrarySettings({ displayMode: mode.value })}
42 | label={mode.label}
43 | theme={theme}
44 | />
45 | ))}
46 |
47 |
48 | );
49 | };
50 |
51 | export default DisplayModeModal;
52 |
53 | const styles = StyleSheet.create({
54 | modalHeader: {
55 | fontSize: 24,
56 | marginBottom: 10,
57 | },
58 | });
59 |
--------------------------------------------------------------------------------
/src/screens/settings/SettingsLibraryScreen/DefaultCategoryDialog.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { FlatList, StyleSheet } from 'react-native';
3 | import { Button, Dialog } from 'react-native-paper';
4 |
5 | import { RadioButton } from '@components';
6 |
7 | import { getString } from '@strings/translations';
8 | import { useTheme } from '@hooks/persisted';
9 |
10 | import { Category } from '@database/types';
11 |
12 | interface DefaultCategoryDialogProps {
13 | visible: boolean;
14 | hideDialog: () => void;
15 | categories: Category[];
16 | defaultCategoryId: number;
17 | setDefaultCategory: (categoryId: number) => void;
18 | }
19 |
20 | const DefaultCategoryDialog: React.FC = ({
21 | categories,
22 | defaultCategoryId,
23 | hideDialog,
24 | visible,
25 | setDefaultCategory,
26 | }) => {
27 | const theme = useTheme();
28 |
29 | return (
30 |
60 | );
61 | };
62 |
63 | export default DefaultCategoryDialog;
64 |
65 | const styles = StyleSheet.create({
66 | scrollArea: {
67 | maxHeight: 480,
68 | },
69 | });
70 |
--------------------------------------------------------------------------------
/src/screens/settings/SettingsLibraryScreen/SettingsLibraryScreen.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Appbar, List } from '@components';
3 | import { getString } from '@strings/translations';
4 | import { useBoolean } from '@hooks';
5 | import { useCategories, useTheme } from '@hooks/persisted';
6 | import { useNavigation } from '@react-navigation/native';
7 | import { Portal } from 'react-native-paper';
8 | import DefaultCategoryDialog from './DefaultCategoryDialog';
9 |
10 | const SettingsLibraryScreen = () => {
11 | const theme = useTheme();
12 | const { goBack, navigate } = useNavigation();
13 | const { categories } = useCategories();
14 |
15 | const defaultCategoryDialog = useBoolean();
16 |
17 | const setDefaultCategoryId = (categoryId: number) => {
18 | // TODO: update default category
19 |
20 | categoryId;
21 | };
22 |
23 | return (
24 | <>
25 |
30 |
31 | navigate('MoreStack', { screen: 'Categories' })}
37 | theme={theme}
38 | />
39 | category.sort === 1)?.name}
42 | onPress={defaultCategoryDialog.setTrue}
43 | theme={theme}
44 | />
45 |
46 |
47 |
54 |
55 | >
56 | );
57 | };
58 |
59 | export default SettingsLibraryScreen;
60 |
--------------------------------------------------------------------------------
/src/screens/settings/SettingsReaderScreen/Modals/FontPickerModal.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { Portal } from 'react-native-paper';
4 | import { RadioButton } from '@components/RadioButton/RadioButton';
5 |
6 | import { useChapterReaderSettings, useTheme } from '@hooks/persisted';
7 |
8 | import { readerFonts } from '@utils/constants/readerConstants';
9 | import { Modal } from '@components';
10 |
11 | interface FontPickerModalProps {
12 | visible: boolean;
13 | onDismiss: () => void;
14 | currentFont: string;
15 | }
16 |
17 | const FontPickerModal: React.FC = ({
18 | currentFont,
19 | onDismiss,
20 | visible,
21 | }) => {
22 | const theme = useTheme();
23 | const { setChapterReaderSettings } = useChapterReaderSettings();
24 |
25 | return (
26 |
27 |
28 | {readerFonts.map(item => (
29 |
33 | setChapterReaderSettings({ fontFamily: item.fontFamily })
34 | }
35 | label={item.name}
36 | labelStyle={{ fontFamily: item.fontFamily }}
37 | theme={theme}
38 | />
39 | ))}
40 |
41 |
42 | );
43 | };
44 |
45 | export default FontPickerModal;
46 |
--------------------------------------------------------------------------------
/src/screens/settings/SettingsReaderScreen/ReaderTextSize.tsx:
--------------------------------------------------------------------------------
1 | import { StyleSheet, Text, TextStyle, View } from 'react-native';
2 | import React from 'react';
3 |
4 | import { useChapterReaderSettings, useTheme } from '@hooks/persisted';
5 | import { IconButtonV2 } from '@components/index';
6 | import { getString } from '@strings/translations';
7 |
8 | interface ReaderTextSizeProps {
9 | labelStyle?: TextStyle | TextStyle[];
10 | }
11 |
12 | const ReaderTextSize: React.FC = ({ labelStyle }) => {
13 | const theme = useTheme();
14 | const { textSize, setChapterReaderSettings } = useChapterReaderSettings();
15 |
16 | return (
17 |
18 |
19 | {getString('readerScreen.bottomSheet.textSize')}
20 |
21 |
22 | setChapterReaderSettings({ textSize: textSize - 1 })}
28 | theme={theme}
29 | />
30 |
31 | {textSize}
32 |
33 | setChapterReaderSettings({ textSize: textSize + 1 })}
38 | theme={theme}
39 | />
40 |
41 |
42 | );
43 | };
44 |
45 | export default ReaderTextSize;
46 |
47 | const styles = StyleSheet.create({
48 | buttonContainer: {
49 | alignItems: 'center',
50 | flexDirection: 'row',
51 | },
52 | container: {
53 | alignItems: 'center',
54 | flexDirection: 'row',
55 | justifyContent: 'space-between',
56 | marginVertical: 6,
57 | paddingHorizontal: 16,
58 | },
59 | value: {
60 | paddingHorizontal: 24,
61 | },
62 | });
63 |
--------------------------------------------------------------------------------
/src/screens/settings/SettingsReaderScreen/Settings/DisplaySettings.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { List } from '@components/index';
4 |
5 | import { useChapterGeneralSettings, useTheme } from '@hooks/persisted';
6 | import { getString } from '@strings/translations';
7 | import SettingSwitch from '../../components/SettingSwitch';
8 |
9 | const DisplaySettings: React.FC = () => {
10 | const theme = useTheme();
11 |
12 | const {
13 | fullScreenMode = true,
14 | showScrollPercentage = true,
15 | showBatteryAndTime = false,
16 | setChapterGeneralSettings,
17 | } = useChapterGeneralSettings();
18 |
19 | return (
20 | <>
21 |
22 | {getString('common.display')}
23 |
24 |
28 | setChapterGeneralSettings({ fullScreenMode: !fullScreenMode })
29 | }
30 | theme={theme}
31 | />
32 |
36 | setChapterGeneralSettings({
37 | showScrollPercentage: !showScrollPercentage,
38 | })
39 | }
40 | theme={theme}
41 | />
42 |
46 | setChapterGeneralSettings({ showBatteryAndTime: !showBatteryAndTime })
47 | }
48 | theme={theme}
49 | />
50 | >
51 | );
52 | };
53 | export default DisplaySettings;
54 |
--------------------------------------------------------------------------------
/src/screens/settings/SettingsRepositoryScreen/components/DeleteRepositoryModal.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { StyleSheet, Text, View } from 'react-native';
3 | import { Portal } from 'react-native-paper';
4 |
5 | import { Button, Modal } from '@components/index';
6 |
7 | import { Repository } from '@database/types';
8 | import { deleteRepositoryById } from '@database/queries/RepositoryQueries';
9 | import { useTheme } from '@hooks/persisted';
10 |
11 | import { getString } from '@strings/translations';
12 |
13 | interface DeleteRepositoryModalProps {
14 | repository: Repository;
15 | visible: boolean;
16 | closeModal: () => void;
17 | onSuccess: () => void;
18 | }
19 |
20 | const DeleteRepositoryModal: React.FC = ({
21 | repository,
22 | closeModal,
23 | visible,
24 | onSuccess,
25 | }) => {
26 | const theme = useTheme();
27 | return (
28 |
29 |
30 |
31 | {'Delete repository'}
32 |
33 |
34 | {`Do you wish to delete repository "${repository.url}"?`}
35 |
36 |
37 | {
40 | deleteRepositoryById(repository.id);
41 | closeModal();
42 | onSuccess();
43 | }}
44 | />
45 |
46 |
47 |
48 |
49 | );
50 | };
51 |
52 | export default DeleteRepositoryModal;
53 |
54 | const styles = StyleSheet.create({
55 | btnContainer: {
56 | flexDirection: 'row-reverse',
57 | marginTop: 24,
58 | },
59 | modalDesc: {},
60 | modalTitle: {
61 | fontSize: 24,
62 | marginBottom: 16,
63 | },
64 | });
65 |
--------------------------------------------------------------------------------
/src/screens/settings/components/DefaultChapterSortModal.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { Portal } from 'react-native-paper';
4 | import { SortItem } from '@components/Checkbox/Checkbox';
5 |
6 | import { ThemeColors } from '@theme/types';
7 | import { AppSettings } from '@hooks/persisted/useSettings';
8 | import { getString } from '@strings/translations';
9 | import { Modal } from '@components';
10 |
11 | interface DefaultChapterSortModalProps {
12 | theme: ThemeColors;
13 | setAppSettings: (values: Partial) => void;
14 | defaultChapterSort: string;
15 | hideDisplayModal: () => void;
16 | displayModalVisible: boolean;
17 | }
18 |
19 | const DefaultChapterSortModal = ({
20 | theme,
21 | setAppSettings,
22 | defaultChapterSort,
23 | hideDisplayModal,
24 | displayModalVisible,
25 | }: DefaultChapterSortModalProps) => {
26 | return (
27 |
28 |
29 |
36 | defaultChapterSort === 'ORDER BY position ASC'
37 | ? setAppSettings({
38 | defaultChapterSort: 'ORDER BY position DESC',
39 | })
40 | : setAppSettings({ defaultChapterSort: 'ORDER BY position ASC' })
41 | }
42 | />
43 |
44 |
45 | );
46 | };
47 |
48 | export default DefaultChapterSortModal;
49 |
--------------------------------------------------------------------------------
/src/screens/settings/components/SettingSwitch.tsx:
--------------------------------------------------------------------------------
1 | import { SwitchItem } from '@components';
2 | import { ThemeColors } from '@theme/types';
3 | import { StyleSheet } from 'react-native';
4 |
5 | interface SettingSwitchProps {
6 | value: boolean;
7 | label: string;
8 | description?: string;
9 | onPress: () => void;
10 | theme: ThemeColors;
11 | }
12 |
13 | export default function SettingSwitch({
14 | value,
15 | label,
16 | description,
17 | onPress,
18 | theme,
19 | }: SettingSwitchProps) {
20 | return (
21 |
29 | );
30 | }
31 |
32 | const styles = StyleSheet.create({
33 | paddingHorizontal: { paddingHorizontal: 16 },
34 | });
35 |
--------------------------------------------------------------------------------
/src/services/backup/types.ts:
--------------------------------------------------------------------------------
1 | export enum ZipBackupName {
2 | DATA = 'data.zip',
3 | DOWNLOAD = 'download.zip',
4 | }
5 |
6 | export enum BackupEntryName {
7 | VERSION = 'Version.json',
8 | CATEGORY = 'Category.json',
9 | SETTING = 'Setting.json',
10 | NOVEL_AND_CHAPTERS = 'NovelAndChapters',
11 | }
12 |
--------------------------------------------------------------------------------
/src/services/plugin/fetch.ts:
--------------------------------------------------------------------------------
1 | import { getPlugin } from '@plugins/pluginManager';
2 | import { isUrlAbsolute } from '@plugins/helpers/isAbsoluteUrl';
3 |
4 | export const fetchNovel = async (pluginId: string, novelPath: string) => {
5 | const plugin = getPlugin(pluginId);
6 | if (!plugin) {
7 | throw new Error(`Unknown plugin: ${pluginId}`);
8 | }
9 | const res = await plugin.parseNovel(novelPath);
10 | return res;
11 | };
12 |
13 | export const fetchChapter = async (pluginId: string, chapterPath: string) => {
14 | const plugin = getPlugin(pluginId);
15 | let chapterText = `Unkown plugin: ${pluginId}`;
16 | if (plugin) {
17 | chapterText = await plugin.parseChapter(chapterPath);
18 | }
19 | return chapterText;
20 | };
21 |
22 | export const fetchChapters = async (pluginId: string, novelPath: string) => {
23 | const plugin = getPlugin(pluginId);
24 | if (!plugin) {
25 | throw new Error(`Unknown plugin: ${pluginId}`);
26 | }
27 | const res = await plugin.parseNovel(novelPath);
28 | return res?.chapters;
29 | };
30 |
31 | export const fetchPage = async (
32 | pluginId: string,
33 | novelPath: string,
34 | page: string,
35 | ) => {
36 | const plugin = getPlugin(pluginId);
37 | if (!plugin || !plugin.parsePage) {
38 | throw new Error('Cant parse page!');
39 | }
40 | const res = await plugin.parsePage(novelPath, page);
41 | return res;
42 | };
43 |
44 | export const resolveUrl = (
45 | pluginId: string,
46 | path: string,
47 | isNovel?: boolean,
48 | ) => {
49 | if (isUrlAbsolute(path)) {
50 | return path;
51 | }
52 | const plugin = getPlugin(pluginId);
53 | try {
54 | if (!plugin) {
55 | throw new Error(`Unknown plugin: ${pluginId}`);
56 | }
57 | if (plugin.resolveUrl) {
58 | return plugin.resolveUrl(path, isNovel);
59 | }
60 | } catch {
61 | return path;
62 | }
63 | return plugin.site + path;
64 | };
65 |
--------------------------------------------------------------------------------
/src/theme/colors.ts:
--------------------------------------------------------------------------------
1 | export const coverPlaceholderColor = '#8888881F';
2 |
3 | export const filterColor: (isDark: boolean) => string = isDark =>
4 | isDark ? '#FFC107' : '#FFC107';
5 |
--------------------------------------------------------------------------------
/src/theme/md3/index.ts:
--------------------------------------------------------------------------------
1 | import { defaultTheme } from './defaultTheme';
2 | import { midnightDusk } from './mignightDusk';
3 | import { tealTurquoise } from './tealTurquoise';
4 | import { yotsubaTheme } from './yotsuba';
5 | import { lavenderTheme } from './lavender';
6 | import { strawberryDaiquiriTheme } from './strawberry';
7 | import { takoTheme } from './tako';
8 | export const lightThemes = [
9 | defaultTheme.light,
10 | midnightDusk.light,
11 | tealTurquoise.light,
12 | yotsubaTheme.light,
13 | lavenderTheme.light,
14 | strawberryDaiquiriTheme.light,
15 | takoTheme.light,
16 | ];
17 | export const darkThemes = [
18 | defaultTheme.dark,
19 | midnightDusk.dark,
20 | tealTurquoise.dark,
21 | yotsubaTheme.dark,
22 | lavenderTheme.dark,
23 | strawberryDaiquiriTheme.dark,
24 | takoTheme.dark,
25 | ];
26 |
--------------------------------------------------------------------------------
/src/theme/types/index.ts:
--------------------------------------------------------------------------------
1 | export interface MD3ThemeType {
2 | id: number;
3 | name: string;
4 | isDark: boolean;
5 | primary: string;
6 | onPrimary: string;
7 | primaryContainer: string;
8 | onPrimaryContainer: string;
9 | secondary: string;
10 | onSecondary: string;
11 | secondaryContainer: string;
12 | onSecondaryContainer: string;
13 | tertiary: string;
14 | onTertiary: string;
15 | tertiaryContainer: string;
16 | onTertiaryContainer: string;
17 | error: string;
18 | onError: string;
19 | errorContainer: string;
20 | onErrorContainer: string;
21 | background: string;
22 | onBackground: string;
23 | surface: string;
24 | onSurface: string;
25 | surfaceVariant: string;
26 | onSurfaceVariant: string;
27 | outline: string;
28 | outlineVariant: string;
29 | shadow: string;
30 | scrim: string;
31 | inverseSurface: string;
32 | inverseOnSurface: string;
33 | inversePrimary: string;
34 | surfaceDisabled: string;
35 | onSurfaceDisabled: string;
36 | backdrop: string;
37 | }
38 |
39 | export interface ThemeColors extends MD3ThemeType {
40 | rippleColor?: string;
41 | surface2?: string;
42 | overlay3?: string;
43 | surfaceReader?: string;
44 | }
45 |
--------------------------------------------------------------------------------
/src/theme/utils/setBarColor.ts:
--------------------------------------------------------------------------------
1 | import { StatusBar } from 'react-native';
2 | import { ThemeColors } from '@theme/types';
3 | import * as NavigationBar from 'expo-navigation-bar';
4 | import Color, { ColorInstance } from 'color';
5 |
6 | export const setStatusBarColor = (color: ThemeColors | ColorInstance) => {
7 | if (color instanceof Color) {
8 | // fullscreen reader mode
9 | StatusBar.setBarStyle(color.isDark() ? 'light-content' : 'dark-content');
10 | StatusBar.setBackgroundColor(color.hexa());
11 | } else {
12 | StatusBar.setTranslucent(true);
13 | StatusBar.setBackgroundColor('transparent');
14 | StatusBar.setBarStyle(color.isDark ? 'light-content' : 'dark-content');
15 | }
16 | };
17 |
18 | export const changeNavigationBarColor = (color: string, isDark = false) => {
19 | NavigationBar.setBackgroundColorAsync(color);
20 | NavigationBar.setButtonStyleAsync(isDark ? 'light' : 'dark');
21 | };
22 |
--------------------------------------------------------------------------------
/src/type/icon.ts:
--------------------------------------------------------------------------------
1 | import MaterialCommunityIcons from '@react-native-vector-icons/material-design-icons';
2 |
3 | export type MaterialDesignIconName = Parameters<
4 | typeof MaterialCommunityIcons
5 | >[0]['name'];
6 |
--------------------------------------------------------------------------------
/src/utils/Storages.ts:
--------------------------------------------------------------------------------
1 | import NativeFile from '@specs/NativeFile';
2 |
3 | export const ROOT_STORAGE = NativeFile.getConstants().ExternalDirectoryPath;
4 | export const PLUGIN_STORAGE = ROOT_STORAGE + '/Plugins';
5 | export const NOVEL_STORAGE = ROOT_STORAGE + '/Novels';
6 |
--------------------------------------------------------------------------------
/src/utils/askForPostNoftificationsPermission.ts:
--------------------------------------------------------------------------------
1 | import { PermissionsAndroid, Platform } from 'react-native';
2 |
3 | export async function askForPostNotificationsPermission(): Promise {
4 | if (Platform.OS !== 'android' || Platform.Version < 33) return true;
5 |
6 | const granted = await PermissionsAndroid.request(
7 | PermissionsAndroid.PERMISSIONS.POST_NOTIFICATIONS,
8 | {
9 | title: 'Notification Permission',
10 | message: 'We need your permission to show you notifications',
11 | buttonNeutral: 'Ask Me Later',
12 | buttonNegative: 'Cancel',
13 | buttonPositive: 'OK',
14 | },
15 | );
16 | return granted === PermissionsAndroid.RESULTS.GRANTED;
17 | }
18 |
--------------------------------------------------------------------------------
/src/utils/compareVersion.ts:
--------------------------------------------------------------------------------
1 | export const older = (a: string, b: string): boolean => {
2 | a = a.replace(/[^\d.]/g, '');
3 | b = b.replace(/[^\d.]/g, '');
4 | const arrA = a.split('.').map(value => Number(value));
5 | const arrB = b.split('.').map(value => Number(value));
6 | try {
7 | for (let i = 0; i < Math.min(arrA.length, arrB.length); i++) {
8 | if (arrA[i] < arrB[i]) {
9 | return true;
10 | }
11 | if (arrA[i] > arrB[i]) {
12 | return false;
13 | }
14 | }
15 | } catch {
16 | return arrA.length < arrB.length;
17 | }
18 | return false;
19 | };
20 |
21 | export const equal = (a: string, b: string): boolean => {
22 | a = a.replace(/[^\d.]/g, '');
23 | b = b.replace(/[^\d.]/g, '');
24 | const arrA = a.split('.').map(value => Number(value));
25 | const arrB = b.split('.').map(value => Number(value));
26 | if (arrA.length !== arrB.length) {
27 | return false;
28 | }
29 | for (let i = 0; i < arrA.length; i++) {
30 | if (arrA[i] !== arrB[i]) {
31 | return false;
32 | }
33 | }
34 | return true;
35 | };
36 |
37 | export const newer = (a: string, b: string): boolean =>
38 | !older(a, b) && !equal(a, b);
39 |
--------------------------------------------------------------------------------
/src/utils/constants/languages.ts:
--------------------------------------------------------------------------------
1 | // references:
2 | // https://en.wikipedia.org/wiki/IETF_language_tag
3 | // https://en.wikipedia.org/wiki/List_of_language_names
4 |
5 | import { getString } from '@strings/translations';
6 |
7 | export const languagesMapping: Record = {
8 | 'id': 'Bahasa Indonesia',
9 | 'en': 'English',
10 | 'es': 'Español',
11 | 'fr': 'Français',
12 | 'pl': 'Polski',
13 | 'pt': 'Português',
14 | 'vi': 'Tiếng Việt',
15 | 'tr': 'Türkçe',
16 | 'ru': 'Русский',
17 | 'uk': 'Українська',
18 | 'ab': 'العربية',
19 | 'th': 'ไทย',
20 | 'zh': '中文, 汉语, 漢語',
21 | 'ja': '日本語',
22 | 'ko': '조선말, 한국어',
23 | 'multi': 'Multi',
24 | };
25 |
26 | export const languages = Object.values(languagesMapping);
27 |
28 | export const getLocaleLanguageName = (lang: string): string => {
29 | if (lang !== 'Multi') return lang;
30 | return getString('browseSettingsScreen.multi');
31 | };
32 |
--------------------------------------------------------------------------------
/src/utils/constants/readerConstants.ts:
--------------------------------------------------------------------------------
1 | import { ReaderTheme } from '@hooks/persisted/useSettings';
2 | import { MaterialDesignIconName } from '@type/icon';
3 |
4 | export const presetReaderThemes: ReaderTheme[] = [
5 | { backgroundColor: '#f5f5fa', textColor: '#111111' },
6 | { backgroundColor: '#F7DFC6', textColor: '#593100' },
7 | { backgroundColor: '#dce5e2', textColor: '#000000' },
8 | { backgroundColor: '#292832', textColor: '#CCCCCC' },
9 | {
10 | backgroundColor: '#000000',
11 | textColor: '#FFFFFFB3',
12 | },
13 | ];
14 |
15 | interface TextAlignments {
16 | value: string;
17 | icon: MaterialDesignIconName;
18 | }
19 |
20 | export const textAlignments: TextAlignments[] = [
21 | { value: 'left', icon: 'format-align-left' },
22 | { value: 'center', icon: 'format-align-center' },
23 | { value: 'justify', icon: 'format-align-justify' },
24 | { value: 'right', icon: 'format-align-right' },
25 | ];
26 |
27 | export interface Font {
28 | fontFamily: string;
29 | name: string;
30 | }
31 |
32 | export const readerFonts: Font[] = [
33 | { fontFamily: '', name: 'Original' },
34 | { fontFamily: 'lora', name: 'Lora' },
35 | { fontFamily: 'nunito', name: 'Nunito' },
36 | { fontFamily: 'noto-sans', name: 'Noto Sans' },
37 | { fontFamily: 'open-sans', name: 'Open Sans' },
38 | { fontFamily: 'arbutus-slab', name: 'Arbutus Slab' },
39 | { fontFamily: 'domine', name: 'Domine' },
40 | { fontFamily: 'lato', name: 'Lato' },
41 | { fontFamily: 'pt-serif', name: 'PT Serif' },
42 | { fontFamily: 'OpenDyslexic3-Regular', name: 'OpenDyslexic' },
43 | ];
44 |
--------------------------------------------------------------------------------
/src/utils/error.ts:
--------------------------------------------------------------------------------
1 | export function getErrorMessage(any: any) {
2 | return any?.message || String(any);
3 | }
4 |
--------------------------------------------------------------------------------
/src/utils/fetch/fetch.ts:
--------------------------------------------------------------------------------
1 | export const fetchTimeout = async (
2 | url: string,
3 | init?: any,
4 | timeout: number = 5000,
5 | ) => {
6 | const constroller = new AbortController();
7 | setTimeout(() => constroller.abort(), timeout);
8 | return fetch(url, {
9 | ...init,
10 | signal: constroller.signal,
11 | });
12 | };
13 |
--------------------------------------------------------------------------------
/src/utils/mmkv/mmkv.ts:
--------------------------------------------------------------------------------
1 | import { MMKV } from 'react-native-mmkv';
2 |
3 | export const MMKVStorage = new MMKV();
4 |
5 | export function getMMKVObject(key: string) {
6 | const data = MMKVStorage.getString(key);
7 | if (data) {
8 | return JSON.parse(data) as T;
9 | }
10 | return undefined;
11 | }
12 |
13 | export function setMMKVObject(key: string, obj: T) {
14 | MMKVStorage.set(key, JSON.stringify(obj));
15 | }
16 |
--------------------------------------------------------------------------------
/src/utils/showToast.ts:
--------------------------------------------------------------------------------
1 | import { ToastAndroid } from 'react-native';
2 |
3 | export const showToast = (message: string) =>
4 | ToastAndroid.show(message, ToastAndroid.SHORT);
5 |
--------------------------------------------------------------------------------
/src/utils/sleep.ts:
--------------------------------------------------------------------------------
1 | export const sleep = (time: number): Promise =>
2 | new Promise(resolve => setTimeout(() => resolve(), time));
3 |
--------------------------------------------------------------------------------
/src/utils/translateEnum.ts:
--------------------------------------------------------------------------------
1 | import { NovelStatus } from '@plugins/types';
2 | import { getString } from '@strings/translations';
3 |
4 | export const translateNovelStatus = (status?: NovelStatus | string) => {
5 | switch (status) {
6 | case NovelStatus.Ongoing:
7 | return getString('novelScreen.status.ongoing');
8 | case NovelStatus.OnHiatus:
9 | return getString('novelScreen.status.onHiatus');
10 | case NovelStatus.Completed:
11 | return getString('novelScreen.status.completed');
12 | case NovelStatus.Unknown:
13 | return getString('novelScreen.status.unknown');
14 | case NovelStatus.Cancelled:
15 | return getString('novelScreen.status.cancelled');
16 | case NovelStatus.Licensed:
17 | return getString('novelScreen.status.licensed');
18 | case NovelStatus.PublishingFinished:
19 | return getString('novelScreen.status.publishingFinished');
20 | default:
21 | return status ?? '';
22 | }
23 | };
24 |
--------------------------------------------------------------------------------
/src/utils/useLoadingColors.ts:
--------------------------------------------------------------------------------
1 | import { ThemeColors } from '@theme/types';
2 | import color from 'color';
3 | import { useAppSettings } from '@hooks/persisted';
4 | import { interpolateColor } from 'react-native-reanimated';
5 |
6 | const useLoadingColors = (theme: ThemeColors) => {
7 | const highlightColor = color(theme.primary).alpha(0.08).string();
8 | const backgroundColor = color(theme.surface);
9 |
10 | let adjustedBackgroundColor;
11 |
12 | if (backgroundColor.isDark()) {
13 | adjustedBackgroundColor =
14 | backgroundColor.luminosity() !== 0
15 | ? backgroundColor.lighten(0.1).toString()
16 | : backgroundColor.negate().darken(0.98).toString();
17 | } else {
18 | adjustedBackgroundColor = backgroundColor.darken(0.04).toString();
19 | }
20 |
21 | const { disableLoadingAnimations } = useAppSettings();
22 |
23 | if (disableLoadingAnimations) {
24 | //If loading animations is disabled highlight color is never shown so make background color more visible to compensate
25 | adjustedBackgroundColor = interpolateColor(
26 | 0.01, //I have no idea why the interpolation amount has to be so small, I think its cus of the massive difference in alpha
27 | [0, 1],
28 | [adjustedBackgroundColor, highlightColor],
29 | );
30 | }
31 |
32 | return [highlightColor, adjustedBackgroundColor];
33 | };
34 | export default useLoadingColors;
35 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "baseUrl": ".",
4 | "noUnusedLocals": true,
5 | "module": "ES2022",
6 | "paths": {
7 | "@components": [
8 | "src/components/index"
9 | ],
10 | "@components/*": [
11 | "src/components/*"
12 | ],
13 | "@database/*": [
14 | "src/database/*"
15 | ],
16 | "@hooks/*": [
17 | "src/hooks/*"
18 | ],
19 | "@hooks": [
20 | "src/hooks/index"
21 | ],
22 | "@screens/*": [
23 | "src/screens/*"
24 | ],
25 | "@strings/*": [
26 | "strings/*"
27 | ],
28 | "@theme/*": [
29 | "src/theme/*"
30 | ],
31 | "@utils/*": [
32 | "src/utils/*"
33 | ],
34 | "@plugins/*": [
35 | "src/plugins/*"
36 | ],
37 | "@services/*": [
38 | "src/services/*"
39 | ],
40 | "@navigators/*": [
41 | "src/navigators/*"
42 | ],
43 | "@native/*": [
44 | "src/native/*"
45 | ],
46 | "@api/*": [
47 | "src/api/*"
48 | ],
49 | "@type/*": [
50 | "src/type/*"
51 | ],
52 | "@specs/*": [
53 | "specs/*"
54 | ],
55 | },
56 | "types": ["react-native"]
57 | },
58 | "exclude": [
59 | "node_modules",
60 | "babel.config.js",
61 | "metro.config.js"
62 | ],
63 | "extends": "@react-native/typescript-config/tsconfig.json"
64 | }
--------------------------------------------------------------------------------