├── .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 | 39 | {title} 40 | 41 | 42 | {message} 43 | 44 | 45 | 46 | 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 | 39 | 40 | 41 | 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 | 34 | 42 | {getString('downloadScreen.removeDownloadsWarning')} 43 | 44 | 45 | 46 | 47 | 48 | 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 | 58 | 59 | 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 |