├── .github └── workflows │ ├── main.yml │ └── test.yml ├── .gitignore ├── IronDB ├── .gitignore ├── README.md ├── build.gradle └── src │ ├── main │ └── java │ │ └── cc │ │ └── aoeiuv020 │ │ └── irondb │ │ ├── Iron.kt │ │ ├── database.kt │ │ ├── delegate.kt │ │ ├── file.kt │ │ ├── impl │ │ ├── DatabaseImpl.kt │ │ ├── GsonSerializer.kt │ │ ├── KeyLocker.kt │ │ ├── KeysContainer.kt │ │ ├── Md5HaxSerializer.kt │ │ └── ReplaceFileSeparator.kt │ │ └── serializer.kt │ └── test │ └── java │ └── cc │ └── aoeiuv020 │ └── irondb │ ├── IronTest.kt │ └── impl │ ├── GsonSerializerTest.kt │ ├── Md5HaxSerializerTest.kt │ └── ReplaceFileSeparatorTest.kt ├── LICENSE ├── README.md ├── api ├── build.gradle └── src │ ├── main │ └── java │ │ └── cc │ │ └── aoeiuv020 │ │ └── panovel │ │ └── api │ │ ├── NoInternetException.kt │ │ ├── NovelContext.kt │ │ ├── base │ │ ├── DslJsoupNovelContext.kt │ │ ├── JsNovelContext.kt │ │ ├── JsoupNovelContext.kt │ │ └── OkHttpNovelContext.kt │ │ ├── data.kt │ │ ├── ext.kt │ │ └── site │ │ ├── aileleba.kt │ │ ├── biquge.kt │ │ ├── biquge5200.kt │ │ ├── biqugebook.kt │ │ ├── biqugese.kt │ │ ├── biqugezhh.kt │ │ ├── bqg5200.kt │ │ ├── byzw.kt │ │ ├── dajiadu.kt │ │ ├── dmzz.kt │ │ ├── exiaoshuo.kt │ │ ├── fenghuaju.kt │ │ ├── ggdown.kt │ │ ├── guanshuwang.kt │ │ ├── gulizw.kt │ │ ├── gxwztv.kt │ │ ├── haxds.kt │ │ ├── jdxs520.kt │ │ ├── kenshuzw.kt │ │ ├── kssw.kt │ │ ├── kuxiaoshuo.kt │ │ ├── lewen123.kt │ │ ├── liewen.kt │ │ ├── liudatxt.kt │ │ ├── lnovel.kt │ │ ├── lread.kt │ │ ├── manhuagui.kt │ │ ├── mianhuatang.kt │ │ ├── miaobige.kt │ │ ├── n123du.kt │ │ ├── n168kanshu.kt │ │ ├── n2kzw.kt │ │ ├── n360dxs.kt │ │ ├── n52ranwen.kt │ │ ├── n69shu.kt │ │ ├── n73xs.kt │ │ ├── n7dsw.kt │ │ ├── n9txs.kt │ │ ├── piaotian.kt │ │ ├── qidian.kt │ │ ├── qingkan.kt │ │ ├── qingkan5.kt │ │ ├── qinxiaoshuo.kt │ │ ├── qlwx.kt │ │ ├── sfacg.kt │ │ ├── shangshu.kt │ │ ├── shoudashu.kt │ │ ├── shu8.kt │ │ ├── sifang.kt │ │ ├── siluke.kt │ │ ├── snwx.kt │ │ ├── syxs.kt │ │ ├── ttkan.kt │ │ ├── uctxt.kt │ │ ├── wenxuemi.kt │ │ ├── wukong.kt │ │ ├── wukong0.kt │ │ ├── x23us.kt │ │ ├── yidm.kt │ │ ├── yipinxia.kt │ │ ├── yllxs.kt │ │ ├── ymoxuan.kt │ │ ├── yssm.kt │ │ ├── yunduwu.kt │ │ ├── zaidudu.kt │ │ ├── zhuaji.kt │ │ ├── zhuishu.kt │ │ └── zzdxsw.kt │ └── test │ └── java │ └── cc │ └── aoeiuv020 │ └── panovel │ └── api │ ├── JsoupNovelContextTest.kt │ ├── MakeJunitTest.kt │ ├── NovelContextTest.kt │ ├── OkHttpTest.kt │ ├── ProxyUtils.kt │ ├── RegexTest.kt │ └── site │ ├── AilelebaTest.kt │ ├── Biquge5200Test.kt │ ├── BiqugeTest.kt │ ├── BiqugebookTest.kt │ ├── BiqugeseTest.kt │ ├── BiqugezhhTest.kt │ ├── Bqg5200Test.kt │ ├── ByzwTest.kt │ ├── DajiaduTest.kt │ ├── DmzzTest.kt │ ├── ExiaoshuoTest.kt │ ├── FenghuajuTest.kt │ ├── GgdownTest.kt │ ├── GuanshuwangTest.kt │ ├── GulizwTest.kt │ ├── GxwztvTest.kt │ ├── HaxdsTest.kt │ ├── Jdxs520Test.kt │ ├── KenshuzwTest.kt │ ├── KsswTest.kt │ ├── KuxiaoshuoTest.kt │ ├── Lewen123Test.kt │ ├── LiewenTest.kt │ ├── LiudatxtTest.kt │ ├── LnovelTest.kt │ ├── LreadTest.kt │ ├── ManhuaguiTest.kt │ ├── MianhuatangTest.kt │ ├── MiaobigeTest.kt │ ├── N123duTest.kt │ ├── N168kanshuTest.kt │ ├── N2kzwTest.kt │ ├── N360dxsTest.kt │ ├── N52ranwenTest.kt │ ├── N69shuTest.kt │ ├── N73xsTest.kt │ ├── N7dswTest.kt │ ├── N9txsTest.kt │ ├── PiaotianTest.kt │ ├── QidianTest.kt │ ├── Qingkan5Test.kt │ ├── QingkanTest.kt │ ├── QinxiaoshuoTest.kt │ ├── QlwxTest.kt │ ├── SfacgTest.kt │ ├── ShangshuTest.kt │ ├── ShoudashuTest.kt │ ├── Shu8Test.kt │ ├── SiFangTest.kt │ ├── SilukeTest.kt │ ├── SnwxTest.kt │ ├── SyxsTest.kt │ ├── TtkanTest.kt │ ├── UctxtTest.kt │ ├── WenxuemiTest.kt │ ├── Wukong0Test.kt │ ├── WukongTest.kt │ ├── X23usTest.kt │ ├── YidmTest.kt │ ├── YipinxiaTest.kt │ ├── YllxsTest.kt │ ├── YmoxuanTest.kt │ ├── YssmTest.kt │ ├── YunduwuTest.kt │ ├── ZaiduduTest.kt │ ├── ZhuajiTest.kt │ ├── ZhuishuTest.kt │ ├── ZzdxswTest.kt │ └── base.kt ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro ├── schemas │ └── cc.aoeiuv020.panovel.data.db.AppDatabase │ │ ├── 1.json │ │ ├── 2.json │ │ ├── 3.json │ │ ├── 4.json │ │ ├── 5.json │ │ └── 6.json └── src │ ├── androidTest │ └── java │ │ └── cc │ │ └── aoeiuv020 │ │ └── panovel │ │ ├── ExampleInstrumentedTest.kt │ │ ├── RegexInstrumentedTest.kt │ │ ├── data │ │ └── db │ │ │ └── AppDatabaseTest.kt │ │ ├── local │ │ └── CheckTest.kt │ │ └── util │ │ └── DnsUtilsAndroidTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── assets │ │ ├── ChangeLog.txt │ │ ├── Disclaimer.txt │ │ ├── Donate.txt │ │ ├── Explain.txt │ │ └── Licenses.txt │ ├── ic_launcher-web.png │ ├── java │ │ └── cc │ │ │ └── aoeiuv020 │ │ │ └── panovel │ │ │ ├── App.kt │ │ │ ├── IView.kt │ │ │ ├── Presenter.kt │ │ │ ├── ad │ │ │ ├── AdConstants.kt │ │ │ ├── AdHelper.kt │ │ │ ├── AdListHelper.kt │ │ │ ├── EmptyAdListHelper.kt │ │ │ └── TestAdListHelper.kt │ │ │ ├── api │ │ │ └── ApiNovelProvider.kt │ │ │ ├── backup │ │ │ ├── BackupActivity.kt │ │ │ ├── BackupHelper.kt │ │ │ ├── BackupManager.kt │ │ │ ├── BackupOption.kt │ │ │ ├── BackupPresenter.kt │ │ │ ├── IBackup.kt │ │ │ ├── impl │ │ │ │ ├── BackupV1.kt │ │ │ │ ├── BackupV2.kt │ │ │ │ ├── BackupV3.kt │ │ │ │ ├── BackupV4.kt │ │ │ │ └── DefaultBackup.kt │ │ │ └── webdav │ │ │ │ ├── BackupWebDavConfigActivity.kt │ │ │ │ └── BackupWebDavHelper.kt │ │ │ ├── booklist │ │ │ ├── BookListActivity.kt │ │ │ ├── BookListActivityPresenter.kt │ │ │ ├── BookListFragment.kt │ │ │ ├── BookListFragmentAdapter.kt │ │ │ └── BookListFragmentPresenter.kt │ │ │ ├── bookshelf │ │ │ ├── BookshelfFragment.kt │ │ │ ├── BookshelfPresenter.kt │ │ │ └── RefreshingDotView.kt │ │ │ ├── data │ │ │ ├── ApiManager.kt │ │ │ ├── AppDatabaseManager.kt │ │ │ ├── CacheManager.kt │ │ │ ├── CookieManager.kt │ │ │ ├── DataManager.kt │ │ │ ├── DownloadManager.kt │ │ │ ├── LocalManager.kt │ │ │ ├── NovelManager.kt │ │ │ ├── NovelProvider.kt │ │ │ ├── ServerManager.kt │ │ │ ├── dao │ │ │ │ ├── BookListDao.kt │ │ │ │ ├── NovelDao.kt │ │ │ │ └── SiteDao.kt │ │ │ ├── db │ │ │ │ ├── AppDatabase.kt │ │ │ │ └── converter.kt │ │ │ └── entity │ │ │ │ ├── BookList.kt │ │ │ │ ├── BookListItem.kt │ │ │ │ ├── Novel.kt │ │ │ │ ├── NovelMinimal.kt │ │ │ │ ├── NovelWithProgress.kt │ │ │ │ ├── NovelWithProgressAndPinnedTime.kt │ │ │ │ └── Site.kt │ │ │ ├── detail │ │ │ ├── CheckableFloatingActionButton.java │ │ │ ├── NovelDetailActivity.kt │ │ │ ├── NovelDetailPresenter.kt │ │ │ └── ScrollAwareFABBehavior.java │ │ │ ├── donate │ │ │ ├── Donate.kt │ │ │ └── DonateActivity.kt │ │ │ ├── download │ │ │ ├── DownloadActivity.kt │ │ │ ├── DownloadNotificationManager.kt │ │ │ ├── DownloadPresenter.kt │ │ │ ├── DownloadProgressListener.kt │ │ │ └── DownloadingNotificationManager.kt │ │ │ ├── find │ │ │ ├── qidiantu │ │ │ │ ├── QidiantuActivity.kt │ │ │ │ ├── QidiantuPresenter.kt │ │ │ │ └── list │ │ │ │ │ ├── Item.kt │ │ │ │ │ ├── Post.kt │ │ │ │ │ ├── QidiantuListActivity.kt │ │ │ │ │ ├── QidiantuListAdapter.kt │ │ │ │ │ ├── QidiantuListPresenter.kt │ │ │ │ │ └── QidiantuPostAdapter.kt │ │ │ ├── shuju │ │ │ │ ├── QidianshujuActivity.kt │ │ │ │ ├── QidianshujuPresenter.kt │ │ │ │ ├── list │ │ │ │ │ ├── Item.kt │ │ │ │ │ ├── QidianshujuListActivity.kt │ │ │ │ │ ├── QidianshujuListAdapter.kt │ │ │ │ │ └── QidianshujuListPresenter.kt │ │ │ │ └── post │ │ │ │ │ ├── Post.kt │ │ │ │ │ ├── QidianshujuPostActivity.kt │ │ │ │ │ ├── QidianshujuPostAdapter.kt │ │ │ │ │ └── QidianshujuPostPresenter.kt │ │ │ └── sp7 │ │ │ │ ├── Sp7Activity.kt │ │ │ │ ├── Sp7Presenter.kt │ │ │ │ └── list │ │ │ │ ├── Item.kt │ │ │ │ ├── Sp7ListActivity.kt │ │ │ │ ├── Sp7ListAdapter.kt │ │ │ │ └── Sp7ListPresenter.kt │ │ │ ├── history │ │ │ ├── HistoryFragment.kt │ │ │ └── HistoryPresenter.kt │ │ │ ├── list │ │ │ ├── DefaultNovelItemActionListener.kt │ │ │ ├── NovelItemActionListener.kt │ │ │ ├── NovelListAdapter.kt │ │ │ └── NovelViewHolder.kt │ │ │ ├── local │ │ │ ├── ImportRequireValue.kt │ │ │ ├── LocalNovelProvider.kt │ │ │ └── NovelExporter.kt │ │ │ ├── main │ │ │ ├── MainActivity.kt │ │ │ ├── check.kt │ │ │ └── message.kt │ │ │ ├── migration │ │ │ ├── MigrateException.kt │ │ │ ├── Migration.kt │ │ │ ├── MigrationPresenter.kt │ │ │ ├── MigrationView.kt │ │ │ └── impl │ │ │ │ ├── AdSettingsMigration.kt │ │ │ │ ├── DataMigration.kt │ │ │ │ ├── DownloadMigration.kt │ │ │ │ ├── LoginMigration.kt │ │ │ │ └── SitesMigration.kt │ │ │ ├── open │ │ │ └── OpenManager.kt │ │ │ ├── qrcode │ │ │ └── QrCodeManager.kt │ │ │ ├── report │ │ │ └── Reporter.kt │ │ │ ├── search │ │ │ ├── FuzzySearchActivity.kt │ │ │ ├── FuzzySearchPresenter.kt │ │ │ ├── SingleSearchActivity.kt │ │ │ ├── SingleSearchPresenter.kt │ │ │ ├── SiteChooseActivity.kt │ │ │ ├── SiteChoosePresenter.kt │ │ │ ├── SiteListAdapter.kt │ │ │ ├── SiteSettingsActivity.kt │ │ │ └── SiteSettingsPresenter.kt │ │ │ ├── server │ │ │ ├── ServerManager.kt │ │ │ └── converter.kt │ │ │ ├── settings │ │ │ ├── AppCompatPreferenceActivity.kt │ │ │ ├── FilePickerPreference.kt │ │ │ ├── SelectableTextView.kt │ │ │ ├── SettingsActivity.kt │ │ │ ├── about.kt │ │ │ ├── ad.kt │ │ │ ├── backup.kt │ │ │ ├── clear.kt │ │ │ ├── disclaimer.kt │ │ │ ├── download.kt │ │ │ ├── fragment.kt │ │ │ ├── general.kt │ │ │ ├── interface.kt │ │ │ ├── list.kt │ │ │ ├── location.kt │ │ │ ├── margins.kt │ │ │ ├── other.kt │ │ │ ├── preference.kt │ │ │ ├── reader.kt │ │ │ ├── server.kt │ │ │ └── site.kt │ │ │ ├── share │ │ │ ├── Expiration.kt │ │ │ ├── PasteUbuntu.kt │ │ │ ├── Share.kt │ │ │ └── data.kt │ │ │ ├── text │ │ │ ├── CheckableImageView.java │ │ │ ├── DispatchTouchFrameLayout.kt │ │ │ ├── NovelContentsAdapter.kt │ │ │ ├── NovelTextActivity.kt │ │ │ ├── NovelTextBaseFullScreenActivity.kt │ │ │ ├── NovelTextNavigation.kt │ │ │ └── NovelTextPresenter.kt │ │ │ └── util │ │ │ ├── SignatureUtil.java │ │ │ ├── VersionUtil.kt │ │ │ ├── constants.kt │ │ │ ├── delegate.kt │ │ │ ├── dns.kt │ │ │ ├── ext.kt │ │ │ ├── file.kt │ │ │ ├── glide.kt │ │ │ ├── notification.kt │ │ │ ├── version.kt │ │ │ └── view.kt │ └── res │ │ ├── drawable-anydpi │ │ └── ic_jump_qidian.xml │ │ ├── drawable │ │ ├── background_circle.xml │ │ ├── circle_dot.xml │ │ ├── ic_add.xml │ │ ├── ic_block.xml │ │ ├── ic_close.xml │ │ ├── ic_color_lens.xml │ │ ├── ic_favorite.xml │ │ ├── ic_favorite_border.xml │ │ ├── ic_favorite_selector.xml │ │ ├── ic_info.xml │ │ ├── ic_jump_qidian_blocked.xml │ │ ├── ic_list.xml │ │ ├── ic_more_vert.xml │ │ ├── ic_open.xml │ │ ├── ic_open_in_browser.xml │ │ ├── ic_read.xml │ │ ├── ic_refresh.xml │ │ ├── ic_scan.xml │ │ ├── ic_search.xml │ │ ├── ic_settings.xml │ │ ├── novel_name_background_dark.xml │ │ └── splash_bg.xml │ │ ├── layout │ │ ├── activity_backup_web_dav_config.xml │ │ ├── activity_book_list.xml │ │ ├── activity_donate.xml │ │ ├── activity_download.xml │ │ ├── activity_export.xml │ │ ├── activity_fuzzy_search.xml │ │ ├── activity_main.xml │ │ ├── activity_novel_detail.xml │ │ ├── activity_novel_text.xml │ │ ├── activity_qidianshuju.xml │ │ ├── activity_qidianshuju_list.xml │ │ ├── activity_qidianshuju_post.xml │ │ ├── activity_qidiantu_list.xml │ │ ├── activity_single_search.xml │ │ ├── activity_site_choose.xml │ │ ├── activity_site_settings.xml │ │ ├── activity_sp7_list.xml │ │ ├── activity_splash.xml │ │ ├── book_list_item.xml │ │ ├── content_about.xml │ │ ├── content_disclaimer.xml │ │ ├── dialog_download_count.xml │ │ ├── dialog_editor.xml │ │ ├── dialog_seekbar.xml │ │ ├── dialog_select_color_scheme.xml │ │ ├── dialog_shared.xml │ │ ├── item_ad.xml │ │ ├── item_qidianshuju_list.xml │ │ ├── item_qidianshuju_post.xml │ │ ├── item_qidiantu_list.xml │ │ ├── item_qidiantu_post.xml │ │ ├── item_sp7_list.xml │ │ ├── item_test_ad.xml │ │ ├── novel_chapter_item.xml │ │ ├── novel_item_big.xml │ │ ├── novel_item_grid_big.xml │ │ ├── novel_item_grid_small.xml │ │ ├── novel_item_list.xml │ │ ├── novel_item_small.xml │ │ ├── novel_text_item.xml │ │ ├── novel_text_navigation.xml │ │ ├── novel_text_read_animation.xml │ │ ├── novel_text_read_default.xml │ │ ├── novel_text_read_margins.xml │ │ ├── novel_text_read_margins_item.xml │ │ ├── novel_text_read_settings.xml │ │ ├── novel_text_read_typesetting.xml │ │ ├── site_list_item.xml │ │ └── view_refreshing_dot.xml │ │ ├── menu │ │ ├── menu_book_list.xml │ │ ├── menu_detail.xml │ │ ├── menu_find.xml │ │ ├── menu_fuzzy_search.xml │ │ ├── menu_main.xml │ │ ├── menu_qidianshuju_list.xml │ │ ├── menu_qidianshuju_post.xml │ │ ├── menu_single_search.xml │ │ ├── menu_site_list.xml │ │ └── menu_text.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ ├── mipmap │ │ ├── donate_alipay.png │ │ ├── donate_alipay_red_packet.jpg │ │ ├── donate_paypal.png │ │ ├── donate_wechatpay.png │ │ ├── no_cover.jpg │ │ └── qrcode_wechatpay.jpg │ │ ├── values-v21 │ │ └── styles.xml │ │ ├── values-w820dp │ │ └── dimens.xml │ │ ├── values │ │ ├── array.xml │ │ ├── attrs.xml │ │ ├── colors.xml │ │ ├── dimens.xml │ │ ├── ic_launcher_background.xml │ │ ├── ids.xml │ │ ├── strings.xml │ │ └── styles.xml │ │ └── xml │ │ ├── backup_descriptor.xml │ │ ├── pref_ad.xml │ │ ├── pref_cache_clear.xml │ │ ├── pref_download.xml │ │ ├── pref_general.xml │ │ ├── pref_headers.xml │ │ ├── pref_interface.xml │ │ ├── pref_list.xml │ │ ├── pref_location.xml │ │ ├── pref_others.xml │ │ ├── pref_read.xml │ │ └── pref_server.xml │ └── test │ └── java │ └── cc │ └── aoeiuv020 │ └── panovel │ ├── ExampleUnitTest.kt │ ├── RegexUnitTest.kt │ ├── backup │ └── BackupManagerTest.kt │ ├── share │ └── PasteUbuntuTest.kt │ └── util │ ├── DnsUtilsTest.kt │ └── VersionNameTest.kt ├── baseJar ├── .gitignore ├── build.gradle └── src │ ├── main │ └── java │ │ └── cc │ │ └── aoeiuv020 │ │ └── base │ │ └── jar │ │ ├── date.kt │ │ ├── image.kt │ │ ├── io │ │ ├── BufferedRandomAccessFile.java │ │ └── io.kt │ │ ├── jsoup.kt │ │ ├── thread.kt │ │ └── url.kt │ └── test │ └── java │ └── cc │ └── aoeiuv020 │ └── base │ └── jar │ └── JsoupKtTest.kt ├── build.gradle ├── bump-version.sh ├── filepicker ├── .gitignore ├── LICENSE ├── build.gradle ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── cc │ │ └── aoeiuv020 │ │ └── filepicker │ │ ├── FilePickerDialog.java │ │ ├── FilePickerPreference.kt │ │ ├── controller │ │ ├── DialogSelectionListener.java │ │ ├── NotifyItemChecked.java │ │ └── adapters │ │ │ └── FileListAdapter.java │ │ ├── model │ │ ├── DialogConfigs.java │ │ ├── DialogProperties.java │ │ ├── FileListItem.java │ │ └── MarkedItemList.java │ │ ├── utils │ │ ├── ExtensionFilter.java │ │ └── Utility.java │ │ └── widget │ │ ├── MaterialCheckbox.java │ │ └── OnCheckedChangeListener.java │ └── res │ ├── anim │ ├── marked_item_animation.xml │ └── unmarked_item_animation.xml │ ├── drawable-hdpi │ └── ic_add.png │ ├── drawable-mdpi │ └── ic_add.png │ ├── drawable-xhdpi │ └── ic_add.png │ ├── drawable-xxhdpi │ └── ic_add.png │ ├── drawable-xxxhdpi │ └── ic_add.png │ ├── drawable │ └── bottom_shadow.9.png │ ├── layout-v11 │ └── dialog_footer.xml │ ├── layout-v21 │ └── dialog_file_list_item.xml │ ├── layout │ ├── dialog_file_list.xml │ ├── dialog_file_list_item.xml │ ├── dialog_footer.xml │ ├── dialog_header.xml │ ├── dialog_input_text.xml │ └── dialog_main.xml │ ├── mipmap-hdpi │ ├── ic_directory_parent.png │ ├── ic_type_file.png │ └── ic_type_folder.png │ ├── mipmap-mdpi │ ├── ic_directory_parent.png │ ├── ic_type_file.png │ └── ic_type_folder.png │ ├── mipmap-xhdpi │ ├── ic_directory_parent.png │ ├── ic_type_file.png │ └── ic_type_folder.png │ ├── mipmap-xxhdpi │ ├── ic_directory_parent.png │ ├── ic_type_file.png │ └── ic_type_folder.png │ ├── values-de │ └── strings.xml │ ├── values-es │ └── strings.xml │ ├── values-fr │ └── strings.xml │ ├── values-ru │ └── strings.xml │ ├── values-w820dp │ └── dimens.xml │ ├── values-zh-rCN │ └── strings.xml │ ├── values-zh-rHK │ └── strings.xml │ ├── values-zh-rTW │ └── strings.xml │ └── values │ ├── attrs.xml │ ├── colors.xml │ ├── dimens.xml │ └── strings.xml ├── gradle.properties ├── gradle ├── signing.gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── js ├── .gitignore ├── build.gradle └── src │ ├── main │ └── java │ │ └── cc │ │ └── aoeiuv020 │ │ └── js │ │ ├── JsContext.kt │ │ └── JsUtil.kt │ └── test │ └── java │ └── cc │ └── aoeiuv020 │ └── js │ └── JsUtilTest.kt ├── latest-changelog.sh ├── latest-version.sh ├── local ├── .gitignore ├── build.gradle └── src │ ├── main │ └── java │ │ └── cc │ │ └── aoeiuv020 │ │ └── panovel │ │ └── local │ │ ├── ContentProvider.kt │ │ ├── EpubExpoter.kt │ │ ├── EpubParser.kt │ │ ├── FileCharsetDetector.java │ │ ├── LocalNovelExpoter.kt │ │ ├── LocalNovelParser.kt │ │ ├── Previewer.kt │ │ ├── TextExporter.kt │ │ ├── TextParser.kt │ │ └── data.kt │ └── test │ ├── java │ └── cc │ │ └── aoeiuv020 │ │ └── panovel │ │ └── local │ │ ├── EpubExpoterTest.kt │ │ ├── EpubParserTest.kt │ │ ├── ParserTest.kt │ │ └── TextParserTest.kt │ └── resources │ ├── panovel-old.txt │ ├── panovel.txt │ └── zxcs.txt ├── merge-version.sh ├── pager ├── build.gradle ├── proguard-rules.pro └── src │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── cc │ │ │ └── aoeiuv020 │ │ │ └── pager │ │ │ ├── AnimMode.kt │ │ │ ├── BlankPagerDrawer.kt │ │ │ ├── IPagerDrawer.kt │ │ │ ├── Pager.kt │ │ │ ├── PagerAnimation.kt │ │ │ ├── PagerDirection.kt │ │ │ ├── PagerDrawer.kt │ │ │ ├── Size.kt │ │ │ ├── animation │ │ │ ├── AnimationConfig.kt │ │ │ ├── CoverPageAnim.java │ │ │ ├── HorizonPageAnim.java │ │ │ ├── NonePageAnim.java │ │ │ ├── PageAnimation.java │ │ │ ├── ScrollPageAnim.java │ │ │ ├── SimulationPageAnim.java │ │ │ └── SlidePageAnim.java │ │ │ └── margins.kt │ └── res │ │ └── values │ │ └── strings.xml │ └── test │ └── java │ └── cc │ └── aoeiuv020 │ └── pager │ └── ExampleUnitTest.java ├── publish.properties.example ├── reader ├── build.gradle ├── proguard-rules.pro └── src │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── cc │ │ │ └── aoeiuv020 │ │ │ └── reader │ │ │ ├── AnimationMode.kt │ │ │ ├── ConfigChangedListener.kt │ │ │ ├── INovelReader.kt │ │ │ ├── ReaderConfig.kt │ │ │ ├── ReaderConfigName.kt │ │ │ ├── Readers.kt │ │ │ ├── complex │ │ │ ├── ComplexReader.kt │ │ │ ├── Page.kt │ │ │ ├── ReaderDrawer.kt │ │ │ └── type.kt │ │ │ ├── data.kt │ │ │ ├── ext.kt │ │ │ ├── listener.kt │ │ │ ├── margins.kt │ │ │ └── simple │ │ │ ├── DispatchTouchFrameLayout.kt │ │ │ ├── ImageViewHolder.kt │ │ │ ├── NovelTextPagerAdapter.kt │ │ │ ├── PageHolder.kt │ │ │ ├── PageRecyclerAdapter.kt │ │ │ ├── ResizableImageView.kt │ │ │ ├── SimpleReader.kt │ │ │ └── TextViewHolder.kt │ └── res │ │ ├── layout │ │ ├── simple.xml │ │ ├── simple_image_item.xml │ │ ├── simple_text_item.xml │ │ └── simple_view_pager_item.xml │ │ └── values │ │ └── strings.xml │ └── test │ └── java │ └── cc │ └── aoeiuv020 │ └── reader │ └── ExampleUnitTest.java ├── refresher ├── .gitignore ├── build.gradle └── src │ └── main │ └── java │ └── cc │ └── aoeiuv020 │ └── panovel │ ├── api │ └── context.kt │ ├── refresher │ ├── Application.kt │ ├── Config.kt │ ├── Refresher.kt │ └── data.kt │ └── share │ └── PasteUbuntu.kt ├── screenshots ├── bookshelf.jpg ├── detail.jpg ├── epub.jpg ├── genre.jpg ├── list.jpg └── text.jpg ├── server ├── .gitignore ├── build.gradle └── src │ ├── main │ └── java │ │ └── cc │ │ └── aoeiuv020 │ │ └── panovel │ │ └── server │ │ ├── ServerAddress.kt │ │ ├── common │ │ ├── ErrorCode.kt │ │ ├── json.kt │ │ ├── md5.kt │ │ └── novel.kt │ │ ├── dal │ │ └── model │ │ │ ├── Config.kt │ │ │ ├── Message.kt │ │ │ ├── MobRequest.kt │ │ │ ├── MobResponse.kt │ │ │ ├── QueryResponse.kt │ │ │ └── autogen │ │ │ └── Novel.java │ │ └── service │ │ ├── NovelService.kt │ │ └── impl │ │ └── NovelServiceImpl.kt │ └── test │ └── kotlin │ └── cc │ └── aoeiuv020 │ └── panovel │ └── server │ └── service │ └── impl │ └── NovelServiceImplTest.kt ├── settings.gradle ├── signing.txt ├── template-update.md └── version.properties /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | env: 3 | TZ: Asia/Shanghai 4 | 5 | on: 6 | schedule: 7 | - cron: '0 20 * * fri' # 时区对cron无效,所以要提前8个小时, 8 | workflow_dispatch: 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v2 15 | with: 16 | ref: dev 17 | - name: Test with Gradle 18 | run: | 19 | ./gradlew api:compileTestJava 20 | ./gradlew api:test --tests "**.site.*" || echo test return $? 21 | - name: Deploy 22 | uses: peaceiris/actions-gh-pages@v3 23 | with: 24 | github_token: ${{ secrets.GITHUB_TOKEN }} 25 | force_orphan: true 26 | publish_dir: ./api/build/reports/tests/test/ 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### Android template 2 | # Built application files 3 | *.apk 4 | *.ap_ 5 | 6 | # Files for the ART/Dalvik VM 7 | *.dex 8 | 9 | # Java class files 10 | *.class 11 | 12 | # Generated files 13 | bin/ 14 | gen/ 15 | out/ 16 | 17 | # Gradle files 18 | .gradle/ 19 | build/ 20 | 21 | # Local configuration file (sdk path, etc) 22 | local.properties 23 | 24 | # Proguard folder generated by Eclipse 25 | proguard/ 26 | 27 | # Log Files 28 | *.log 29 | 30 | # Android Studio Navigation editor temp files 31 | .navigation/ 32 | 33 | # Android Studio captures folder 34 | captures/ 35 | 36 | # Intellij 37 | *.iml 38 | .idea/ 39 | 40 | # Keystore files 41 | *.jks 42 | 43 | # External native build folder generated in Android Studio 2.2 and later 44 | .externalNativeBuild 45 | 46 | # Google Services (e.g. APIs or Firebase) 47 | google-services.json 48 | 49 | # Freeline 50 | freeline.py 51 | freeline/ 52 | freeline_project_description.json 53 | 54 | ### 55 | /release 56 | /signing.properties 57 | /publish.properties 58 | class_files.txt 59 | .attach_pid* -------------------------------------------------------------------------------- /IronDB/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /IronDB/README.md: -------------------------------------------------------------------------------- 1 | # IronDB: NO-SQL Database 2 | 3 | File base simple key-value storage for kotlin, 4 | serialize by gson, 5 | 6 | It is suitable for saving large data class objects infrequently. 7 | 适用于不频繁的保存较大的数据类对象, 8 | 9 | 1. Each value save as a file, 10 | 2. Provide sub method imitate get the table from database, 11 | 3. Multithreading, lock key, -------------------------------------------------------------------------------- /IronDB/build.gradle: -------------------------------------------------------------------------------- 1 | dependencies { 2 | implementation project(':baseJar') 3 | } 4 | -------------------------------------------------------------------------------- /IronDB/src/main/java/cc/aoeiuv020/irondb/Iron.kt: -------------------------------------------------------------------------------- 1 | package cc.aoeiuv020.irondb 2 | 3 | import cc.aoeiuv020.irondb.impl.DatabaseImpl 4 | import cc.aoeiuv020.irondb.impl.GsonSerializer 5 | import cc.aoeiuv020.irondb.impl.ReplaceFileSeparator 6 | import java.io.File 7 | 8 | /** 9 | * Created by AoEiuV020 on 2018.05.27-14:42:39. 10 | */ 11 | object Iron { 12 | fun db( 13 | base: File, 14 | // 默认使用gson进行序列化, 15 | dataSerializer: DataSerializer = GsonSerializer(), 16 | // 默认简单替换文件分隔符, 17 | keySerializer: KeySerializer = ReplaceFileSeparator(), 18 | subSerializer: KeySerializer = keySerializer 19 | ): Database = DatabaseImpl( 20 | base = base, 21 | subSerializer = subSerializer, 22 | keySerializer = keySerializer, 23 | dataSerializer = dataSerializer 24 | ) 25 | } -------------------------------------------------------------------------------- /IronDB/src/main/java/cc/aoeiuv020/irondb/database.kt: -------------------------------------------------------------------------------- 1 | package cc.aoeiuv020.irondb 2 | 3 | import cc.aoeiuv020.gson.type 4 | import java.lang.reflect.Type 5 | 6 | /** 7 | * Created by AoEiuV020 on 2018.05.27-14:43:23. 8 | */ 9 | interface Database { 10 | fun sub(table: String): Database 11 | /** 12 | * @param value 为空则删除对应文件, 13 | */ 14 | fun write(key: String, value: T?, type: Type) 15 | 16 | /** 17 | * @return key不存在则返回null, 18 | */ 19 | fun read(key: String, type: Type): T? 20 | 21 | /** 22 | * 封装文件的使用,确保线程安全, 23 | */ 24 | fun file(key: String): FileWrapper 25 | 26 | fun drop() 27 | 28 | /** 29 | * @return 返回用于判断指定key是否存在的集合,不可用于读出key, 30 | */ 31 | fun keysContainer(): Collection 32 | 33 | } 34 | 35 | /** 36 | * 通过gson的TypeToken得到T的真实类型, 37 | * 这里不是必须的, 38 | */ 39 | @Suppress("unused") 40 | inline fun Database.write(key: String, value: T) = write(key, value, type()) 41 | 42 | /** 43 | * 通过gson的TypeToken得到T的真实类型, 44 | */ 45 | @Suppress("unused") 46 | inline fun Database.read(key: String): T? = read(key, type()) 47 | -------------------------------------------------------------------------------- /IronDB/src/main/java/cc/aoeiuv020/irondb/file.kt: -------------------------------------------------------------------------------- 1 | package cc.aoeiuv020.irondb 2 | 3 | import java.io.File 4 | 5 | /** 6 | * Created by AoEiuV020 on 2018.06.13-15:55:42. 7 | */ 8 | interface FileWrapper { 9 | fun use(block: (File) -> T): T 10 | fun delete(): Boolean 11 | } -------------------------------------------------------------------------------- /IronDB/src/main/java/cc/aoeiuv020/irondb/impl/GsonSerializer.kt: -------------------------------------------------------------------------------- 1 | package cc.aoeiuv020.irondb.impl 2 | 3 | import cc.aoeiuv020.gson.GsonUtils 4 | import cc.aoeiuv020.irondb.DataSerializer 5 | import com.google.gson.Gson 6 | import java.lang.reflect.Type 7 | 8 | /** 9 | * Created by AoEiuV020 on 2018.05.27-15:51:22. 10 | */ 11 | class GsonSerializer( 12 | private val gson: Gson = GsonUtils.gson 13 | ) : DataSerializer { 14 | override fun serialize(value: T, type: Type): String = 15 | gson.toJson(value, type) 16 | 17 | override fun deserialize(string: String, type: Type): T = 18 | gson.fromJson(string, type) 19 | } -------------------------------------------------------------------------------- /IronDB/src/main/java/cc/aoeiuv020/irondb/impl/KeyLocker.kt: -------------------------------------------------------------------------------- 1 | package cc.aoeiuv020.irondb.impl 2 | 3 | import java.util.concurrent.ConcurrentHashMap 4 | import java.util.concurrent.Semaphore 5 | 6 | /** 7 | * Created by AoEiuV020 on 2018.05.27-15:18:53. 8 | */ 9 | @Suppress("MemberVisibilityCanBePrivate") 10 | class KeyLocker { 11 | // 没有删除不再被使用的entry, 可能浪费内存, 12 | private val semaphoreMap = ConcurrentHashMap() 13 | 14 | fun acquire(key: String) { 15 | val semaphore = semaphoreMap.getOrPut(key) { Semaphore(1, true) } 16 | semaphore.acquireUninterruptibly() 17 | } 18 | 19 | fun release(key: String) { 20 | // key不存在就什么都不做, 21 | semaphoreMap[key]?.release() 22 | } 23 | 24 | fun runInAcquire(key: String, block: () -> T): T = try { 25 | acquire(key) 26 | block() 27 | } finally { 28 | // 放在finally以防万一io异常时也要释放锁, 29 | release(key) 30 | } 31 | } -------------------------------------------------------------------------------- /IronDB/src/main/java/cc/aoeiuv020/irondb/impl/KeysContainer.kt: -------------------------------------------------------------------------------- 1 | package cc.aoeiuv020.irondb.impl 2 | 3 | import cc.aoeiuv020.irondb.KeySerializer 4 | import java.io.File 5 | 6 | /** 7 | * 只用于判断元素是否存在集合中, 8 | * 不可用于读出元素, 9 | * 10 | * Created by AoEiuV020 on 2018.05.27-15:14:05. 11 | */ 12 | class KeysContainer( 13 | base: File, 14 | private val keySerializer: KeySerializer 15 | ) : Collection { 16 | // 文件名列表,包括目录,也就是Database.sub方法产生的, 17 | private val nameSet = base.list()?.toSet() ?: emptySet() 18 | 19 | override val size: Int = nameSet.size 20 | 21 | override fun contains(element: String): Boolean = 22 | nameSet.contains(keySerializer.serialize(element)) 23 | 24 | override fun containsAll(elements: Collection): Boolean { 25 | for (element in elements) { 26 | if (!contains(element)) { 27 | return false 28 | } 29 | } 30 | return true 31 | } 32 | 33 | override fun isEmpty(): Boolean = nameSet.isEmpty() 34 | 35 | // 不打算支持, 36 | override fun iterator(): Iterator = throw UnsupportedOperationException() 37 | } 38 | -------------------------------------------------------------------------------- /IronDB/src/main/java/cc/aoeiuv020/irondb/impl/Md5HaxSerializer.kt: -------------------------------------------------------------------------------- 1 | package cc.aoeiuv020.irondb.impl 2 | 3 | import cc.aoeiuv020.encrypt.hex 4 | import cc.aoeiuv020.encrypt.md5 5 | import cc.aoeiuv020.irondb.KeySerializer 6 | 7 | /** 8 | * 慎用,多级使用md5进行序列化可能导致路径过长, 9 | * 序列化成utf-8转md5转16进制的小写, 10 | * 11 | * Created by AoEiuV020 on 2018.05.27-16:17:39. 12 | */ 13 | class Md5HaxSerializer : KeySerializer { 14 | override fun serialize(from: String): String { 15 | return from.md5().hex() 16 | } 17 | } -------------------------------------------------------------------------------- /IronDB/src/main/java/cc/aoeiuv020/irondb/impl/ReplaceFileSeparator.kt: -------------------------------------------------------------------------------- 1 | package cc.aoeiuv020.irondb.impl 2 | 3 | import cc.aoeiuv020.irondb.KeySerializer 4 | import cc.aoeiuv020.regex.compileRegex 5 | 6 | /** 7 | * 简单替换路径分隔符,因此若是名字仅这一处不同,将产生冲突, 8 | * 9 | * Created by AoEiuV020 on 2018.05.27-16:03:39. 10 | */ 11 | class ReplaceFileSeparator( 12 | private val replaceWith: String = "." 13 | ) : KeySerializer { 14 | companion object { 15 | /** 16 | * fat32不支持这些字符,而sdcard基本上是fat32文件系统, 17 | */ 18 | val NOT_SUPPORT_CHARACTER = compileRegex("[/\\:|=?\";\\[\\],^]") 19 | } 20 | override fun serialize(from: String): String { 21 | return from.replace(compileRegex("[/\\:|=?\";\\[\\],^]"), replaceWith) 22 | } 23 | } -------------------------------------------------------------------------------- /IronDB/src/main/java/cc/aoeiuv020/irondb/serializer.kt: -------------------------------------------------------------------------------- 1 | package cc.aoeiuv020.irondb 2 | 3 | import java.io.File 4 | import java.lang.reflect.Type 5 | 6 | /** 7 | * 用于将key序列化成合法的文件路径, 8 | * 重点是不能有路径分隔符[File.separatorChar], 9 | * 只需要序列化,不需要反序列化,因此可以使用md5之类的, 10 | * 11 | * Created by AoEiuV020 on 2018.05.27-14:55:59. 12 | */ 13 | interface KeySerializer { 14 | fun serialize(from: String): String 15 | } 16 | 17 | /** 18 | * 用于对象序列化, 19 | */ 20 | interface DataSerializer { 21 | fun serialize(value: T, type: Type): String 22 | fun deserialize(string: String, type: Type): T 23 | } -------------------------------------------------------------------------------- /IronDB/src/test/java/cc/aoeiuv020/irondb/impl/Md5HaxSerializerTest.kt: -------------------------------------------------------------------------------- 1 | package cc.aoeiuv020.irondb.impl 2 | 3 | import org.junit.Assert.assertEquals 4 | import org.junit.Test 5 | 6 | /** 7 | * Created by AoEiuV020 on 2018.05.27-16:44:05. 8 | */ 9 | class Md5HaxSerializerTest { 10 | private val serializer = Md5HaxSerializer() 11 | 12 | @Test 13 | fun serialize() { 14 | assertEquals("d9685229a4d0622da3f75e77bfa56d5a", serializer.serialize("AoEiuV020")) 15 | assertEquals("e10adc3949ba59abbe56e057f20f883e", serializer.serialize("123456")) 16 | assertEquals("a7bac2239fcdcb3a067903d8077c4a07", serializer.serialize("中文")) 17 | assertEquals("2249f181c4e28e3b844e5ab57d5dd4b8", serializer.serialize("啊o额iu鱼")) 18 | assertEquals("206d81c8660a4e7d7b81f175acbdcc8b", serializer.serialize("123/456")) 19 | assertEquals("09ca1819e6bb545e495dc35feaf40ccf", serializer.serialize(" \t\n")) 20 | assertEquals("fcbc5d773d0351226348e96536c24448", serializer.serialize("""`~!@#${'$'}%^&*()-_=+[]{}\|;:'"/?.>,<""")) 21 | } 22 | } -------------------------------------------------------------------------------- /IronDB/src/test/java/cc/aoeiuv020/irondb/impl/ReplaceFileSeparatorTest.kt: -------------------------------------------------------------------------------- 1 | package cc.aoeiuv020.irondb.impl 2 | 3 | import org.junit.Assert.assertEquals 4 | import org.junit.Test 5 | 6 | /** 7 | * Created by AoEiuV020 on 2018.05.27-16:55:57. 8 | */ 9 | class ReplaceFileSeparatorTest { 10 | private val serializer = ReplaceFileSeparator() 11 | 12 | @Test 13 | fun serialize() { 14 | assertEquals("AoEiuV020", serializer.serialize("AoEiuV020")) 15 | assertEquals("123456", serializer.serialize("123456")) 16 | assertEquals("中文", serializer.serialize("中文")) 17 | assertEquals("啊o额iu鱼", serializer.serialize("啊o额iu鱼")) 18 | assertEquals("123.456", serializer.serialize("123/456")) 19 | assertEquals("123.456", serializer.serialize("123|456")) 20 | assertEquals(" \t\n", serializer.serialize(" \t\n")) 21 | assertEquals("""`~!@#${'$'}%.&*()-_.+..{}\...'....>.<""", serializer.serialize("""`~!@#${'$'}%^&*()-_=+[]{}\|;:'"/?.>,<""")) 22 | } 23 | 24 | @Test 25 | fun char() { 26 | println(ReplaceFileSeparator.NOT_SUPPORT_CHARACTER) 27 | } 28 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 爬小说 2 | 我们不生产小说,我们只做网站的搬运工, 3 | [![img](https://img.shields.io/github/release/AoEiuV020/PaNovel.svg)](https://github.com/AoEiuV020/PaNovel/releases) 4 | [![CI](https://github.com/AoEiuV020/PaNovel/workflows/CI/badge.svg)](https://github.com/AoEiuV020/PaNovel/actions) 5 | [![Chat](https://img.shields.io/badge/Telegram-Chat-blue.svg?logo=telegram)](https://t.me/PaNovelGroup) 6 | 7 | ## 应用简介 8 | 这是有理想的小说神器,目前支持<35>个网站, 9 | 支持阅读本地.txt以及.epub小说, 10 | 11 | 【库】 12 | kotlin + mvp + room 13 | [jsoup](https://github.com/jhy/jsoup) 14 | [glide](https://github.com/bumptech/glide) 15 | [anko](https://github.com/Kotlin/anko) 16 | [MaterialSearchView](https://github.com/MiguelCatalan/MaterialSearchView) 17 | [slf4j](https://github.com/qos-ch/slf4j) 18 | [gson](https://github.com/google/gson) 19 | [ColorPicker](https://github.com/QuadFlask/colorpicker) 20 | [MagicIndicator](https://github.com/hackware1993/MagicIndicator) 21 | [NovelReader](https://github.com/newbiechen1024/NovelReader) 22 | [Bugly](https://github.com/BuglyDevTeam/Bugly-Android) 23 | [Zip4j](https://mvnrepository.com/artifact/net.lingala.zip4j/zip4j) 24 | [AspectRatioImageView](https://github.com/santalu/aspect-ratio-imageview) 25 | [jchardet](http://jchardet.sourceforge.net/index.html) 26 | 27 | ![img](screenshots/text.jpg) 28 | ![img](screenshots/epub.jpg) 29 | ![img](screenshots/bookshelf.jpg) 30 | -------------------------------------------------------------------------------- /api/build.gradle: -------------------------------------------------------------------------------- 1 | dependencies { 2 | implementation 'org.jsoup:jsoup:' + jsoup_version 3 | implementation project(':baseJar') 4 | implementation project(':js') 5 | } 6 | -------------------------------------------------------------------------------- /api/src/main/java/cc/aoeiuv020/panovel/api/NoInternetException.kt: -------------------------------------------------------------------------------- 1 | package cc.aoeiuv020.panovel.api 2 | 3 | /** 4 | * 归到断网的异常,不上报, 5 | * 6 | * Created by AoEiuV020 on 2018.06.03-08:38:46. 7 | */ 8 | class NoInternetException(cause: Throwable) 9 | : RuntimeException("没有连接网络,", cause) -------------------------------------------------------------------------------- /api/src/main/java/cc/aoeiuv020/panovel/api/base/JsNovelContext.kt: -------------------------------------------------------------------------------- 1 | package cc.aoeiuv020.panovel.api.base 2 | 3 | import cc.aoeiuv020.js.JsContext 4 | import cc.aoeiuv020.js.JsUtil 5 | import org.jsoup.nodes.Element 6 | import java.util.concurrent.ExecutorService 7 | import java.util.concurrent.Executors 8 | import java.util.concurrent.Future 9 | 10 | /** 11 | * Created by AoEiuV020 on 2019.02.11-18:27:45. 12 | */ 13 | abstract class JsNovelContext : DslJsoupNovelContext() { 14 | // js线程,一个js上下文只能一个线程使用,其他线程需要时都等待这个线程执行, 15 | private val jsExecutor: ExecutorService = Executors.newSingleThreadExecutor() 16 | private val jsContext: JsContext = get { JsUtil.create() } 17 | 18 | @Suppress("UNCHECKED_CAST") 19 | private fun submit(block: () -> T): Future { 20 | return jsExecutor.submit(block) as Future 21 | } 22 | 23 | private fun get(block: () -> T): T { 24 | return submit(block).get() 25 | } 26 | 27 | protected fun js(string: String): String = get { 28 | jsContext.run(string) 29 | } 30 | 31 | protected fun Element.script(query: String) = requireElement(query, TAG_SCRIPT).data() 32 | } -------------------------------------------------------------------------------- /api/src/test/java/cc/aoeiuv020/panovel/api/NovelContextTest.kt: -------------------------------------------------------------------------------- 1 | package cc.aoeiuv020.panovel.api 2 | 3 | import org.junit.Test 4 | 5 | /** 6 | * Created by AoEiuV020 on 2018.12.31-20:22:56. 7 | */ 8 | class NovelContextTest { 9 | @Test 10 | fun count() { 11 | println(NovelContext.getAllSite().count { !it.hide }) 12 | } 13 | } -------------------------------------------------------------------------------- /api/src/test/java/cc/aoeiuv020/panovel/api/ProxyUtils.kt: -------------------------------------------------------------------------------- 1 | package cc.aoeiuv020.panovel.api 2 | 3 | /** 4 | * Created by AoEiuV020 on 2021.10.13-23:39:51. 5 | */ 6 | object ProxyUtils { 7 | @Suppress("PrivatePropertyName") 8 | private val PROXY_ENABLED = false 9 | fun proxy() { 10 | if (PROXY_ENABLED) { 11 | System.setProperty("socksProxyHost", "localhost") 12 | System.setProperty("socksProxyPort", "1081") 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /api/src/test/java/cc/aoeiuv020/panovel/api/RegexTest.kt: -------------------------------------------------------------------------------- 1 | package cc.aoeiuv020.panovel.api 2 | 3 | import org.junit.Assert.assertEquals 4 | import org.junit.Test 5 | 6 | /** 7 | * Created by AoEiuV020 on 2018.06.03-12:29:01. 8 | */ 9 | class RegexTest { 10 | @Test 11 | fun group() { 12 | "https://www.gxwztv.com/55/55886/16079406.html" 13 | .replace(Regex("/(\\d+)/(\\d+)/(\\d+)"), "\\1,\\2,\\3") 14 | .let { 15 | assertEquals("https://www.gxwztv.com1,2,3.html", it) 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /api/src/test/java/cc/aoeiuv020/panovel/api/site/BiqugeTest.kt: -------------------------------------------------------------------------------- 1 | package cc.aoeiuv020.panovel.api.site 2 | 3 | import org.junit.Test 4 | 5 | /** 6 | * 7 | * Created by AoEiuV020 on 2017.10.08-21:52:04. 8 | */ 9 | class BiqugeTest : BaseNovelContextText(Biquge::class) { 10 | @Test 11 | fun search() { 12 | search("都市") 13 | search("最大权限", "肥鱼马甲", "18156") 14 | search("都市酒仙系统", "酒剑仙人", "19679") 15 | } 16 | 17 | @Test 18 | fun detail() { 19 | detail( 20 | "18156", "18156", "最大权限", "肥鱼马甲", 21 | "https://www.biqugee.com/cover/18/18156/18156s.jpg", 22 | "制作游戏成功的林陨意外猝死,穿越到自己制作的游戏世界,结果拥有了这个世界的最大权限!\n" + 23 | "最大权限书友群:305908807", 24 | "2018-04-26 21:42:32" 25 | ) 26 | } 27 | 28 | @Test 29 | fun chapters() { 30 | chapters("18156", "序章", "18156/7491883", null, 31 | "第505章 格斗", "18156/9637191", "2018-04-25 21:42:32", 32 | 515) 33 | chapters("18156", "序章", "18156/7491883", null, 34 | "关于本书和新书的一些事情", "18156/10188197", "2018-04-26 21:42:32", 35 | 516) 36 | } 37 | 38 | @Test 39 | fun content() { 40 | content("18156/8791124", 41 | "中年男子下意识的接住魂晶,这东西能够让卡片使更好的修炼魂力,可以说是硬通货,属于最高等的金钱。", 42 | "“没什么,绿色级别吗,这不是问题。”林陨摇了摇头,接着拿出一张卡片,直接激发,同时林陨的气息也迅速增强,瞬间魂力翻倍,直接突破一千,迈过绿色级别的门槛。", 43 | 38) 44 | } 45 | } -------------------------------------------------------------------------------- /api/src/test/java/cc/aoeiuv020/panovel/api/site/ByzwTest.kt: -------------------------------------------------------------------------------- 1 | package cc.aoeiuv020.panovel.api.site 2 | 3 | import org.junit.Test 4 | 5 | /** 6 | * Created by AoEiuV020 on 2018.06.02-19:48:12. 7 | */ 8 | class ByzwTest : BaseNovelContextText(Byzw::class) { 9 | @Test 10 | fun search() { 11 | search("都市") 12 | search("道君", "跃千愁", "28675") 13 | search("蛊真人", "蛊真人", "7900") 14 | } 15 | 16 | @Test 17 | fun detail() { 18 | detail("7900", "7900", "蛊真人", "蛊真人", 19 | "https://www.81book.com/files/article/image/7/7900/7900s.jpg", 20 | "人是万物之灵,蛊是天地真精。\n" + 21 | "三观不正,魔头重生。\n" + 22 | "昔日旧梦,同名新作。\n" + 23 | "一个穿越者不断重生的故事。\n" + 24 | "一个养蛊、炼蛊、用蛊的奇特世界。\n" + 25 | "春秋蝉、月光蛊、酒虫、一气金光虫、青丝蛊、希望蛊……\n" + 26 | "还有一个纵情纵横的绝世大魔头!", 27 | "2018-06-02 19:01:52") 28 | } 29 | 30 | @Test 31 | fun chapters() { 32 | chapters("7900", "序:不是走向成功,就是走向毁灭", "7900/234025", null, 33 | "今天无更", "7900/13073827", "2018-06-02 19:01:52", 34 | 2038) 35 | } 36 | 37 | @Test 38 | fun content() { 39 | content("7900/234025", 40 | "在过去的几个月内,我经历了生活赐予的痛苦,并且身心都在其中挣扎。", 41 | "看更新最快的武动乾坤最新章节Www.81zw.Com", 42 | 60) 43 | } 44 | 45 | } -------------------------------------------------------------------------------- /api/src/test/java/cc/aoeiuv020/panovel/api/site/DajiaduTest.kt: -------------------------------------------------------------------------------- 1 | package cc.aoeiuv020.panovel.api.site 2 | 3 | import org.junit.Test 4 | 5 | /** 6 | * Created by AoEiuV020 on 2018.06.06-15:28:17. 7 | */ 8 | class DajiaduTest : BaseNovelContextText(Dajiadu::class) { 9 | @Test 10 | fun search() { 11 | // 这网站搜索可能随机失败,刷新又好, 12 | search("都市") 13 | search("修真聊天群", "圣骑士的传说", "24/24478") 14 | } 15 | 16 | @Test 17 | fun detail() { 18 | detail( 19 | "24/24478", "24/24478", "修真聊天群", "圣骑士的传说", 20 | "https://www.dajiadu8.com/files/article/image/24/24478/24478s.jpg", 21 | "某天,宋书航意外加入了一个仙侠中二病资深患者的交流群,里面的群友们都以‘道友’相称,群名片都是各种府主、洞主、真人、天师。连群主走失的宠物犬都称为大妖犬离家出走。整天聊的是炼丹、闯秘境、炼功经验啥的。\n" + 22 | "突然有一天,潜水良久的他突然发现……群里每一个群员,竟然全部是修真者,能移山倒海、长生千年的那种!\n" + 23 | "啊啊啊啊,世界观在一夜间彻底崩碎啦!\n" + 24 | "(本站郑重提醒:本故事纯属虚构,如有雷同,纯属巧合,切勿模仿。)", 25 | null 26 | ) 27 | } 28 | 29 | @Test 30 | fun chapters() { 31 | chapters( 32 | "24/24478", "书友群", "24/24478/7005434", null, 33 | "新书上传啦,《万界点名册》", "24/24478/14761299", null, 34 | 3229 35 | ) 36 | } 37 | 38 | @Test 39 | fun content() { 40 | content("23/23752/11507981", 41 | "矫若惊龙,漂若浮云,宣纸上的“咏箸”宛若一条蛟龙破纸而出,搅动一室浩然正气和灵气,震惊了场中众人。", 42 | "bq", 43 | 62) 44 | } 45 | 46 | } -------------------------------------------------------------------------------- /api/src/test/java/cc/aoeiuv020/panovel/api/site/GgdownTest.kt: -------------------------------------------------------------------------------- 1 | package cc.aoeiuv020.panovel.api.site 2 | 3 | import org.junit.Test 4 | 5 | /** 6 | * Created by AoEiuV020 on 2018.06.03-15:47:22. 7 | */ 8 | class GgdownTest : BaseNovelContextText(Ggdown::class) { 9 | @Test 10 | fun search() { 11 | search("都市") 12 | search("与天同兽", "雾矢翊", "46272") 13 | search("黑风城战记", "耳雅", "19447") 14 | } 15 | 16 | @Test 17 | fun detail() { 18 | detail("19447", "19447", "黑风城战记", "耳雅", 19 | "https://www.ggdownxs.com/image/19/19447/19447s.jpg", 20 | "恶帝城的建立打破了西北的平静,正邪之战一触即发~~\n" + 21 | "案件结合战役,龙图原班人马继续他们的传奇经历~~\n", 22 | "2019-05-15 00:00:00") 23 | } 24 | 25 | @Test 26 | fun chapters() { 27 | chapters("19447", "第1章 【黑风城】", "19/19447/5585742", null, 28 | "第254章 【番外火凤篇】01 红叶红蝶", "19/19447/33518674", null, 29 | 191) 30 | } 31 | 32 | @Test 33 | fun content() { 34 | content("19/19447/5585742", 35 | "黑风山位于黑风城以南,山顶常年积雪、山腰植被茂密、山脚溪流交错,一山四季。", 36 | "go~", 37 | 100) 38 | } 39 | 40 | } -------------------------------------------------------------------------------- /api/src/test/java/cc/aoeiuv020/panovel/api/site/GuanshuwangTest.kt: -------------------------------------------------------------------------------- 1 | package cc.aoeiuv020.panovel.api.site 2 | 3 | import org.junit.Test 4 | 5 | /** 6 | * Created by AoEiuV020 on 2018.06.03-18:17:22. 7 | */ 8 | class GuanshuwangTest : BaseNovelContextText(Guanshuwang::class) { 9 | @Test 10 | fun search() { 11 | search("都市") 12 | search("剑来", "烽火戏诸侯", "86/86047") 13 | search("天道图书馆", "横扫天涯", "86/86950") 14 | } 15 | 16 | @Test 17 | fun detail() { 18 | detail("86/86047", "86/86047", "剑来", "烽火戏诸侯", 19 | "https://www.guanshu.cc/coverimages/86/86047/86047s.jpg", 20 | "大千世界,无奇不有。" + 21 | "我陈平安,唯有一剑,可搬山,倒海,降妖,镇魔,敕神,摘星,断江,摧城,开天!", 22 | "2018-06-01 23:59:55") 23 | } 24 | 25 | @Test 26 | fun chapters() { 27 | chapters("86/86047", "新书感言", "86/86047/2336", null, 28 | "第三百一十一章 人外有人", "86/86047/1110517", "2018-06-01 23:59:55", 29 | 313) 30 | } 31 | 32 | @Test 33 | fun content() { 34 | content("86/86047/2336", 35 | "新书的重心在于“构建一个光怪陆离却合理有趣的仙侠世界”,对于一个崭新世界基础构建," + 36 | "即人与精怪鬼魅如何共处人间,会比较花力气和心思。当然,“传统”仙侠的套路,也会有,也是必然是不可或缺的。", 37 | "新书框架很大,写的时候会越来越大,但我有信心把那个世界写得有意思,也会很注重故事和人物之间的平衡," + 38 | "“珠子以线串成珠帘”,琳琅满目,毕竟是一件很美好的事物。世间好物不坚牢,彩云易散琉璃脆。那咱们就从小说里找补回来,多好。", 39 | 6) 40 | } 41 | 42 | } -------------------------------------------------------------------------------- /api/src/test/java/cc/aoeiuv020/panovel/api/site/GulizwTest.kt: -------------------------------------------------------------------------------- 1 | package cc.aoeiuv020.panovel.api.site 2 | 3 | import org.junit.Test 4 | 5 | /** 6 | * Created by AoEiuV020 on 2018.06.03-22:26:59. 7 | */ 8 | class GulizwTest : BaseNovelContextText(Gulizw::class) { 9 | @Test 10 | fun search() { 11 | // ProtocolException: Unexpected status line: ?�????????????��??????????HTTP/1.1 302 Moved Temporarily 12 | search("都市") 13 | search("一念永恒", "耳根", "94719") 14 | search("我是至尊", "风凌天下", "5141") 15 | } 16 | 17 | @Test 18 | fun detail() { 19 | detail("94719", "94719", "一念永恒", "耳根", 20 | "http://www.gulizw.com/files/article/image/94/94719/94719s.jpg", 21 | "一念成沧海,一念化桑田。一念斩千魔,一念诛万仙。唯我念……永恒", 22 | "2018-02-09 00:00:00") 23 | } 24 | 25 | @Test 26 | fun chapters() { 27 | chapters("52771", "第1章 黄山真君和九洲一号群", "52771/20455308", null, 28 | "第1743章 你就不能给我正常的晋升一回吗?", "52771/37939770", null, 29 | 1824) 30 | chapters("94719", "外传1 柯父。", "94719/28723558", null, 31 | "第1314章 你的选择(终)3", "94719/37928328", null, 32 | 1529) 33 | } 34 | 35 | @Test 36 | fun content() { 37 | content("94719/37928328", 38 | "画面在这一刻,成为了永恒,渐渐模糊,直至消散。", 39 | "六月一号,不见不散!", 40 | 21) 41 | } 42 | 43 | } -------------------------------------------------------------------------------- /api/src/test/java/cc/aoeiuv020/panovel/api/site/Jdxs520Test.kt: -------------------------------------------------------------------------------- 1 | package cc.aoeiuv020.panovel.api.site 2 | 3 | import org.junit.Test 4 | 5 | /** 6 | * Created by AoEiuV020 on 2018.06.03-18:50:03. 7 | */ 8 | class Jdxs520Test : BaseNovelContextText(Jdxs520::class) { 9 | @Test 10 | fun search() { 11 | // 这网站可能搜索结果可能随机出现乱码开头,刷新又好, 12 | search("都市") 13 | search("念念不忘,总裁乘胜追妻", "七爷", "77903") 14 | search("圣墟", "辰东", "45887") 15 | } 16 | 17 | @Test 18 | fun detail() { 19 | detail("77903", "77903", "念念不忘,总裁乘胜追妻", "七爷", 20 | "https://www.jdxs5200.net/files/article/image/77/77903/77903s.jpg", 21 | "初见,她在下,他在上,他的口中叫着别人的名字。\n" + 22 | "再见,她衣裳凌乱,披头散发,被人屈辱按在地上,狼狈不堪……\n" + 23 | "他是人人敬畏的传奇人物,霍家太子爷。\n" + 24 | "顺手救下她,冷漠送她四个字“咎由自取!”\n" + 25 | "狼狈的她,却露出一抹明媚的笑,声音清脆“姐夫……谢谢啊!”", 26 | "2018-06-02 21:52:00") 27 | } 28 | 29 | @Test 30 | fun chapters() { 31 | chapters("77903", "第1章 错位替身", "77903/45183134", null, 32 | "第148章 重新开始", "77903/46358897", "2018-06-02 21:52:00", 33 | 148) 34 | } 35 | 36 | @Test 37 | fun content() { 38 | content("77903/45183134", 39 | "十二月的南城很冷很冷。", 40 | "从此,她们之间就没有了交集。", 41 | 45) 42 | } 43 | 44 | } -------------------------------------------------------------------------------- /api/src/test/java/cc/aoeiuv020/panovel/api/site/KsswTest.kt: -------------------------------------------------------------------------------- 1 | package cc.aoeiuv020.panovel.api.site 2 | 3 | import org.junit.Test 4 | 5 | class KsswTest : BaseNovelContextText(Kssw::class) { 6 | @Test 7 | fun search() { 8 | search("都市") 9 | search("我真不是邪神走狗", "万劫火", "2588") 10 | } 11 | 12 | @Test 13 | fun detail() { 14 | detail( 15 | "2588", "2588", "我真不是邪神走狗", "万劫火", 16 | "https://pc.kssw.net/files/article/image/2/2588/2588s.jpg", 17 | "", 18 | "2020-10-07 01:19:00" 19 | ) 20 | } 21 | 22 | @Test 23 | fun chapters() { 24 | chapters( 25 | "2588", "001-欢迎光临", "2588/21098411", null, 26 | "255-“季”与A16庄园(第三更)", "2588/24265713", "2020-10-07 01:19:00", 27 | 257 28 | ) 29 | } 30 | 31 | @Test 32 | fun content() { 33 | content( 34 | "2588/24265713", 35 | "就在胡德与安德曾两人在哲罗姆办么室当中,商量着怎么把自家基地连锅端的时候。另一边,同在中央区的a16号住宅之中,此刻却发生了一件或许将要改变-些什么的事情。a16号住宅,姓季", 36 | "费很够,但是你再不去书店拜访,别人就都把羹分完了。季博农把那份文件拿过来翻了翻,心里-惊,眉头皱“这次真理会?”wuhl他的目光不由得移到了桌子的右上角,那里同样也有一模一样类型的纸质文件记录了每一次不同组织、个人和书店的接触,以及各自的结果。最近的一份情报,还是关于太阳神教的建立。在那之前,季博农依靠的还是自己的情报网,但是现在,女儿情报却比自己还要先了", 37 | 10 38 | ) 39 | } 40 | } -------------------------------------------------------------------------------- /api/src/test/java/cc/aoeiuv020/panovel/api/site/LiudatxtTest.kt: -------------------------------------------------------------------------------- 1 | package cc.aoeiuv020.panovel.api.site 2 | 3 | import org.junit.Test 4 | 5 | /** 6 | * 7 | * Created by AoEiuV020 on 2017.10.11-21:00:18. 8 | */ 9 | class LiudatxtTest : BaseNovelContextText(Liudatxt::class) { 10 | @Test 11 | fun search() { 12 | search("都市") 13 | search("诸天万界反派聊天群", "不要尬舞", "22921") 14 | } 15 | 16 | @Test 17 | fun detail() { 18 | detail("22921", "22921", "诸天万界反派聊天群", "不要尬舞", 19 | "http://www.txtshuku.org/headimgs/22/22921/s22921.jpg", 20 | "这个穿越画风有些不对?开局就是地狱难度是个什么鬼?还好自带一个金手指。\n" + 21 | "从此以后踏上了诸天万界各大反派人生导师的不归路!露出一脸和善微笑的雄霸正指挥着风云怒肛帝释天、" + 22 | "海贼世界已经成为一个出色海军的路飞正带着自己的海军攻打四皇、" + 23 | "以及把令狐冲当做亲儿子一般的对待满脸正气的岳不群。\n" + 24 | "......刘锋叹了一...", 25 | "2018-04-26 21:42:32") 26 | } 27 | 28 | @Test 29 | fun chapters() { 30 | chapters("22921", "第1章 穿越到噩梦难度的世界该怎么办?", "22921/8412231", null, 31 | "第584章 自信的零充", "22921/11561166", "2018-05-21 20:15:40", 32 | 610) 33 | } 34 | 35 | @Test 36 | fun content() { 37 | content("22921/8710426", 38 | "“你是怪物吗?!”这会儿刘锋是真的被面前这个身上血迹斑斑的哥们给惊着了,卧槽,二十多刀刀刀避开要害,简直是人才!", 39 | "顺带一提,为了避免实力一样引起的怀疑,刘锋稍稍的将刘淼这个账号的实力水平降低了一点设定在二级。", 40 | 37) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /api/src/test/java/cc/aoeiuv020/panovel/api/site/LreadTest.kt: -------------------------------------------------------------------------------- 1 | package cc.aoeiuv020.panovel.api.site 2 | 3 | import org.junit.Test 4 | 5 | /** 6 | * Created by AoEiuV020 on 2018.06.03-19:36:18. 7 | */ 8 | class LreadTest : BaseNovelContextText(Lread::class) { 9 | @Test 10 | fun search() { 11 | search("都市") 12 | search("飞剑问道", "我吃西红柿", "88917") 13 | } 14 | 15 | @Test 16 | fun detail() { 17 | detail("88917", "88917", "飞剑问道", "我吃西红柿", 18 | "https://www.6ks.net/files/article/image/88/88917/88917s.jpg", 19 | "在这个世界,有狐仙、河神、水怪、大妖,也有求长生的修行者。\n" + 20 | "修行者们,\n" + 21 | "开法眼,可看妖魔鬼怪。\n" + 22 | "炼一口飞剑,可千里杀敌。\n" + 23 | "千里眼、顺风耳,更可探查四方。\n" + 24 | "……\n" + 25 | "秦府二公子‘秦云’,便是一位修行者……", 26 | "2018-06-03 00:15:00") 27 | } 28 | 29 | @Test 30 | fun chapters() { 31 | chapters("88917", "第一章 归来", "88917/32771268", null, 32 | "第十一篇 第八章 魔神世界的帝君", "88917/40963324", "2018-06-03 00:15:00", 33 | 679) 34 | } 35 | 36 | @Test 37 | fun content() { 38 | content("88917/40963324", 39 | "“死!”", 40 | "那模糊庞大身影沉默了片刻,才道:“在蛮祖教布置的一切,全部毁掉,不留任何痕迹。不能让神霄道人他们查出来!至于你们四个,放弃蛮祖教,想办法逃命吧。”", 41 | 64) 42 | } 43 | 44 | } -------------------------------------------------------------------------------- /api/src/test/java/cc/aoeiuv020/panovel/api/site/MianhuatangTest.kt: -------------------------------------------------------------------------------- 1 | package cc.aoeiuv020.panovel.api.site 2 | 3 | import org.junit.Test 4 | 5 | /** 6 | * Created by AoEiuV020 on 2018.06.03-11:13:24. 7 | */ 8 | class MianhuatangTest : BaseNovelContextText(Mianhuatang::class) { 9 | @Test 10 | fun search() { 11 | search("元尊", "天蚕土豆", "8328778") 12 | } 13 | 14 | @Test 15 | fun detail() { 16 | detail("8326615", "8326615", "漫威世界的术士", "火之高兴", 17 | null, 18 | "恶魔是我的奴仆,邪能是我的力量,暗影与烈焰伴我左右,我是一名术士。" + 19 | "一名术士行走在漫威的世界里。(没系统,靠自己吧少年。)" + 20 | "欢迎加入漫威世界的术士书友群,群号码:177873494", 21 | null) 22 | } 23 | 24 | @Test 25 | fun chapters() { 26 | chapters("8326615", "第一章,周围没术士吧?我穿个越", "8326615/81992180", null, 27 | "第1029章", "8326615/84174452", null, 28 | 1057) 29 | } 30 | 31 | @Test 32 | fun content() { 33 | content("8329585/82278161", 34 | "在这样的情况下,如果能够让林霄有机会休息几秒,哪怕是几秒也好,估计也会恢复的比较快,但是奥布勒加却没有给林霄喘息的机会。", 35 | "只是这需要机会!", 36 | 69) 37 | } 38 | } -------------------------------------------------------------------------------- /api/src/test/java/cc/aoeiuv020/panovel/api/site/N7dswTest.kt: -------------------------------------------------------------------------------- 1 | package cc.aoeiuv020.panovel.api.site 2 | 3 | import org.junit.Test 4 | 5 | /** 6 | * Created by AoEiuV020 on 2018.06.03-21:27:57. 7 | */ 8 | class N7dswTest : BaseNovelContextText(N7dsw::class) { 9 | @Test 10 | fun search() { 11 | search("都市") 12 | search("江湖我独行", "心之弈剑", "0/133") 13 | search("重生之神级败家子", "辰机唐红豆", "1/1133") 14 | } 15 | 16 | @Test 17 | fun detail() { 18 | detail("0/654", "0/654", "魔天记", "忘语", 19 | "https://www.7dsw.com/files/article/image/0/654/654s.jpg", 20 | "一名在无数凶徒中长大的亡命少年,一个人与魔并立的时代,一个可以役使厉鬼、妖灵的大千世界……", 21 | null) 22 | } 23 | 24 | @Test 25 | fun chapters() { 26 | chapters("0/654", "凡人感言", "0/654/178709", null, 27 | "忘语新书《玄界之门》", "0/654/10148254", null, 28 | 1606) 29 | } 30 | 31 | @Test 32 | fun content() { 33 | content("0/654/10148254", 34 | "序章", 35 | "忘语新书《玄界之门》已经在正式上传连载了,还望喜欢《凡人修仙传》和《魔天记》的道友们,去关注和收藏一下哦。)(未完待续。)", 36 | 8) 37 | } 38 | 39 | } -------------------------------------------------------------------------------- /api/src/test/java/cc/aoeiuv020/panovel/api/site/PiaotianTest.kt: -------------------------------------------------------------------------------- 1 | package cc.aoeiuv020.panovel.api.site 2 | 3 | import org.junit.Test 4 | 5 | /** 6 | * 7 | * Created by AoEiuV020 on 2017.10.02-16:07:05. 8 | */ 9 | class PiaotianTest : BaseNovelContextText(Piaotian::class) { 10 | @Test 11 | fun search() { 12 | search("都市") 13 | search("都市之位面旅行家", "书仙鱼", "8/8017") 14 | search("斗破苍穹之无上之境", "夜雨闻铃0", "1/1767") 15 | search("从前有座灵剑山", "国王陛下", "4/4316") 16 | } 17 | 18 | @Test 19 | fun detail() { 20 | detail( 21 | "8/8605", "8/8605", "剑灵同居日记", "国王陛下", 22 | "https://www.ptwxz.com/files/article/image/8/8605/8605s.jpg", 23 | "“天外神剑剑灵,应呼唤而苏醒,我问你,你就是我的坐骑么?”\n一个无敌的随身剑灵与天才美少女(们)的同居故事。", 24 | "2018-05-21 17:30:00" 25 | ) 26 | } 27 | 28 | @Test 29 | fun chapters() { 30 | chapters("4/4316", "序幕:天外飞仙+第一章:客栈柴房温暖如春", "4/4316/2216316", null, 31 | "第七十七章:再见", "4/4316/4260402", null, 32 | 852) 33 | chapters("8/8912", 34 | "第001章 狂暴系统", "8/8912/5786830", null, 35 | "第3568章 大道无门?(补一)", "8/8912/6442359", null, 36 | 3564) 37 | } 38 | 39 | @Test 40 | fun content() { 41 | content("8/8605/5582838", 42 | "6月1日凌晨0点,本书正式上架。", 43 | "请各位绅士们量力而行,不必强求逆天。", 44 | 21) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /api/src/test/java/cc/aoeiuv020/panovel/api/site/QingkanTest.kt: -------------------------------------------------------------------------------- 1 | package cc.aoeiuv020.panovel.api.site 2 | 3 | import org.junit.Test 4 | 5 | /** 6 | * Created by AoEiuV020 on 2018.06.03-14:55:54. 7 | */ 8 | class QingkanTest : BaseNovelContextText(Qingkan::class) { 9 | @Test 10 | fun search() { 11 | search("都市") 12 | search("争虚", "何途", "zhengxu") 13 | search("盗天仙途", "荆柯守", "daotianxiantu") 14 | } 15 | 16 | @Test 17 | fun detail() { 18 | detail("daotianxiantu", "daotianxiantu", "盗天仙途", "荆柯守", 19 | null, 20 | "福地产生地仙,洞天来往天仙,我有梅花一株,盗取天机!", 21 | null) 22 | } 23 | 24 | @Test 25 | fun chapters() { 26 | chapters("daotianxiantu", "序", "daotianxiantu/29249297", null, 27 | "第三百五十四章 检测", "daotianxiantu/54903911", null, 28 | 308) 29 | } 30 | 31 | @Test 32 | fun content() { 33 | content("daotianxiantu/54903911", 34 | "古镜六寸高,形制古雅,镜面却有一个虚影,现出极淡的金光,正照着下去,一批拔干净,就换下一批,只是这时发生了变故。", 35 | "看着骑兵冲了上来,押粮将军冷笑:“你区区两三百轻甲就敢冲击押粮队?也罢,杀光你们,就是一功!”", 36 | 48) 37 | } 38 | 39 | } -------------------------------------------------------------------------------- /api/src/test/java/cc/aoeiuv020/panovel/api/site/ShoudashuTest.kt: -------------------------------------------------------------------------------- 1 | package cc.aoeiuv020.panovel.api.site 2 | 3 | import org.junit.Test 4 | 5 | /** 6 | * Created by AoEiuV020 on 2021.05.15-22:58:40. 7 | */ 8 | class ShoudashuTest : BaseNovelContextText(Shoudashu::class) { 9 | @Test 10 | fun search() { 11 | search("都市") 12 | search("都市最强狂神萧羽秦媛媛", "半只凉鞋", "277/277083") 13 | } 14 | 15 | @Test 16 | fun detail() { 17 | detail( 18 | "277/277083", "277/277083", "都市最强狂神萧羽秦媛媛", "半只凉鞋", 19 | "https://www.shoudashu.com/files/article/image//277/277083/277083s.jpg", 20 | "萧羽活了五千年,死不了,也老不成,这搞得他心情有点差,所以还请没事别招惹。要是惹怒了被暴打一顿,他还会狠狠地告诉你:“我的世界,没有能与不能,只有想与不想。”", 21 | null 22 | ) 23 | } 24 | 25 | @Test 26 | fun chapters() { 27 | chapters( 28 | "277/277083", "第一章 大亨之殇", "277/277083/66921365", null, 29 | "第四百二十六章 你怎么敢", "277/277083/70018272", null, 30 | 425 31 | ) 32 | } 33 | 34 | @Test 35 | fun content() { 36 | content( 37 | "277/277083/66921365", 38 | "炎夏,魔都。", 39 | "萧羽感受到的怨气,就是由她的身上散发出来的。", 40 | 91 41 | ) 42 | } 43 | 44 | } -------------------------------------------------------------------------------- /api/src/test/java/cc/aoeiuv020/panovel/api/site/SiFangTest.kt: -------------------------------------------------------------------------------- 1 | package cc.aoeiuv020.panovel.api.site 2 | 3 | import org.junit.Test 4 | 5 | /** 6 | * Created by AoEiuV020 on 2019.10.13-13:50:00. 7 | */ 8 | class SiFangTest : BaseNovelContextText(SiFang::class) { 9 | @Test 10 | fun search() { 11 | search("逆风岁月", "君尽欢", "20") 12 | } 13 | 14 | @Test 15 | fun detail() { 16 | detail("20", "20", "逆风岁月", "君尽欢", 17 | "http://www.sifangbook.com/uploads/20180724/4cf6966367c66aef17091409b493c895.jpg", 18 | "当曾经的谎言破碎在手中,原本一心想逃离家族的俞南思不得不停下脚步。为夺回母亲该有的一切,她甚至不惜踏上违背初心的路。而赫北书,无论曾经现在,虚无的皮囊,内心下依然隐藏着恶魔,肆无忌惮。\n" + 19 | "开始的二人,命途相似,做的却都不是真实的自...", 20 | "2018-12-24 23:49:32") 21 | } 22 | 23 | @Test 24 | fun chapters() { 25 | chapters("20", "001:关于俞家", "712", null, 26 | "140:圆满结局", "5543", "2018-12-24 23:49:32", 27 | 140) 28 | } 29 | 30 | @Test 31 | fun content() { 32 | content("712", 33 | "三月,是雨最肆意的时节。", 34 | "想到这儿,俞南思不屑地笑了笑,这种鬼话父亲也信,真是傻透了。", 35 | 56) 36 | } 37 | 38 | } -------------------------------------------------------------------------------- /api/src/test/java/cc/aoeiuv020/panovel/api/site/WenxuemiTest.kt: -------------------------------------------------------------------------------- 1 | package cc.aoeiuv020.panovel.api.site 2 | 3 | import org.junit.Test 4 | 5 | /** 6 | * Created by AoEiuV020 on 2018.06.03-19:57:36. 7 | */ 8 | class WenxuemiTest : BaseNovelContextText(Wenxuemi::class) { 9 | @Test 10 | fun search() { 11 | search("都市") 12 | search("大道朝天", "猫腻", "24/24163") 13 | } 14 | 15 | @Test 16 | fun detail() { 17 | detail( 18 | "30/30818", "30/30818", "长生种", "月中阴", 19 | "https://www.wenxuemi.cc/files/article/image/30/30818/30818s.jpg", 20 | "这是一个疯狂氪金(挂逼),在异界为所欲为的故事。生死看淡,不服就干!……ps:老月已经完本《法师奥义》《永恒武道》皆是精品,大家有兴趣的可以去看一看。老月出品,必属精品!书友企鹅群:189099589", 21 | "2020-03-22 23:11:15" 22 | ) 23 | } 24 | 25 | @Test 26 | fun chapters() { 27 | chapters("30/30818", "第一章 你可还有童子身?", "30/30818/15644464", null, 28 | "新书《以力服人》已经发布", "30/30818/20073343", "2020-03-22 23:11:15", 29 | 1126) 30 | } 31 | 32 | @Test 33 | fun content() { 34 | content("30/30818/15644464", 35 | "姓名:雷道(十八岁)", 36 | "张青龙也没有看那些银子,而是沉声道:“道哥儿真要练武也行,但这童子功却有些特殊。嗯,我得先问道哥儿一句,道哥儿,你可还有童子身?”", 37 | 73) 38 | } 39 | 40 | } -------------------------------------------------------------------------------- /api/src/test/java/cc/aoeiuv020/panovel/api/site/X23usTest.kt: -------------------------------------------------------------------------------- 1 | package cc.aoeiuv020.panovel.api.site 2 | 3 | import org.junit.Test 4 | 5 | /** 6 | * Created by AoEiuV020 on 2018.06.08-18:03:24. 7 | */ 8 | class X23usTest : BaseNovelContextText(X23us::class) { 9 | @Test 10 | fun search() { 11 | search("都市") 12 | search("帝霸", "厌笔萧生", "64889") 13 | } 14 | 15 | @Test 16 | fun detail() { 17 | detail("64889", "64889", "帝霸", "厌笔萧生", 18 | "https://www.x23us.com/files/article/image/64/64889/64889s.jpg", 19 | "天若逆我,我必封之,神若挡我,我必屠之——站在万族之巅的李七夜立下豪言!\n" + 20 | "这是属于一个平凡小子崛起的故事,一个牧童走向万族之巅的征程。\n" + 21 | "在这里充满神话与奇迹,天魔建起古国,石人筑就天城,鬼族铺成仙路,魅灵修补神府……", 22 | "2018-06-08 00:00:00") 23 | } 24 | 25 | @Test 26 | fun chapters() { 27 | chapters("64889", "契子(读者必看 非常重要)", "64/64889/26487455", null, 28 | "第3800章三才剑法", "64/64889/33839747", "2019-10-11 00:00:00", 29 | 3842) 30 | } 31 | 32 | @Test 33 | fun content() { 34 | content("64/64889/26487455", 35 | "契子", 36 | "一只不甘命运被左右的乌鸦,对抗着天地最可怕的存在,左右着千万年中的一个又一个大时代的变迁!", 37 | 24) 38 | } 39 | 40 | } -------------------------------------------------------------------------------- /api/src/test/java/cc/aoeiuv020/panovel/api/site/YmoxuanTest.kt: -------------------------------------------------------------------------------- 1 | package cc.aoeiuv020.panovel.api.site 2 | 3 | import org.junit.Test 4 | 5 | /** 6 | * Created by AoEiuV020 on 2018.06.03-13:57:34. 7 | */ 8 | class YmoxuanTest : BaseNovelContextText(Ymoxuan::class) { 9 | @Test 10 | fun search() { 11 | search("都市") 12 | search("斗战狂潮", "骷髅精灵", "31577") 13 | search("大主宰", "天蚕土豆", "21825") 14 | } 15 | 16 | @Test 17 | fun detail() { 18 | detail("31577", "31577", "斗战狂潮", "骷髅精灵", 19 | "http://r.m.ymxxs.com/jieqi/cover/31/31577/31577s.jpg", 20 | "双月当空,无限可能的英魂世界\n" + 21 | "孤寂黑暗,神秘古怪的嬉命小丑\n" + 22 | "百城联邦,三大帝国,异族横行,魂兽霸幽\n" + 23 | "这是一个英雄辈出的年代,人类卧薪尝胆重掌地球主权,孕育着进军高纬度的野望!\n" + 24 | "重点是……二年级的废柴学长王同学,如何使用嬉命轮盘,撬动整个世界,伙伴们,请注意,学长来了!!!", 25 | "2018-06-03 00:04:00") 26 | } 27 | 28 | @Test 29 | fun chapters() { 30 | chapters("31577", "第一章 嬉命小丑", "31/31577/29700048", null, 31 | "12月1日,英雄联盟:我的时代!", "31/31577/172511130", null, 32 | 1245) 33 | } 34 | 35 | @Test 36 | fun content() { 37 | content("31/31577/29700048", 38 | "欲望是进步的动力,也孕育了毁灭。", 39 | "小丑的脸一下子跨了下来,“什么小狗,我是嬉命小丑辛巴,戏弄命运,无所不能,小孩,你摊上大事儿了!”", 40 | 28) 41 | } 42 | 43 | } -------------------------------------------------------------------------------- /api/src/test/java/cc/aoeiuv020/panovel/api/site/YunduwuTest.kt: -------------------------------------------------------------------------------- 1 | package cc.aoeiuv020.panovel.api.site 2 | 3 | import org.junit.Test 4 | 5 | /** 6 | * Created by AoEiuV020 on 2020.03.28-23:30:33. 7 | */ 8 | class YunduwuTest : BaseNovelContextText(Yunduwu::class) { 9 | @Test 10 | fun search() { 11 | search("书") 12 | search("曾国藩家书", "曾国藩", "607") 13 | } 14 | 15 | @Test 16 | fun detail() { 17 | detail("607", "607", "曾国藩家书", "曾国藩", 18 | "https://www.yunduwu.com/upload/novel/20191116/157387546035259.jpg", 19 | "愚于近人,独服曾文正,观其收拾洪杨一役,完满无缺。使以今人易其位,其能如彼之完满乎?\n" + 20 | "——毛泽东1917年8月23日致黎锦熙\n" + 21 | "何兆武说:“中国历史上真正能够做到立德,立功,立言的只有二人:王阳明,曾国藩。”\n" + 22 | "曾国藩是中国近代史上权位显赫、作用极大、很有影响的人物,又是一个复杂的人物,近代中国人尤其湖南人,从权贵政要、志士仁人到青年学子,大多佩服曾国藩,佩服其治学为人和带兵做事。从李鸿章、张之洞到袁世凯、蒋介石,无不对他顶礼膜拜,尊为“圣哲”;从梁启超到杨昌济,从陈独秀到毛泽东,也无不表示过推崇其师法,受过其人的种种影响。", 23 | "2019-11-16 11:47:26") 24 | } 25 | 26 | @Test 27 | fun chapters() { 28 | chapters("607", "前言", "607/3576", null, 29 | "同治十年十月二十三日", "607/3631", null, 30 | 56) 31 | } 32 | 33 | @Test 34 | fun content() { 35 | content("607/3631", 36 | "澄、沅两弟左右:", 37 | "为兄在外两个多月,应酬太过繁忙,眩晕和疝气等毛病幸好没有复发,脚肿的毛病也好了。只是老眼昏花,一天比一天厉害;小便也太过频繁。衰弱和年老逼人,是当然之理,不值为怪。", 38 | 21) 39 | } 40 | } -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | admob_test_device_list 2 | class_files.txt -------------------------------------------------------------------------------- /app/schemas/cc.aoeiuv020.panovel.data.db.AppDatabase/1.json: -------------------------------------------------------------------------------- 1 | { 2 | "formatVersion": 1, 3 | "database": { 4 | "version": 1, 5 | "identityHash": "21396976adb39f249c4500e23b2b5d90", 6 | "entities": [ 7 | { 8 | "tableName": "SiteEnabled", 9 | "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT NOT NULL, `enabled` INTEGER NOT NULL, PRIMARY KEY(`name`))", 10 | "fields": [ 11 | { 12 | "fieldPath": "name", 13 | "columnName": "name", 14 | "affinity": "TEXT", 15 | "notNull": true 16 | }, 17 | { 18 | "fieldPath": "enabled", 19 | "columnName": "enabled", 20 | "affinity": "INTEGER", 21 | "notNull": true 22 | } 23 | ], 24 | "primaryKey": { 25 | "columnNames": [ 26 | "name" 27 | ], 28 | "autoGenerate": false 29 | }, 30 | "indices": [], 31 | "foreignKeys": [] 32 | } 33 | ], 34 | "setupQueries": [ 35 | "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", 36 | "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"21396976adb39f249c4500e23b2b5d90\")" 37 | ] 38 | } 39 | } -------------------------------------------------------------------------------- /app/src/androidTest/java/cc/aoeiuv020/panovel/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package cc.aoeiuv020.panovel 2 | 3 | import android.content.Context 4 | import androidx.test.core.app.ApplicationProvider 5 | import androidx.test.ext.junit.runners.AndroidJUnit4 6 | import org.junit.Assert.assertEquals 7 | import org.junit.Test 8 | import org.junit.runner.RunWith 9 | 10 | /** 11 | * Instrumented test, which will execute on an Android device. 12 | * 13 | * See [testing documentation](http://d.android.com/tools/testing). 14 | */ 15 | @RunWith(AndroidJUnit4::class) 16 | class ExampleInstrumentedTest { 17 | @Test 18 | fun useAppContext() { 19 | // Context of the app under test. 20 | val appContext: Context = ApplicationProvider.getApplicationContext() 21 | assertEquals(BuildConfig.APPLICATION_ID, appContext.packageName) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /app/src/androidTest/java/cc/aoeiuv020/panovel/RegexInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package cc.aoeiuv020.panovel 2 | 3 | import androidx.test.ext.junit.runners.AndroidJUnit4 4 | import org.junit.Assert.assertTrue 5 | import org.junit.Test 6 | import org.junit.runner.RunWith 7 | 8 | /** 9 | * 测试正则是否需要escape右中括号, 10 | * 之前爬漫画时遇到这个在最新android api 25模拟器上必须转义escape,而pc java又都可以,然后as警告建议不转义, 11 | * 之后我就习惯顶着警告写这个转义, 12 | * 突然想起这个问题,试一下,结果居然正常,不需要转义, 13 | * Created by AoEiuV020 on 2018.05.16-19:57:03. 14 | */ 15 | @RunWith(AndroidJUnit4::class) 16 | class RegexInstrumentedTest { 17 | private val withoutEscapeRegex = Regex("]") 18 | @Suppress("RegExpRedundantEscape") 19 | private val withEscapeRegex = Regex("\\]") 20 | private val str = "]" 21 | @Test 22 | fun withTest() { 23 | assertTrue(str.matches(withEscapeRegex)) 24 | } 25 | 26 | @Test 27 | fun withoutTest() { 28 | assertTrue(str.matches(withoutEscapeRegex)) 29 | } 30 | } -------------------------------------------------------------------------------- /app/src/androidTest/java/cc/aoeiuv020/panovel/local/CheckTest.kt: -------------------------------------------------------------------------------- 1 | package cc.aoeiuv020.panovel.local 2 | 3 | import android.content.Context 4 | import androidx.test.InstrumentationRegistry 5 | import androidx.test.runner.AndroidJUnit4 6 | import cc.aoeiuv020.panovel.main.Check 7 | import org.junit.Before 8 | import org.junit.Test 9 | import org.junit.runner.RunWith 10 | 11 | /** 12 | * Created by AoEiuV020 on 2018.05.14-11:46:55. 13 | */ 14 | @RunWith(AndroidJUnit4::class) 15 | class CheckTest { 16 | 17 | lateinit var context: Context 18 | 19 | @Before 20 | fun setUp() { 21 | context = InstrumentationRegistry.getTargetContext() 22 | } 23 | 24 | @Test 25 | fun curChangeLog() { 26 | Check.getChangeLogFromAssert(context, "2.1.1").let { 27 | println(it) 28 | } 29 | } 30 | 31 | } -------------------------------------------------------------------------------- /app/src/androidTest/java/cc/aoeiuv020/panovel/util/DnsUtilsAndroidTest.kt: -------------------------------------------------------------------------------- 1 | package cc.aoeiuv020.panovel.util 2 | 3 | import androidx.test.core.app.ApplicationProvider 4 | import androidx.test.ext.junit.runners.AndroidJUnit4 5 | import cc.aoeiuv020.panovel.ad.AdConstants 6 | import org.junit.Assert.assertEquals 7 | import org.junit.Assert.assertTrue 8 | import org.junit.Before 9 | import org.junit.Test 10 | import org.junit.runner.RunWith 11 | 12 | @RunWith(AndroidJUnit4::class) 13 | class DnsUtilsAndroidTest { 14 | 15 | @Before 16 | fun init() { 17 | DnsUtils.init(ApplicationProvider.getApplicationContext()) 18 | } 19 | 20 | @Test 21 | fun test13lm() { 22 | val txtList = DnsUtils.getTxtList(AdConstants.HOST_13LM) 23 | println(txtList) 24 | assertTrue(txtList.contains("enabled=1")) 25 | } 26 | 27 | @Test 28 | fun testParseTxt() { 29 | val txtMap = DnsUtils.parseTxt(AdConstants.HOST_13LM) 30 | println(txtMap) 31 | assertEquals("1", txtMap["enabled"]) 32 | } 33 | } -------------------------------------------------------------------------------- /app/src/main/assets/Disclaimer.txt: -------------------------------------------------------------------------------- 1 |   爬小说提醒您:在使用爬小说前,请您务必仔细阅读并透彻理解本声明。您可以选择不使用爬小说,但如果您使用爬小说,您的使用行为将被视为对本声明全部内容的认可。 2 |   爬小说是一款提供网络小说即时更新的工具,为广大小说爱好者提供一种方便、快捷、舒适的试读体验。爬小说致力于最大程度的减少网络小说阅读者在自行搜寻过程中毫无意义的时间浪费。 3 |   爬小说为广大小说爱好者提供方便、快捷、舒适的试读体验的同时,也使优秀网络小说得以更迅捷、更广泛的传播,从而达到了在一定程度促进网络文学充分繁荣发展之目的。 4 |   当您点击搜索一本书时,爬小说会将该书的书名以关键词的形式提交到第三方网站。第三方网站返回的内容与爬小说无关,爬小说对其概不负责,亦不承担任何法律责任。 5 |   爬小说不提供服务器缓存或传播这些小说内容,用户直接从网站获取小说。 6 |   任何通过使用爬小说而链接到的第三方网页均系他人制作或提供,您可能从该第三方网页上获得其他服务,爬小说对其合法性概不负责,亦不承担任何法律责任。 7 |   第三方搜索引擎结果根据你提交的书名自动搜索获得并提供试读,不代表爬小说赞成被搜索链接到的第三方网页上的内容或立场。您应该对使用搜索引擎的结果自行承担风险。 8 |   爬小说不做任何形式的保证:不保证第三方搜索引擎的搜索结果满足您的要求,不保证搜索服务不中断,不保证搜索结果的安全性、正确性、及时性、合法性。因网络状况、通讯线路、第三方网站等任何原因而导致您不能正常使用爬小说,爬小说不承担任何法律责任。 9 |   爬小说鼓励广大小说爱好者通过爬小说发现优秀网络小说及其提供商,并建议阅读正版图书。 10 |   任何单位或个人认为通过爬小说搜索链接到的第三方网页内容可能涉嫌侵犯其信息网络传播权,应该及时向爬小说提出书面权利通知,并提供身份证明、权属证明及详细侵权情况证明。 11 |   爬小说在收到上述法律文件后,将会依法尽快断开相关链接内容。 12 | -------------------------------------------------------------------------------- /app/src/main/assets/Donate.txt: -------------------------------------------------------------------------------- 1 | 捐赠说明: 2 | paypal会跳到浏览器打开我的paypal.me付款页面, 3 | paypal账号:AoEiuV020 4 | 支付宝跳到浏览器,在网页点打开支付宝跳到转账页面, 5 | 支付宝账号:490674483@qq.com 6 | 微信没法直接跳到转账界面,只能自己保存二维码,跳到微信扫码选择, 7 | 微信账号:lenl020 8 | 9 | 捐赠只是捐赠,聊表心意, 10 | 不是应用付费功能, 11 | 无法知道是否捐赠, 12 | 无法知道是谁捐赠的, 13 | 无法记住这个捐赠行为, 14 | 15 | 事实上主要是国内支付接口对个人开发者不友好, 16 | 另外本app也没有服务器保存用户信息, 17 | -------------------------------------------------------------------------------- /app/src/main/assets/Explain.txt: -------------------------------------------------------------------------------- 1 | Q: 为什么的我的缓存第二天就不见了? 2 | A: 可能是被手机管家自动清了缓存,可以在设置->路径设置中修改缓存路径,改到sd卡里就安全了, 3 | 4 | Q: 导出的小说在哪里? 5 | A: 通知里的显示,默认在sd卡app私有目录里,可以在设置->路径设置中修改导出文件保存位置, 6 | 7 | Q: 怎么导入本地小说? 8 | A: 主页右上菜单中的打开,点弹出对话框的“本地小说”按钮,目前支持txt格式和epub格式电子书, 9 | 另外导入本地小说是复制一份到app内部,不影响原文件, 10 | 且不刷新,原文件的改动要重新导入, 11 | 12 | Q: 清除小说和移出书架有什么区别? 13 | A: 清除小说会清除包括缓存,阅读进度在内的一切,本地小说要用清除小说来删除,否则会有残留, 14 | 15 | Q: 扫一扫和打开是做什么的? 16 | A: 主页菜单里的“扫一扫”和“打开”支持的内容是一样的, 17 | 目前支持打开分享的书单页面就会导入这个书单, 18 | 以及打开小说详情页地址就会跳到app内的小说详情页, 19 | 不是地址就会直接跳到搜索, 20 | 21 | Q: 为什么搜索出来的小说章节不是最新的? 22 | A: 需要等搜索结束后手动下拉刷新,或者长按某一本小说选择刷新这一本, 23 | 主要是出于省流量考虑不随便刷新章节, 24 | 25 | Q: 为什么有些书源(无法搜索|无法阅读)? 26 | A: 书源网站调整可能导致app中的书源失效, 27 | 新添加的书源都是有不定时维护的,有问题可能下次更新就能修复, 28 | 如果原网站失效(比如关站跑路了)就会删除书源, 29 | 如果原网站只是部分功能失效(比如无法搜索但能正常阅读)就会停止维护该书源, 30 | 已经停止维护的书源如果哪天原网站恢复正常或者彻底失效,有可能不会发现所以不会修复或者删除书源,读者发现了可以联系我处理, 31 | 另外书源页面会默认勾选所有还在维护的书源,搜索只会搜索勾选了的书源, 32 | 33 | 34 | 有的问题已经忘了,所以这里有点空荡荡的, 35 | 有问题可以直接联系我,联系方式在设置->关于页面, 36 | -------------------------------------------------------------------------------- /app/src/main/assets/Licenses.txt: -------------------------------------------------------------------------------- 1 | [jsoup](https://github.com/jhy/jsoup) 2 | [glide](https://github.com/bumptech/glide) 3 | [anko](https://github.com/Kotlin/anko) 4 | [MaterialSearchView](https://github.com/MiguelCatalan/MaterialSearchView) 5 | [slf4j](https://github.com/qos-ch/slf4j) 6 | [gson](https://github.com/google/gson) 7 | [ColorPicker](https://github.com/QuadFlask/colorpicker) 8 | [MagicIndicator](https://github.com/hackware1993/MagicIndicator) 9 | [NovelReader](https://github.com/newbiechen1024/NovelReader) 10 | [Bugly](https://github.com/BuglyDevTeam/Bugly-Android) 11 | [Zip4j](https://mvnrepository.com/artifact/net.lingala.zip4j/zip4j) 12 | [AspectRatioImageView](https://github.com/santalu/aspect-ratio-imageview) 13 | [jchardet](http://jchardet.sourceforge.net/index.html) 14 | -------------------------------------------------------------------------------- /app/src/main/ic_launcher-web.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AoEiuV020/PaNovel/48fc2248521cfeb52ac9fbcbb9a09e2fc6e0e95d/app/src/main/ic_launcher-web.png -------------------------------------------------------------------------------- /app/src/main/java/cc/aoeiuv020/panovel/IView.kt: -------------------------------------------------------------------------------- 1 | package cc.aoeiuv020.panovel 2 | 3 | /** 4 | * mvp的view, 5 | * Created by AoEiuV020 on 2017.10.11-15:32:52. 6 | */ 7 | interface IView -------------------------------------------------------------------------------- /app/src/main/java/cc/aoeiuv020/panovel/Presenter.kt: -------------------------------------------------------------------------------- 1 | package cc.aoeiuv020.panovel 2 | 3 | import org.jetbrains.anko.AnkoLogger 4 | import org.jetbrains.anko.verbose 5 | 6 | /** 7 | * mvp的presenter, 8 | * Created by AoEiuV020 on 2017.10.11-15:32:17. 9 | */ 10 | abstract class Presenter : AnkoLogger { 11 | var view: T? = null 12 | private set 13 | 14 | fun attach(view: IView) { 15 | verbose { "$this attach $view" } 16 | @Suppress("UNCHECKED_CAST") 17 | this.view = view as? T 18 | } 19 | 20 | fun detach() { 21 | verbose { "$this detach $view" } 22 | view = null 23 | } 24 | 25 | override fun toString(): String = "${javaClass.simpleName}@${hashCode()}" 26 | } -------------------------------------------------------------------------------- /app/src/main/java/cc/aoeiuv020/panovel/ad/AdConstants.kt: -------------------------------------------------------------------------------- 1 | package cc.aoeiuv020.panovel.ad 2 | 3 | /** 4 | * Created by AoEiuV020 on 2021.05.15-19:35:37. 5 | */ 6 | object AdConstants { 7 | const val HOST_13LM = "13lm.aoeiuv020.com" 8 | const val GDT_APP_ID = "1111762810" 9 | const val GDT_AD_ID_SPLASH = "2091388033327424" 10 | const val GDT_AD_ID_LIST = "9061885053908861" 11 | const val CD_SPLASH_BACKGROUND = 30 12 | const val CD_SPLASH_CREATE = 10 13 | } -------------------------------------------------------------------------------- /app/src/main/java/cc/aoeiuv020/panovel/ad/AdHelper.kt: -------------------------------------------------------------------------------- 1 | package cc.aoeiuv020.panovel.ad 2 | 3 | import android.app.Application 4 | import android.os.Build 5 | import cc.aoeiuv020.panovel.settings.AdSettings 6 | import org.jetbrains.anko.AnkoLogger 7 | 8 | /** 9 | * Created by AoEiuV020 on 2021.04.25-23:45:31. 10 | */ 11 | object AdHelper : AnkoLogger { 12 | fun init(context: Application) { 13 | } 14 | 15 | fun checkSplashAdAvailable() = AdSettings.adEnabled 16 | && isArm() 17 | 18 | private fun isArm(): Boolean { 19 | // GDT只支持arm系列, 20 | // 不能判断SUPPORTED_ABIS,因为模拟器支持arm情况也是优先使用x86导致无法加载广告, 21 | @Suppress("DEPRECATION") 22 | return Build.CPU_ABI.contains("arm") 23 | } 24 | 25 | fun createListHelper() = TestAdListHelper() 26 | } -------------------------------------------------------------------------------- /app/src/main/java/cc/aoeiuv020/panovel/ad/EmptyAdListHelper.kt: -------------------------------------------------------------------------------- 1 | package cc.aoeiuv020.panovel.ad 2 | 3 | /** 4 | * Created by AoEiuV020 on 2021.05.15-19:41:26. 5 | */ 6 | class EmptyAdListHelper { 7 | } -------------------------------------------------------------------------------- /app/src/main/java/cc/aoeiuv020/panovel/backup/BackupHelper.kt: -------------------------------------------------------------------------------- 1 | package cc.aoeiuv020.panovel.backup 2 | 3 | import android.app.Activity 4 | import cc.aoeiuv020.panovel.util.Pref 5 | import java.io.File 6 | 7 | /** 8 | * Created by AoEiuV020 on 2021.04.25-12:38:46. 9 | */ 10 | interface BackupHelper : Pref { 11 | override val name: String 12 | get() = "Backup$type" 13 | val type: String 14 | fun ready(): Boolean 15 | fun configPreview(): String 16 | fun configActivity(): Class 17 | fun restore(tempFile: File) 18 | fun backup(tempFile: File) 19 | } -------------------------------------------------------------------------------- /app/src/main/java/cc/aoeiuv020/panovel/backup/BackupOption.kt: -------------------------------------------------------------------------------- 1 | package cc.aoeiuv020.panovel.backup 2 | 3 | /** 4 | * Created by AoEiuV020 on 2018.05.11-19:32:55. 5 | */ 6 | enum class BackupOption { 7 | Bookshelf, 8 | BookList, 9 | Progress, 10 | Settings, 11 | } -------------------------------------------------------------------------------- /app/src/main/java/cc/aoeiuv020/panovel/backup/IBackup.kt: -------------------------------------------------------------------------------- 1 | package cc.aoeiuv020.panovel.backup 2 | 3 | import java.io.File 4 | 5 | /** 6 | * Created by AoEiuV020 on 2018.05.11-17:46:28. 7 | */ 8 | interface IBackup { 9 | fun import(base: File, options: Set): String 10 | fun export(base: File, options: Set): String 11 | } -------------------------------------------------------------------------------- /app/src/main/java/cc/aoeiuv020/panovel/data/NovelProvider.kt: -------------------------------------------------------------------------------- 1 | package cc.aoeiuv020.panovel.data 2 | 3 | import cc.aoeiuv020.panovel.api.NovelChapter 4 | import java.net.URL 5 | 6 | /** 7 | * 负责提供小说相关数据, 8 | * 9 | * Created by AoEiuV020 on 2018.06.13-12:58:30. 10 | */ 11 | interface NovelProvider { 12 | fun getContentUrl(chapter: NovelChapter): String 13 | fun getNovelContent(chapter: NovelChapter, listener: ((Long, Long) -> Unit)?): List 14 | fun requestNovelChapters(): List 15 | fun getDetailUrl(): String 16 | fun updateNovelDetail() 17 | fun cleanData() 18 | fun cleanCache() 19 | // 图片也要看上下文获取,比如epub内部, 20 | fun getImage(extra: String): URL 21 | } -------------------------------------------------------------------------------- /app/src/main/java/cc/aoeiuv020/panovel/data/ServerManager.kt: -------------------------------------------------------------------------------- 1 | package cc.aoeiuv020.panovel.data 2 | 3 | import android.content.Context 4 | import cc.aoeiuv020.panovel.data.entity.Novel 5 | import cc.aoeiuv020.panovel.server.ServerManager 6 | import cc.aoeiuv020.panovel.server.dal.model.Message 7 | import cc.aoeiuv020.panovel.server.dal.model.QueryResponse 8 | import cc.aoeiuv020.panovel.server.toServer 9 | 10 | /** 11 | * 12 | * Created by AoEiuV020 on 2018.05.31-18:22:55. 13 | */ 14 | class ServerManager(@Suppress("UNUSED_PARAMETER") ctx: Context) { 15 | 16 | /** 17 | * 订阅书架列表,覆盖所有tag, 18 | */ 19 | fun setTags(list: List) { 20 | } 21 | 22 | /** 23 | * 添加这些小说的订阅, 24 | */ 25 | fun addTags( 26 | list: List, callback: (List) -> Unit = { novelList -> 27 | } 28 | ) { 29 | } 30 | 31 | fun removeTags( 32 | list: List, callback: (List) -> Unit = { novelList -> 33 | } 34 | ) { 35 | } 36 | 37 | fun touchUpdate(novel: Novel) = ServerManager.touch(novel.toServer()) 38 | fun askUpdate(list: List): Map = 39 | ServerManager.queryList(list.map { it.nId to it.toServer() }.toMap()) 40 | 41 | fun getMessage(): Message? = ServerManager.message() 42 | } -------------------------------------------------------------------------------- /app/src/main/java/cc/aoeiuv020/panovel/data/db/converter.kt: -------------------------------------------------------------------------------- 1 | package cc.aoeiuv020.panovel.data.db 2 | 3 | import androidx.room.TypeConverter 4 | import java.util.* 5 | 6 | 7 | /** 8 | * Created by AoEiuV020 on 2018.05.26-22:43:21. 9 | */ 10 | class DateTypeConverter { 11 | 12 | @TypeConverter 13 | fun toDate(value: Long?): Date? { 14 | return value?.let { Date(it) } 15 | } 16 | 17 | @TypeConverter 18 | fun toLong(value: Date?): Long? { 19 | return value?.time 20 | } 21 | } -------------------------------------------------------------------------------- /app/src/main/java/cc/aoeiuv020/panovel/data/entity/BookList.kt: -------------------------------------------------------------------------------- 1 | package cc.aoeiuv020.panovel.data.entity 2 | 3 | import androidx.room.Entity 4 | import androidx.room.Index 5 | import androidx.room.PrimaryKey 6 | import cc.aoeiuv020.panovel.util.notNullOrReport 7 | import java.util.* 8 | 9 | /** 10 | * Created by AoEiuV020 on 2018.05.24-17:15:36. 11 | */ 12 | 13 | /** 14 | * 书单, 15 | */ 16 | @Entity(indices = [ 17 | Index( 18 | value = ["uuid"], 19 | unique = true 20 | ) 21 | ]) 22 | data class BookList( 23 | /** 24 | * 普通的id, 25 | * 要给个null才能autoGenerate, 26 | * 插入时拿到id再赋值回来,所以要可变var, 27 | */ 28 | @PrimaryKey(autoGenerate = true) 29 | val id: Long? = null, 30 | val name: String, 31 | /** 32 | * 创建书单的时间,用于展示时排序, 33 | */ 34 | val createTime: Date = Date(), 35 | /** 36 | * 唯一的uuid, 为了避免重复导入同一个书单造成重复, 37 | */ 38 | val uuid: String = UUID.randomUUID().toString() 39 | ) { 40 | // id的非空版本,实在是要经常用id, 而且是不可能为空的id, 41 | val nId: Long get() = id.notNullOrReport() 42 | } -------------------------------------------------------------------------------- /app/src/main/java/cc/aoeiuv020/panovel/data/entity/NovelWithProgress.kt: -------------------------------------------------------------------------------- 1 | package cc.aoeiuv020.panovel.data.entity 2 | 3 | /** 4 | * Created by AoEiuV020 on 2018.05.31-12:48:03. 5 | */ 6 | data class NovelWithProgress( 7 | /** 8 | * 网站名, 9 | * 必须存在,不可空,一本小说至少要有["site", "author“, ”name", "detail"], 10 | * 不外键到网站表,那张表不稳定, 11 | */ 12 | var site: String, 13 | /** 14 | * 作者名, 15 | * 必须存在,不可空,一本小说至少要有["site", "author“, ”name", "detail"], 16 | */ 17 | var author: String, 18 | /** 19 | * 小说名, 20 | * 必须存在,不可空,一本小说至少要有["site", "author“, ”name", "detail"], 21 | */ 22 | var name: String, 23 | /** 24 | * 用于请求小说详情页的额外信息, 25 | * 必须存在,不可空,一本小说至少要有["site", "author“, ”name", "detail"], 26 | * [cc.aoeiuv020.panovel.api.NovelItem.extra] 27 | */ 28 | var detail: String, 29 | 30 | // 阅读进度, 31 | 32 | /** 33 | * 阅读进度, 34 | * 阅读至的章节索引, 35 | */ 36 | var readAtChapterIndex: Int = 0, 37 | /** 38 | * 章节内的阅读进度, 39 | * 看到第几页或者第几个字,具体没决定, 40 | */ 41 | var readAtTextIndex: Int = 0 42 | ) { 43 | constructor(novel: Novel) 44 | : this(novel.site, novel.author, novel.name, novel.detail, novel.readAtChapterIndex, novel.readAtTextIndex) 45 | } -------------------------------------------------------------------------------- /app/src/main/java/cc/aoeiuv020/panovel/data/entity/Site.kt: -------------------------------------------------------------------------------- 1 | package cc.aoeiuv020.panovel.data.entity 2 | 3 | import androidx.room.Entity 4 | import androidx.room.PrimaryKey 5 | import java.util.* 6 | 7 | /** 8 | * Created by AoEiuV020 on 2018.05.24-14:16:53. 9 | */ 10 | /** 11 | * 小说网站信息, 12 | */ 13 | @Entity 14 | data class Site( 15 | @PrimaryKey 16 | val name: String, 17 | var baseUrl: String, 18 | var logo: String, 19 | // 启用,true表示全站搜索时包括这个站, 20 | var enabled: Boolean = true, 21 | /** 22 | * 置顶时间,网站列表按这个排序, 23 | * 不置顶的给个最小时间, 24 | */ 25 | var pinnedTime: Date = Date(0), 26 | // 隐藏,true列表在网站列表里看不到, 27 | var hide: Boolean = false, 28 | /** 29 | * 创建时间,以便把新书源和老书源分开, 30 | */ 31 | var createTime: Date = Date(0) 32 | ) 33 | -------------------------------------------------------------------------------- /app/src/main/java/cc/aoeiuv020/panovel/download/DownloadActivity.kt: -------------------------------------------------------------------------------- 1 | package cc.aoeiuv020.panovel.download 2 | 3 | import android.content.Context 4 | import android.os.Bundle 5 | import androidx.appcompat.app.AppCompatActivity 6 | import cc.aoeiuv020.panovel.IView 7 | import cc.aoeiuv020.panovel.R 8 | import org.jetbrains.anko.startActivity 9 | 10 | class DownloadActivity : AppCompatActivity(), IView { 11 | companion object { 12 | fun start(ctx: Context) { 13 | ctx.startActivity() 14 | } 15 | } 16 | 17 | private lateinit var presenter: DownloadPresenter 18 | 19 | override fun onCreate(savedInstanceState: Bundle?) { 20 | super.onCreate(savedInstanceState) 21 | setContentView(R.layout.activity_download) 22 | 23 | presenter = DownloadPresenter() 24 | presenter.attach(this) 25 | presenter.start() 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /app/src/main/java/cc/aoeiuv020/panovel/download/DownloadPresenter.kt: -------------------------------------------------------------------------------- 1 | package cc.aoeiuv020.panovel.download 2 | 3 | import cc.aoeiuv020.panovel.Presenter 4 | import org.jetbrains.anko.AnkoLogger 5 | 6 | /** 7 | * Created by AoEiuV020 on 2018.10.06-19:06:57. 8 | */ 9 | class DownloadPresenter : Presenter(), AnkoLogger { 10 | fun start() { 11 | } 12 | } -------------------------------------------------------------------------------- /app/src/main/java/cc/aoeiuv020/panovel/download/DownloadProgressListener.kt: -------------------------------------------------------------------------------- 1 | package cc.aoeiuv020.panovel.download 2 | 3 | /** 4 | * Created by AoEiuV020 on 2018.10.07-13:10:45. 5 | */ 6 | interface DownloadProgressListener { 7 | fun downloading(offset: Long, length: Long) 8 | } -------------------------------------------------------------------------------- /app/src/main/java/cc/aoeiuv020/panovel/find/qidiantu/list/Item.kt: -------------------------------------------------------------------------------- 1 | package cc.aoeiuv020.panovel.find.qidiantu.list 2 | 3 | data class Item( 4 | val url: String, 5 | // 名字可能有省略号需要更新, 6 | var name: String, 7 | val author: String, 8 | val level: String, 9 | val type: String, 10 | val words: String, 11 | val collection: String, 12 | val firstOrder: String, 13 | val ratio: String, 14 | val dateAdded: String, 15 | var image: String? = null 16 | ) 17 | -------------------------------------------------------------------------------- /app/src/main/java/cc/aoeiuv020/panovel/find/qidiantu/list/Post.kt: -------------------------------------------------------------------------------- 1 | package cc.aoeiuv020.panovel.find.qidiantu.list 2 | 3 | data class Post( 4 | val name: String, 5 | val url: String 6 | ) 7 | -------------------------------------------------------------------------------- /app/src/main/java/cc/aoeiuv020/panovel/find/shuju/list/Item.kt: -------------------------------------------------------------------------------- 1 | package cc.aoeiuv020.panovel.find.shuju.list 2 | 3 | /** 4 | * Created by AoEiuV020 on 2021.09.06-22:02:55. 5 | */ 6 | data class Item( 7 | val url: String, 8 | // 名字可能有省略号需要更新, 9 | var name: String, 10 | val author: String, 11 | val level: String, 12 | val type: String, 13 | val words: String, 14 | val collection: String, 15 | val firstOrder: String, 16 | val ratio: String, 17 | var image: String? = null 18 | ) 19 | -------------------------------------------------------------------------------- /app/src/main/java/cc/aoeiuv020/panovel/find/shuju/post/Post.kt: -------------------------------------------------------------------------------- 1 | package cc.aoeiuv020.panovel.find.shuju.post 2 | 3 | import java.util.* 4 | 5 | /** 6 | * Created by AoEiuV020 on 2021.09.06-22:02:55. 7 | */ 8 | data class Post( 9 | val url: String, 10 | val title: String, 11 | val num: String, 12 | val date: Date? 13 | ) 14 | -------------------------------------------------------------------------------- /app/src/main/java/cc/aoeiuv020/panovel/find/sp7/list/Item.kt: -------------------------------------------------------------------------------- 1 | package cc.aoeiuv020.panovel.find.sp7.list 2 | 3 | data class Item( 4 | val url: String, 5 | // 名字可能有省略号需要更新, 6 | var name: String, 7 | val author: String, 8 | val level: String, 9 | val type: String, 10 | val words: String, 11 | val collection: String, 12 | val firstOrder: String, 13 | val ratio: String, 14 | val dateAdded: String, 15 | var image: String? = null 16 | ) 17 | -------------------------------------------------------------------------------- /app/src/main/java/cc/aoeiuv020/panovel/list/NovelItemActionListener.kt: -------------------------------------------------------------------------------- 1 | package cc.aoeiuv020.panovel.list 2 | 3 | interface NovelItemActionListener { 4 | fun onDotClick(vh: NovelViewHolder) 5 | fun onDotLongClick(vh: NovelViewHolder): Boolean 6 | fun onCheckUpdateClick(vh: NovelViewHolder) 7 | fun onNameClick(vh: NovelViewHolder) 8 | fun onNameLongClick(vh: NovelViewHolder): Boolean 9 | fun onLastChapterClick(vh: NovelViewHolder) 10 | fun onItemClick(vh: NovelViewHolder) 11 | fun onItemLongClick(vh: NovelViewHolder): Boolean 12 | fun onStarChanged(vh: NovelViewHolder, star: Boolean) 13 | fun refreshChapters(vh: NovelViewHolder) 14 | } -------------------------------------------------------------------------------- /app/src/main/java/cc/aoeiuv020/panovel/local/ImportRequireValue.kt: -------------------------------------------------------------------------------- 1 | package cc.aoeiuv020.panovel.local 2 | 3 | /** 4 | * 导入小说时需要询问用户决定的值, 5 | * 6 | * Created by AoEiuV020 on 2018.06.16-15:14:25. 7 | */ 8 | enum class ImportRequireValue { 9 | TYPE, CHARSET, AUTHOR, NAME 10 | } -------------------------------------------------------------------------------- /app/src/main/java/cc/aoeiuv020/panovel/migration/MigrateException.kt: -------------------------------------------------------------------------------- 1 | package cc.aoeiuv020.panovel.migration 2 | 3 | /** 4 | * Created by AoEiuV020 on 2018.05.17-17:06:35. 5 | */ 6 | class MigrateException( 7 | val migration: Migration, 8 | message: String? = null, 9 | cause: Throwable? 10 | ) : Exception("迁移数据到版本<${migration.to.name}>失败: $message", cause) -------------------------------------------------------------------------------- /app/src/main/java/cc/aoeiuv020/panovel/migration/Migration.kt: -------------------------------------------------------------------------------- 1 | package cc.aoeiuv020.panovel.migration 2 | 3 | import android.content.Context 4 | import cc.aoeiuv020.panovel.util.VersionName 5 | 6 | /** 7 | * Created by AoEiuV020 on 2018.05.17-16:16:10. 8 | */ 9 | abstract class Migration { 10 | /** 11 | * 从哪个版本开始需要这个Migration, 12 | * 低于这个版本的需要升级到这个patchVersion版本, 13 | */ 14 | abstract val to: VersionName 15 | 16 | abstract val message: String 17 | 18 | /** 19 | * 实际的升级方法, 20 | * 21 | * TODO: 考虑下要不要询问是否删除旧版数据,先不删除旧版数据, 22 | */ 23 | abstract fun migrate(ctx: Context, from: VersionName) 24 | 25 | override fun toString(): String { 26 | return "${javaClass.simpleName}(to=$to, message=$message)" 27 | } 28 | } -------------------------------------------------------------------------------- /app/src/main/java/cc/aoeiuv020/panovel/migration/MigrationView.kt: -------------------------------------------------------------------------------- 1 | package cc.aoeiuv020.panovel.migration 2 | 3 | import cc.aoeiuv020.panovel.IView 4 | import cc.aoeiuv020.panovel.util.VersionName 5 | 6 | /** 7 | * Created by AoEiuV020 on 2018.05.16-22:56:40. 8 | */ 9 | interface MigrationView : IView { 10 | fun showDowngrade(from: VersionName, to: VersionName) 11 | // 这个必调用,除非迁移抛异常,其他操作等迁移完了在这个方法内进行, 12 | fun showMigrateComplete(from: VersionName, to: VersionName) 13 | fun showUpgrading(from: VersionName, migration: Migration) 14 | fun showMigrateError(from: VersionName, migration: Migration) 15 | fun showError(message: String, e: Throwable) 16 | } -------------------------------------------------------------------------------- /app/src/main/java/cc/aoeiuv020/panovel/migration/impl/AdSettingsMigration.kt: -------------------------------------------------------------------------------- 1 | package cc.aoeiuv020.panovel.migration.impl 2 | 3 | import android.content.Context 4 | import cc.aoeiuv020.panovel.migration.Migration 5 | import cc.aoeiuv020.panovel.settings.AdSettings 6 | import cc.aoeiuv020.panovel.settings.GeneralSettings 7 | import cc.aoeiuv020.panovel.util.VersionName 8 | 9 | /** 10 | * 3.4.1添加专门的广告设置相关, 11 | * 老的广告开关迁移, 12 | */ 13 | class AdSettingsMigration : Migration() { 14 | override val to: VersionName = VersionName("3.4.1") 15 | override val message: String = "广告设置" 16 | 17 | override fun migrate(ctx: Context, from: VersionName) { 18 | GeneralSettings.sharedPreferences.all.forEach { (key, value) -> 19 | when (key) { 20 | "adEnabled" -> AdSettings.adEnabled = value as Boolean 21 | } 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /app/src/main/java/cc/aoeiuv020/panovel/migration/impl/DownloadMigration.kt: -------------------------------------------------------------------------------- 1 | package cc.aoeiuv020.panovel.migration.impl 2 | 3 | import android.content.Context 4 | import cc.aoeiuv020.panovel.migration.Migration 5 | import cc.aoeiuv020.panovel.settings.DownloadSettings 6 | import cc.aoeiuv020.panovel.settings.GeneralSettings 7 | import cc.aoeiuv020.panovel.util.VersionName 8 | 9 | /** 10 | * 3.2.4开始添加下载相关专用的设置页, 11 | * 原先在在GeneralSettings里的下载相关设置迁移到DownloadSettings里, 12 | * 13 | * Created by AoEiuV020 on 2018.11.11-12:27:38. 14 | */ 15 | class DownloadMigration : Migration() { 16 | override val to: VersionName = VersionName("3.2.4") 17 | override val message: String = "下载设置" 18 | 19 | override fun migrate(ctx: Context, from: VersionName) { 20 | GeneralSettings.sharedPreferences.all.forEach { (key, value) -> 21 | when (key) { 22 | "downloadThreadsLimit" -> DownloadSettings.downloadThreadsLimit = value as Int 23 | "downloadCount" -> DownloadSettings.downloadCount = value as Int 24 | "autoDownloadCount" -> DownloadSettings.autoDownloadCount = value as Int 25 | } 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /app/src/main/java/cc/aoeiuv020/panovel/migration/impl/SitesMigration.kt: -------------------------------------------------------------------------------- 1 | package cc.aoeiuv020.panovel.migration.impl 2 | 3 | import android.content.Context 4 | import cc.aoeiuv020.panovel.BuildConfig 5 | import cc.aoeiuv020.panovel.data.DataManager 6 | import cc.aoeiuv020.panovel.migration.Migration 7 | import cc.aoeiuv020.panovel.util.VersionName 8 | 9 | /** 10 | * Created by AoEiuV020 on 2018.05.28-11:31:31. 11 | */ 12 | class SitesMigration : Migration() { 13 | /** 14 | * 网站数据直接按最新的处理就好,版本号直接最新, 15 | * 不包含在其他小版本迁移中, 16 | */ 17 | override val to: VersionName = VersionName(BuildConfig.VERSION_NAME) 18 | override val message: String = "刷新支持的网站列表," 19 | 20 | override fun migrate(ctx: Context, from: VersionName) { 21 | // 同步所有网站信息到数据库, 22 | // 如果有网站不再支持,可以在后面加上删除指定条目, 23 | DataManager.syncSites() 24 | } 25 | } -------------------------------------------------------------------------------- /app/src/main/java/cc/aoeiuv020/panovel/qrcode/QrCodeManager.kt: -------------------------------------------------------------------------------- 1 | package cc.aoeiuv020.panovel.qrcode 2 | 3 | import java.net.URLEncoder 4 | 5 | /** 6 | * 二维码相关, 7 | * Created by AoEiuV020 on 2018.03.07-23:10:39. 8 | */ 9 | object QrCodeManager { 10 | /** 11 | * @return 生成二维码图片地址, 12 | */ 13 | fun generate(str: String): String { 14 | val data = URLEncoder.encode(str, "UTF-8") 15 | return "http://tool.oschina.net/action/qrcode/generate?data=$data&error=L&type=0&margin=4&size=4" 16 | } 17 | } -------------------------------------------------------------------------------- /app/src/main/java/cc/aoeiuv020/panovel/server/converter.kt: -------------------------------------------------------------------------------- 1 | package cc.aoeiuv020.panovel.server 2 | 3 | import cc.aoeiuv020.panovel.data.entity.Novel 4 | import cc.aoeiuv020.panovel.server.dal.model.autogen.Novel as ServerNovel 5 | 6 | /** 7 | * Created by AoEiuV020 on 2018.05.31-19:31:04. 8 | */ 9 | fun Novel.toServer() = ServerNovel().also { sn -> 10 | sn.site = site 11 | sn.author = author 12 | sn.name = name 13 | sn.detail = detail 14 | sn.chaptersCount = chaptersCount 15 | sn.receiveUpdateTime = receiveUpdateTime 16 | sn.checkUpdateTime = checkUpdateTime 17 | } -------------------------------------------------------------------------------- /app/src/main/java/cc/aoeiuv020/panovel/settings/ad.kt: -------------------------------------------------------------------------------- 1 | package cc.aoeiuv020.panovel.settings 2 | 3 | import cc.aoeiuv020.panovel.util.Delegates 4 | import cc.aoeiuv020.panovel.util.Pref 5 | 6 | object AdSettings : Pref { 7 | override val name: String 8 | get() = "Ad" 9 | 10 | // 提供彩蛋,满足条件就关闭广告,要在代码中改这个值,所以可变,var, 11 | var adEnabled: Boolean by Delegates.boolean(true) 12 | 13 | // 口袋工厂, https://www.13lm.com/ 14 | var middle13lmEnabled: Boolean by Delegates.boolean(false) 15 | } 16 | -------------------------------------------------------------------------------- /app/src/main/java/cc/aoeiuv020/panovel/settings/backup.kt: -------------------------------------------------------------------------------- 1 | package cc.aoeiuv020.panovel.settings 2 | 3 | import cc.aoeiuv020.panovel.util.Delegates 4 | import cc.aoeiuv020.panovel.util.Pref 5 | 6 | /** 7 | * Created by AoEiuV020 on 2021.04.25-13:42:29. 8 | */ 9 | object BackupSettings : Pref { 10 | override val name: String 11 | get() = "Backup" 12 | 13 | /** 14 | * 备份选中的单选框索引,最后一个其他就是-1, 15 | */ 16 | var checkedButtonIndex: Int by Delegates.int(0) 17 | 18 | // 几个开关, 19 | var cbBookshelf: Boolean by Delegates.boolean(true) 20 | var cbBookList: Boolean by Delegates.boolean(true) 21 | var cbProgress: Boolean by Delegates.boolean(true) 22 | var cbSettings: Boolean by Delegates.boolean(true) 23 | } -------------------------------------------------------------------------------- /app/src/main/java/cc/aoeiuv020/panovel/settings/disclaimer.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("DEPRECATION") 2 | 3 | package cc.aoeiuv020.panovel.settings 4 | 5 | import android.app.Fragment 6 | import android.os.Bundle 7 | import android.view.LayoutInflater 8 | import android.view.View 9 | import android.view.ViewGroup 10 | import cc.aoeiuv020.panovel.R 11 | import kotlinx.android.synthetic.main.content_disclaimer.* 12 | 13 | /** 14 | * 15 | * Created by AoEiuV020 on 2017.12.09-18:24:40. 16 | */ 17 | class DisclaimerFragment : Fragment() { 18 | override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View = inflater.inflate(R.layout.content_disclaimer, container, false) 19 | 20 | override fun onViewCreated(view: View?, savedInstanceState: Bundle?) { 21 | tvDisclaimer.text = activity.assets.open("Disclaimer.txt").reader().readText() 22 | } 23 | } -------------------------------------------------------------------------------- /app/src/main/java/cc/aoeiuv020/panovel/settings/download.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("DEPRECATION") 2 | 3 | package cc.aoeiuv020.panovel.settings 4 | 5 | import cc.aoeiuv020.panovel.util.Delegates 6 | import cc.aoeiuv020.panovel.util.Pref 7 | 8 | /** 9 | * Created by AoEiuV020 on 2018.11.11-11:52:46. 10 | */ 11 | object DownloadSettings : Pref { 12 | override val name: String 13 | get() = "Download" 14 | /** 15 | * 下载线程数, 16 | */ 17 | var downloadThreadsLimit: Int by Delegates.int(4) 18 | /** 19 | * 下载进度用通知方式展示进度, 20 | */ 21 | var downloadProgress: Boolean by Delegates.boolean(true) 22 | /** 23 | * 下载线程具体进度用通知方式展示进度, 24 | */ 25 | var downloadThreadProgress: Boolean by Delegates.boolean(false) 26 | /** 27 | * 点击下载时下载的章节数, 28 | * 0表示下载剩余全部, 29 | * -1表示每次询问, 30 | */ 31 | var downloadCount: Int by Delegates.int(-1) 32 | /** 33 | * 书架小说刷新章节列表后如果新增章节数小于等于该值就自动缓存新章节, 34 | */ 35 | var autoDownloadCount: Int by Delegates.int(2) 36 | 37 | } -------------------------------------------------------------------------------- /app/src/main/java/cc/aoeiuv020/panovel/settings/general.kt: -------------------------------------------------------------------------------- 1 | package cc.aoeiuv020.panovel.settings 2 | 3 | import cc.aoeiuv020.panovel.util.Delegates 4 | import cc.aoeiuv020.panovel.util.Pref 5 | 6 | /** 7 | * Created by AoEiuV020 on 2018.05.25-23:07:17. 8 | */ 9 | object GeneralSettings : Pref { 10 | override val name: String 11 | get() = "General" 12 | /** 13 | * 搜索线程数, 14 | */ 15 | var searchThreadsLimit: Int by Delegates.int(4) 16 | var historyCount: Int by Delegates.int(30) 17 | } 18 | -------------------------------------------------------------------------------- /app/src/main/java/cc/aoeiuv020/panovel/settings/interface.kt: -------------------------------------------------------------------------------- 1 | package cc.aoeiuv020.panovel.settings 2 | 3 | import cc.aoeiuv020.panovel.util.Delegates 4 | import cc.aoeiuv020.panovel.util.Pref 5 | 6 | /** 7 | * Created by AoEiuV020 on 2019.05.01-13:23:43. 8 | */ 9 | object InterfaceSettings : Pref { 10 | override val name: String 11 | get() = "Interface" 12 | /** 13 | * 标签居中显示, 14 | */ 15 | var tabGravityCenter: Boolean by Delegates.boolean(true) 16 | } -------------------------------------------------------------------------------- /app/src/main/java/cc/aoeiuv020/panovel/settings/location.kt: -------------------------------------------------------------------------------- 1 | package cc.aoeiuv020.panovel.settings 2 | 3 | import cc.aoeiuv020.panovel.backup.BackupPresenter 4 | import cc.aoeiuv020.panovel.data.CacheManager 5 | import cc.aoeiuv020.panovel.local.NovelExporter 6 | import cc.aoeiuv020.panovel.util.Delegates 7 | import cc.aoeiuv020.panovel.util.Pref 8 | 9 | /** 10 | * Created by AoEiuV020 on 2018.12.31-20:41:11. 11 | */ 12 | @Suppress("unused") 13 | object LocationSettings : Pref { 14 | override val name: String 15 | get() = "Location" 16 | var cacheLocation: String by Delegates.string(ctx.cacheDir.resolve(CacheManager.NAME_FOLDER).absolutePath) 17 | var backupLocation: String by Delegates.string(sdcardResolve(BackupPresenter.NAME_FOLDER)) 18 | var exportLocation: String by Delegates.string(sdcardResolve(NovelExporter.NAME_FOLDER)) 19 | 20 | // 优先SD卡,不可用就私有目录, 21 | private fun sdcardResolve(name: String): String = ( 22 | ctx.getExternalFilesDir(null) 23 | ?.resolve(name) 24 | ?.apply { exists() || mkdirs() } 25 | ?.takeIf { it.canWrite() } 26 | ?: ctx.filesDir.resolve(name) 27 | ).absolutePath 28 | 29 | } -------------------------------------------------------------------------------- /app/src/main/java/cc/aoeiuv020/panovel/settings/margins.kt: -------------------------------------------------------------------------------- 1 | package cc.aoeiuv020.panovel.settings 2 | 3 | import cc.aoeiuv020.panovel.util.Delegates 4 | import cc.aoeiuv020.panovel.util.SubPref 5 | import cc.aoeiuv020.reader.ItemMargins 6 | 7 | /** 8 | * Created by AoEiuV020 on 2018.05.26-20:36:04. 9 | */ 10 | 11 | class Margins(subName: String, enabled: Boolean, 12 | left: Int, top: Int, right: Int, bottom: Int 13 | ) : SubPref(ReaderSettings, subName), ItemMargins { 14 | // 保存在App.ctx.packageName + "_ReaderSettings" + "_$name" 15 | /** 16 | * 对应的东西是否显示, 17 | * 除了小说内容,其他都支持不显示, 18 | */ 19 | override var enabled: Boolean by Delegates.boolean(enabled) 20 | override var left: Int by Delegates.int(left) 21 | override var top: Int by Delegates.int(top) 22 | override var right: Int by Delegates.int(right) 23 | override var bottom: Int by Delegates.int(bottom) 24 | override fun toString(): String { 25 | return "Margins($left, $top, $right, $bottom)" 26 | } 27 | } 28 | 29 | -------------------------------------------------------------------------------- /app/src/main/java/cc/aoeiuv020/panovel/settings/server.kt: -------------------------------------------------------------------------------- 1 | package cc.aoeiuv020.panovel.settings 2 | 3 | import cc.aoeiuv020.panovel.util.Delegates 4 | import cc.aoeiuv020.panovel.util.Pref 5 | 6 | /** 7 | * Created by AoEiuV020 on 2018.06.05-12:17:02. 8 | */ 9 | object ServerSettings : Pref { 10 | override val name: String 11 | get() = "Server" 12 | /** 13 | * 服务器地址,, 14 | */ 15 | var serverAddress: String by Delegates.string("") 16 | /** 17 | * 收到小说更新推送时是否弹出通知小说更新, 18 | */ 19 | var notifyNovelUpdate: Boolean by Delegates.boolean(true) 20 | /** 21 | * 更新通知只限置顶过的小说, 22 | */ 23 | var notifyPinnedOnly: Boolean by Delegates.boolean(false) 24 | /** 25 | * 更新推送的通知只保留最后一个, 26 | */ 27 | var singleNotification: Boolean by Delegates.boolean(true) 28 | /** 29 | * 是否询问服务器有无更新, 30 | * 并会刷新“上次刷新”为服务器上的时间, 31 | * 省流量用, 32 | */ 33 | var askUpdate: Boolean by Delegates.boolean(true) 34 | /** 35 | * 添加移出书架时会订阅更新提醒的推送,订阅成功时是否弹出toast提示, 36 | */ 37 | var subscriptToast: Boolean by Delegates.boolean(true) 38 | } 39 | -------------------------------------------------------------------------------- /app/src/main/java/cc/aoeiuv020/panovel/settings/site.kt: -------------------------------------------------------------------------------- 1 | package cc.aoeiuv020.panovel.settings 2 | 3 | import cc.aoeiuv020.panovel.util.Delegates 4 | import cc.aoeiuv020.panovel.util.Pref 5 | 6 | object SiteSettings : Pref { 7 | override val name: String 8 | get() = "Site" 9 | var cachedVersion: Int by Delegates.int(0) 10 | } 11 | -------------------------------------------------------------------------------- /app/src/main/java/cc/aoeiuv020/panovel/share/Expiration.kt: -------------------------------------------------------------------------------- 1 | package cc.aoeiuv020.panovel.share 2 | 3 | enum class Expiration(val value: String) { 4 | NONE(""), DAY("day"), MONTH("month"), YEAR("year") 5 | } -------------------------------------------------------------------------------- /app/src/main/java/cc/aoeiuv020/panovel/text/DispatchTouchFrameLayout.kt: -------------------------------------------------------------------------------- 1 | package cc.aoeiuv020.panovel.text 2 | 3 | import android.content.Context 4 | import android.util.AttributeSet 5 | import android.view.MotionEvent 6 | import android.widget.FrameLayout 7 | import org.jetbrains.anko.AnkoLogger 8 | import org.jetbrains.anko.verbose 9 | 10 | /** 11 | * 12 | * Created by AoEiuV020 on 2017.11.24-19:54:53. 13 | */ 14 | class DispatchTouchFrameLayout : FrameLayout, AnkoLogger { 15 | constructor(context: Context) 16 | : super(context) 17 | 18 | constructor(context: Context, attrs: AttributeSet) 19 | : super(context, attrs) 20 | 21 | constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) 22 | : super(context, attrs, defStyleAttr) 23 | 24 | var activity: NovelTextActivity? = null 25 | 26 | private var previousAction: Int = MotionEvent.ACTION_UP 27 | override fun dispatchTouchEvent(event: MotionEvent): Boolean { 28 | verbose { event } 29 | if (previousAction == MotionEvent.ACTION_DOWN 30 | && event.action == MotionEvent.ACTION_UP) { 31 | activity?.toggle() 32 | } 33 | previousAction = event.action 34 | return super.dispatchTouchEvent(event) 35 | } 36 | } -------------------------------------------------------------------------------- /app/src/main/java/cc/aoeiuv020/panovel/util/constants.kt: -------------------------------------------------------------------------------- 1 | package cc.aoeiuv020.panovel.util 2 | 3 | /** 4 | * Created by AoEiuV020 on 2018.05.23-15:22:44. 5 | */ 6 | -------------------------------------------------------------------------------- /app/src/main/java/cc/aoeiuv020/panovel/util/ext.kt: -------------------------------------------------------------------------------- 1 | package cc.aoeiuv020.panovel.util 2 | 3 | import cc.aoeiuv020.gson.type 4 | import cc.aoeiuv020.panovel.report.Reporter 5 | 6 | /** 7 | * Created by AoEiuV020 on 2018.05.24-12:22:17. 8 | */ 9 | 10 | inline fun T?.notNullOrReport(): T = 11 | notNullOrReport(type().toString()) 12 | 13 | fun T?.notNullOrReport(value: String): T = 14 | Reporter.notNullOrReport(this, value) 15 | -------------------------------------------------------------------------------- /app/src/main/java/cc/aoeiuv020/panovel/util/file.kt: -------------------------------------------------------------------------------- 1 | package cc.aoeiuv020.panovel.util 2 | 3 | import android.content.Context 4 | 5 | /** 6 | * 文件操作相关, 7 | * Created by AoEiuV020 on 2017.11.23-11:27:40. 8 | */ 9 | fun Context.assetsRead(name: String): String = assets.open(name).reader().readText() -------------------------------------------------------------------------------- /app/src/main/java/cc/aoeiuv020/panovel/util/version.kt: -------------------------------------------------------------------------------- 1 | package cc.aoeiuv020.panovel.util 2 | 3 | /** 4 | * app版本名相关的封装, 5 | * 6 | * Created by AoEiuV020 on 2018.05.17-15:53:29. 7 | */ 8 | 9 | class VersionName( 10 | val name: String 11 | ) : Comparable { 12 | override fun compareTo(other: VersionName): Int { 13 | return VersionUtil.compare(name, other.name) 14 | } 15 | 16 | override fun equals(other: Any?): Boolean { 17 | if (other == null || other !is VersionName) { 18 | return false 19 | } 20 | return VersionUtil.compare(name, other.name) == 0 21 | } 22 | 23 | override fun hashCode(): Int { 24 | return name.hashCode() 25 | } 26 | 27 | override fun toString(): String { 28 | return name 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/background_circle.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 12 | 13 | 18 | 19 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/circle_dot.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_add.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_block.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_close.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_color_lens.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_favorite.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_favorite_border.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_favorite_selector.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_info.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_jump_qidian_blocked.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_list.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_more_vert.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_open.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_open_in_browser.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_read.xml: -------------------------------------------------------------------------------- 1 | 7 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_refresh.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_search.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_settings.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/novel_name_background_dark.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/splash_bg.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_book_list.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_download.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_qidianshuju.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 13 | 14 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_qidianshuju_list.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 14 | 15 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_qidianshuju_post.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 14 | 15 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_site_choose.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 24 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_sp7_list.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 14 | 15 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /app/src/main/res/layout/dialog_editor.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 21 | -------------------------------------------------------------------------------- /app/src/main/res/layout/dialog_seekbar.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 21 | -------------------------------------------------------------------------------- /app/src/main/res/layout/item_ad.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /app/src/main/res/layout/item_qidiantu_post.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 14 | 15 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /app/src/main/res/layout/item_test_ad.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 13 | 14 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /app/src/main/res/layout/novel_item_list.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 13 | 14 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /app/src/main/res/layout/novel_text_item.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 14 | 15 | 19 | 20 | -------------------------------------------------------------------------------- /app/src/main/res/menu/menu_book_list.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/menu/menu_detail.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 10 | 15 | 19 | -------------------------------------------------------------------------------- /app/src/main/res/menu/menu_find.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 11 | 12 | 18 | 19 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /app/src/main/res/menu/menu_fuzzy_search.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/menu/menu_qidianshuju_list.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 11 | 12 | 18 | 19 | -------------------------------------------------------------------------------- /app/src/main/res/menu/menu_qidianshuju_post.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/menu/menu_single_search.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 10 | 11 | 17 | 18 | 24 | 25 | 31 | 32 | 36 | 37 | -------------------------------------------------------------------------------- /app/src/main/res/menu/menu_site_list.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/menu/menu_text.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 10 | 16 | 22 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AoEiuV020/PaNovel/48fc2248521cfeb52ac9fbcbb9a09e2fc6e0e95d/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AoEiuV020/PaNovel/48fc2248521cfeb52ac9fbcbb9a09e2fc6e0e95d/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AoEiuV020/PaNovel/48fc2248521cfeb52ac9fbcbb9a09e2fc6e0e95d/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AoEiuV020/PaNovel/48fc2248521cfeb52ac9fbcbb9a09e2fc6e0e95d/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AoEiuV020/PaNovel/48fc2248521cfeb52ac9fbcbb9a09e2fc6e0e95d/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AoEiuV020/PaNovel/48fc2248521cfeb52ac9fbcbb9a09e2fc6e0e95d/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AoEiuV020/PaNovel/48fc2248521cfeb52ac9fbcbb9a09e2fc6e0e95d/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AoEiuV020/PaNovel/48fc2248521cfeb52ac9fbcbb9a09e2fc6e0e95d/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AoEiuV020/PaNovel/48fc2248521cfeb52ac9fbcbb9a09e2fc6e0e95d/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AoEiuV020/PaNovel/48fc2248521cfeb52ac9fbcbb9a09e2fc6e0e95d/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AoEiuV020/PaNovel/48fc2248521cfeb52ac9fbcbb9a09e2fc6e0e95d/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AoEiuV020/PaNovel/48fc2248521cfeb52ac9fbcbb9a09e2fc6e0e95d/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AoEiuV020/PaNovel/48fc2248521cfeb52ac9fbcbb9a09e2fc6e0e95d/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AoEiuV020/PaNovel/48fc2248521cfeb52ac9fbcbb9a09e2fc6e0e95d/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AoEiuV020/PaNovel/48fc2248521cfeb52ac9fbcbb9a09e2fc6e0e95d/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap/donate_alipay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AoEiuV020/PaNovel/48fc2248521cfeb52ac9fbcbb9a09e2fc6e0e95d/app/src/main/res/mipmap/donate_alipay.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap/donate_alipay_red_packet.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AoEiuV020/PaNovel/48fc2248521cfeb52ac9fbcbb9a09e2fc6e0e95d/app/src/main/res/mipmap/donate_alipay_red_packet.jpg -------------------------------------------------------------------------------- /app/src/main/res/mipmap/donate_paypal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AoEiuV020/PaNovel/48fc2248521cfeb52ac9fbcbb9a09e2fc6e0e95d/app/src/main/res/mipmap/donate_paypal.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap/donate_wechatpay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AoEiuV020/PaNovel/48fc2248521cfeb52ac9fbcbb9a09e2fc6e0e95d/app/src/main/res/mipmap/donate_wechatpay.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap/no_cover.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AoEiuV020/PaNovel/48fc2248521cfeb52ac9fbcbb9a09e2fc6e0e95d/app/src/main/res/mipmap/no_cover.jpg -------------------------------------------------------------------------------- /app/src/main/res/mipmap/qrcode_wechatpay.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AoEiuV020/PaNovel/48fc2248521cfeb52ac9fbcbb9a09e2fc6e0e95d/app/src/main/res/mipmap/qrcode_wechatpay.jpg -------------------------------------------------------------------------------- /app/src/main/res/values-v21/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /app/src/main/res/values-w820dp/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/values/attrs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16dp 4 | 8dp 5 | 30dp 6 | 18sp 7 | 60dp 8 | 8dp 9 | -------------------------------------------------------------------------------- /app/src/main/res/values/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FF0000 4 | -------------------------------------------------------------------------------- /app/src/main/res/values/ids.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/xml/backup_descriptor.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/xml/pref_ad.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/xml/pref_cache_clear.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 11 | 12 | 15 | 16 | 19 | 20 | 23 | -------------------------------------------------------------------------------- /app/src/main/res/xml/pref_general.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 13 | 14 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /app/src/main/res/xml/pref_interface.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/xml/pref_location.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 10 | 11 | 17 | 18 | 24 | 25 | -------------------------------------------------------------------------------- /app/src/test/java/cc/aoeiuv020/panovel/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package cc.aoeiuv020.panovel 2 | 3 | import org.junit.Test 4 | 5 | import org.junit.Assert.* 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * See [testing documentation](http://d.android.com/tools/testing). 11 | */ 12 | class ExampleUnitTest { 13 | @Test 14 | fun addition_isCorrect() { 15 | assertEquals(4, 2 + 2) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /app/src/test/java/cc/aoeiuv020/panovel/RegexUnitTest.kt: -------------------------------------------------------------------------------- 1 | package cc.aoeiuv020.panovel 2 | 3 | import org.junit.Assert.assertEquals 4 | import org.junit.Assert.assertTrue 5 | import org.junit.Test 6 | import java.util.regex.Pattern 7 | 8 | /** 9 | * Created by AoEiuV020 on 2018.05.16-19:57:13. 10 | */ 11 | class RegexUnitTest { 12 | private val withoutEscapeRegex = Regex("]") 13 | private val withEscapeRegex = Regex("\\]") 14 | private val str = "]" 15 | @Test 16 | fun withTest() { 17 | assertTrue(str.matches(withEscapeRegex)) 18 | } 19 | 20 | @Test 21 | fun withoutTest() { 22 | assertTrue(str.matches(withoutEscapeRegex)) 23 | } 24 | 25 | /** 26 | * 正则里\Q和\E之间的所有不转义, 27 | */ 28 | @Test 29 | fun quote() { 30 | val str = "[]" 31 | val quoteStr = Pattern.quote(str) 32 | assertEquals("\\Q[]\\E", quoteStr) 33 | assertTrue(str.matches(Regex(quoteStr))) 34 | } 35 | } -------------------------------------------------------------------------------- /app/src/test/java/cc/aoeiuv020/panovel/share/PasteUbuntuTest.kt: -------------------------------------------------------------------------------- 1 | package cc.aoeiuv020.panovel.share 2 | 3 | import org.junit.Assert.assertEquals 4 | import org.junit.Assert.assertTrue 5 | import org.junit.Before 6 | import org.junit.Test 7 | 8 | /** 9 | * 10 | * Created by AoEiuV020 on 2018.03.07-19:31:38. 11 | */ 12 | class PasteUbuntuTest { 13 | private lateinit var paste: PasteUbuntu 14 | private val text = "PasteUbuntuTest" 15 | @Before 16 | fun setUp() { 17 | paste = PasteUbuntu() 18 | } 19 | 20 | @Test 21 | fun upload() { 22 | val link = paste.upload(PasteUbuntu.PasteUbuntuData(text, expiration = Expiration.DAY)) 23 | println(link) 24 | assertTrue(link.matches(Regex("https://paste.ubuntu.com/p/\\w*/"))) 25 | val receive = paste.download(link) 26 | assertEquals(text, receive) 27 | } 28 | 29 | @Test 30 | fun download() { 31 | val receive = paste.download("https://paste.ubuntu.com/p/CH3g747q9S/") 32 | assertEquals("""{ 33 | "list": [ 34 | { 35 | "author": "二目", 36 | "name": "放开那个女巫", 37 | "requester": { 38 | "type": "cc.aoeiuv020.panovel.api.DetailRequester", 39 | "extra": "https://book.qidian.com/info/1003306811" 40 | }, 41 | "site": "起点中文" 42 | } 43 | ], 44 | "name": "bs" 45 | }""", receive) 46 | } 47 | 48 | } -------------------------------------------------------------------------------- /app/src/test/java/cc/aoeiuv020/panovel/util/VersionNameTest.kt: -------------------------------------------------------------------------------- 1 | package cc.aoeiuv020.panovel.util 2 | 3 | import org.junit.Assert.assertTrue 4 | import org.junit.Test 5 | 6 | /** 7 | * Created by AoEiuV020 on 2018.05.17-15:56:59. 8 | */ 9 | class VersionNameTest { 10 | 11 | @Test 12 | fun compareTo() { 13 | assertTrue("0".v == "0".v) 14 | assertTrue("1.0.8".v < "1.1.1".v) 15 | assertTrue("1.1.2".v < "1.1.12".v) 16 | assertTrue("1.1.2".v < "1.2".v) 17 | assertTrue("2.2.2".v > "1.3.1".v) 18 | assertTrue("2.2.2".v > "2.2".v) 19 | assertTrue("3.4.5.1".v > "3.4.5-2022".v) 20 | assertTrue("3.4.5".v > "3.4.5-2022".v) 21 | assertTrue("3.4.5-2022".v > "3.4.5-2021".v) 22 | assertTrue("3.4.5-2022".v > "3.4.4".v) 23 | assertTrue("3.4.5-2022".v > "3.4.4.4".v) 24 | } 25 | 26 | private val String.v get() = VersionName(this) 27 | } -------------------------------------------------------------------------------- /baseJar/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /baseJar/build.gradle: -------------------------------------------------------------------------------- 1 | dependencies { 2 | api 'cc.aoeiuv020:gson:' + a_gson_version 3 | api 'cc.aoeiuv020:jsonpath:' + a_jsonpath_version 4 | api 'cc.aoeiuv020:okhttp:' + a_okhttp_version 5 | api 'org.jsoup:jsoup:' + jsoup_version 6 | } 7 | -------------------------------------------------------------------------------- /baseJar/src/main/java/cc/aoeiuv020/base/jar/date.kt: -------------------------------------------------------------------------------- 1 | package cc.aoeiuv020.base.jar 2 | 3 | import java.util.* 4 | import java.util.concurrent.TimeUnit.DAYS 5 | 6 | /** 7 | * Created by AoEiuV020 on 2018.05.31-22:35:08. 8 | */ 9 | 10 | /** 11 | * 判断时间是否有效, 12 | * 考虑到时区问题,不能直接判断是否等于0, 13 | * 改为判断是否小于一天, 14 | * 大于一天的有效, 15 | */ 16 | fun Date.notZero(): Date? = this.takeIf { time > DAYS.toMillis(1) } -------------------------------------------------------------------------------- /baseJar/src/main/java/cc/aoeiuv020/base/jar/image.kt: -------------------------------------------------------------------------------- 1 | package cc.aoeiuv020.base.jar 2 | 3 | /** 4 | * Created by AoEiuV020 on 2019.02.11-19:16:32. 5 | */ 6 | object ImageUtil { 7 | fun getImageFromUrl(url: String): String = "![img]($url)" 8 | } -------------------------------------------------------------------------------- /baseJar/src/main/java/cc/aoeiuv020/base/jar/io/io.kt: -------------------------------------------------------------------------------- 1 | package cc.aoeiuv020.base.jar.io 2 | 3 | import java.util.* 4 | 5 | /** 6 | * Created by AoEiuV020 on 2018.06.13-19:12:58. 7 | */ 8 | 9 | fun BufferedRandomAccessFile.readLines(beginPos: Long, endPos: Long, charset: String): List { 10 | seek(beginPos) 11 | val list = LinkedList() 12 | while (filePointer < endPos) { 13 | @Suppress("DEPRECATION") 14 | readLine(charset)?.let { 15 | list.add(it) 16 | } 17 | } 18 | return list 19 | } 20 | -------------------------------------------------------------------------------- /baseJar/src/main/java/cc/aoeiuv020/base/jar/thread.kt: -------------------------------------------------------------------------------- 1 | package cc.aoeiuv020.base.jar 2 | 3 | import java.util.concurrent.ExecutorService 4 | import java.util.concurrent.Executors 5 | 6 | /** 7 | * Created by AoEiuV020 on 2018.06.01-15:32:50. 8 | */ 9 | /** 10 | * 供大量io异步操作使用, 11 | * 繁重的异步任务不能和其他简单异步混用线程池,可能导致阻塞, 12 | * 比如anko-common的doAsync默认使用anko自带的BackgroundExecutor, 13 | * 如果下载线程过多,任务过重,会导致anko的BackgroundExecutor阻塞,影响其他异步操作, 14 | */ 15 | val ioExecutorService: ExecutorService by lazy { 16 | Executors.newCachedThreadPool() 17 | } 18 | -------------------------------------------------------------------------------- /baseJar/src/main/java/cc/aoeiuv020/base/jar/url.kt: -------------------------------------------------------------------------------- 1 | package cc.aoeiuv020.base.jar 2 | 3 | import java.net.MalformedURLException 4 | import java.net.URL 5 | 6 | /** 7 | * Created by AoEiuV020 on 2018.09.09-02:39:00. 8 | */ 9 | /** 10 | * 地址仅路径,斜杆/开头, 11 | */ 12 | fun path(url: String): String = try { 13 | URL(url).path 14 | } catch (e: MalformedURLException) { 15 | url 16 | } 17 | -------------------------------------------------------------------------------- /bump-version.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # ./bump-version 下个版本, 3 | # 改版本号和版本名,然后提交, 4 | set -e 5 | old=$PWD 6 | cd $(dirname $0) 7 | project=$(pwd) 8 | buildGradleFile="$project/build.gradle" 9 | versionFile="$project/version.properties" 10 | 11 | versionCode=$(sed -n 's/\s*version_code\s*=\s*\(\S*\)/\1/p' $versionFile) 12 | versionCode=$(expr $versionCode + 1) 13 | 14 | sed -i "s/version_code\\s*=\\s*[0-9]*/version_code=$versionCode/" $versionFile 15 | 16 | versionName=$1 17 | sed -i "s/version_name\\s*=\\s*.*/version_name=$versionName/" $versionFile 18 | changeLogFile=app/src/main/assets/ChangeLog.txt 19 | sed -i "1G" $changeLogFile 20 | sed -i "2i$versionName:" $changeLogFile 21 | 22 | git add $versionFile 23 | git add $changeLogFile 24 | git commit -m "Bumped version number to $versionName" 25 | 26 | cd $old 27 | -------------------------------------------------------------------------------- /filepicker/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /filepicker/build.gradle: -------------------------------------------------------------------------------- 1 | dependencies { 2 | } 3 | publish { 4 | licences = ["Apache-2.0"] 5 | } 6 | 7 | -------------------------------------------------------------------------------- /filepicker/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 D:\Android\SDK/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 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | -------------------------------------------------------------------------------- /filepicker/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /filepicker/src/main/java/cc/aoeiuv020/filepicker/controller/DialogSelectionListener.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2016 Angad Singh 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package cc.aoeiuv020.filepicker.controller; 18 | 19 | /*

20 | * Created by Angad Singh on 10-07-2016. 21 | *

22 | */ 23 | 24 | /** 25 | * Interface definition for a callback to be invoked 26 | * when dialog selects files. 27 | */ 28 | public interface DialogSelectionListener { 29 | 30 | /** 31 | * The method is called when files or directories are selected. 32 | * 33 | * @param files The array of String containing selected file paths. 34 | */ 35 | void onSelectedFilePaths(String files[]); 36 | } 37 | -------------------------------------------------------------------------------- /filepicker/src/main/java/cc/aoeiuv020/filepicker/controller/NotifyItemChecked.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2016 Angad Singh 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package cc.aoeiuv020.filepicker.controller; 18 | 19 | /* 20 | *

21 | * Created by Angad Singh on 11-07-2016. 22 | *

23 | */ 24 | 25 | /** 26 | * Interface definition for a callback to be invoked 27 | * when a checkbox is checked. 28 | */ 29 | public interface NotifyItemChecked { 30 | 31 | /** 32 | * Called when a checkbox is checked. 33 | */ 34 | void notifyCheckBoxIsClicked(); 35 | } 36 | -------------------------------------------------------------------------------- /filepicker/src/main/java/cc/aoeiuv020/filepicker/widget/OnCheckedChangeListener.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2017 Angad Singh 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package cc.aoeiuv020.filepicker.widget; 18 | 19 | /** 20 | *

21 | * Created by Angad on 20-05-2017. 22 | *

23 | */ 24 | 25 | public interface OnCheckedChangeListener { 26 | void onCheckedChanged(MaterialCheckbox checkbox, boolean isChecked); 27 | } 28 | -------------------------------------------------------------------------------- /filepicker/src/main/res/anim/marked_item_animation.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /filepicker/src/main/res/anim/unmarked_item_animation.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /filepicker/src/main/res/drawable-hdpi/ic_add.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AoEiuV020/PaNovel/48fc2248521cfeb52ac9fbcbb9a09e2fc6e0e95d/filepicker/src/main/res/drawable-hdpi/ic_add.png -------------------------------------------------------------------------------- /filepicker/src/main/res/drawable-mdpi/ic_add.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AoEiuV020/PaNovel/48fc2248521cfeb52ac9fbcbb9a09e2fc6e0e95d/filepicker/src/main/res/drawable-mdpi/ic_add.png -------------------------------------------------------------------------------- /filepicker/src/main/res/drawable-xhdpi/ic_add.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AoEiuV020/PaNovel/48fc2248521cfeb52ac9fbcbb9a09e2fc6e0e95d/filepicker/src/main/res/drawable-xhdpi/ic_add.png -------------------------------------------------------------------------------- /filepicker/src/main/res/drawable-xxhdpi/ic_add.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AoEiuV020/PaNovel/48fc2248521cfeb52ac9fbcbb9a09e2fc6e0e95d/filepicker/src/main/res/drawable-xxhdpi/ic_add.png -------------------------------------------------------------------------------- /filepicker/src/main/res/drawable-xxxhdpi/ic_add.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AoEiuV020/PaNovel/48fc2248521cfeb52ac9fbcbb9a09e2fc6e0e95d/filepicker/src/main/res/drawable-xxxhdpi/ic_add.png -------------------------------------------------------------------------------- /filepicker/src/main/res/drawable/bottom_shadow.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AoEiuV020/PaNovel/48fc2248521cfeb52ac9fbcbb9a09e2fc6e0e95d/filepicker/src/main/res/drawable/bottom_shadow.9.png -------------------------------------------------------------------------------- /filepicker/src/main/res/layout/dialog_file_list.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | -------------------------------------------------------------------------------- /filepicker/src/main/res/layout/dialog_input_text.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 14 | 15 | -------------------------------------------------------------------------------- /filepicker/src/main/res/layout/dialog_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 12 | 13 | 22 | 23 | 26 | -------------------------------------------------------------------------------- /filepicker/src/main/res/mipmap-hdpi/ic_directory_parent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AoEiuV020/PaNovel/48fc2248521cfeb52ac9fbcbb9a09e2fc6e0e95d/filepicker/src/main/res/mipmap-hdpi/ic_directory_parent.png -------------------------------------------------------------------------------- /filepicker/src/main/res/mipmap-hdpi/ic_type_file.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AoEiuV020/PaNovel/48fc2248521cfeb52ac9fbcbb9a09e2fc6e0e95d/filepicker/src/main/res/mipmap-hdpi/ic_type_file.png -------------------------------------------------------------------------------- /filepicker/src/main/res/mipmap-hdpi/ic_type_folder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AoEiuV020/PaNovel/48fc2248521cfeb52ac9fbcbb9a09e2fc6e0e95d/filepicker/src/main/res/mipmap-hdpi/ic_type_folder.png -------------------------------------------------------------------------------- /filepicker/src/main/res/mipmap-mdpi/ic_directory_parent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AoEiuV020/PaNovel/48fc2248521cfeb52ac9fbcbb9a09e2fc6e0e95d/filepicker/src/main/res/mipmap-mdpi/ic_directory_parent.png -------------------------------------------------------------------------------- /filepicker/src/main/res/mipmap-mdpi/ic_type_file.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AoEiuV020/PaNovel/48fc2248521cfeb52ac9fbcbb9a09e2fc6e0e95d/filepicker/src/main/res/mipmap-mdpi/ic_type_file.png -------------------------------------------------------------------------------- /filepicker/src/main/res/mipmap-mdpi/ic_type_folder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AoEiuV020/PaNovel/48fc2248521cfeb52ac9fbcbb9a09e2fc6e0e95d/filepicker/src/main/res/mipmap-mdpi/ic_type_folder.png -------------------------------------------------------------------------------- /filepicker/src/main/res/mipmap-xhdpi/ic_directory_parent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AoEiuV020/PaNovel/48fc2248521cfeb52ac9fbcbb9a09e2fc6e0e95d/filepicker/src/main/res/mipmap-xhdpi/ic_directory_parent.png -------------------------------------------------------------------------------- /filepicker/src/main/res/mipmap-xhdpi/ic_type_file.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AoEiuV020/PaNovel/48fc2248521cfeb52ac9fbcbb9a09e2fc6e0e95d/filepicker/src/main/res/mipmap-xhdpi/ic_type_file.png -------------------------------------------------------------------------------- /filepicker/src/main/res/mipmap-xhdpi/ic_type_folder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AoEiuV020/PaNovel/48fc2248521cfeb52ac9fbcbb9a09e2fc6e0e95d/filepicker/src/main/res/mipmap-xhdpi/ic_type_folder.png -------------------------------------------------------------------------------- /filepicker/src/main/res/mipmap-xxhdpi/ic_directory_parent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AoEiuV020/PaNovel/48fc2248521cfeb52ac9fbcbb9a09e2fc6e0e95d/filepicker/src/main/res/mipmap-xxhdpi/ic_directory_parent.png -------------------------------------------------------------------------------- /filepicker/src/main/res/mipmap-xxhdpi/ic_type_file.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AoEiuV020/PaNovel/48fc2248521cfeb52ac9fbcbb9a09e2fc6e0e95d/filepicker/src/main/res/mipmap-xxhdpi/ic_type_file.png -------------------------------------------------------------------------------- /filepicker/src/main/res/mipmap-xxhdpi/ic_type_folder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AoEiuV020/PaNovel/48fc2248521cfeb52ac9fbcbb9a09e2fc6e0e95d/filepicker/src/main/res/mipmap-xxhdpi/ic_type_folder.png -------------------------------------------------------------------------------- /filepicker/src/main/res/values-de/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Abbrechen 4 | Auswählen 5 | Verzeichnis hoch 6 | Zuletzt bearbeitet: 7 | Verzeichnis kann nicht zugegriffen werden 8 | ordner erstellen 9 | ordner namen 10 | -------------------------------------------------------------------------------- /filepicker/src/main/res/values-es/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Canclear 4 | Seleccionar 5 | Directorio Padre 6 | Ult. edición: 7 | No se puede acceder al directorio 8 | Crear carpeta 9 | Nombre de carpeta 10 | 11 | -------------------------------------------------------------------------------- /filepicker/src/main/res/values-fr/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Annuler 4 | Choisir 5 | Répertoire parent 6 | Dernière modification: 7 | L\'annuaire ne peut pas être consulté 8 | créer un dossier 9 | nom de fichier 10 | -------------------------------------------------------------------------------- /filepicker/src/main/res/values-ru/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Отмена 4 | Выбрать 5 | Родительская директория 6 | Последние изменения: 7 | Директория недоступна 8 | создать папку 9 | имя папки 10 | 11 | -------------------------------------------------------------------------------- /filepicker/src/main/res/values-w820dp/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 8dp 3 | 16dp 4 | 32dp 5 | 32dp 6 | 7 | -------------------------------------------------------------------------------- /filepicker/src/main/res/values-zh-rCN/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 取消 4 | 选择 5 | 上层资料夹 6 | 最后修改: 7 | 文件夹无法访问 8 | 新建文件夹 9 | 文件夹名称 10 | -------------------------------------------------------------------------------- /filepicker/src/main/res/values-zh-rHK/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 取消 4 | 選擇 5 | 上層資料夾 6 | 最後修改: 7 | 文件夾無法訪問 8 | 新建文件夾 9 | 文件夾名稱 10 | -------------------------------------------------------------------------------- /filepicker/src/main/res/values-zh-rTW/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 取消 4 | 選擇 5 | 上層資料夾 6 | 最後修改: 7 | 文件夾無法訪問 8 | 新建文件夾 9 | 文件夾名稱 10 | -------------------------------------------------------------------------------- /filepicker/src/main/res/values/attrs.xml: -------------------------------------------------------------------------------- 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 | -------------------------------------------------------------------------------- /filepicker/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #3F51B5 4 | #303F9F 5 | #3F51B5 6 | #FFFFFF 7 | #FFFFFF 8 | @color/colorPrimary 9 | 10 | -------------------------------------------------------------------------------- /filepicker/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 4dp 3 | 8dp 4 | 16dp 5 | 24dp 6 | 7 | -------------------------------------------------------------------------------- /filepicker/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Cancel 4 | Select 5 | /sdcard 6 | Parent Directory 7 | Last edited: 8 | Directory cannot be accessed 9 | ... 10 | Create Folder 11 | Folder Name 12 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | 3 | # IDE (e.g. Android Studio) users: 4 | # Gradle settings configured through the IDE *will override* 5 | # any settings specified in this file. 6 | 7 | # For more details on how to configure your build environment visit 8 | # http://www.gradle.org/docs/current/userguide/build_environment.html 9 | 10 | # Specifies the JVM arguments used for the daemon process. 11 | # The setting is particularly useful for tweaking memory settings. 12 | android.enableJetifier=true 13 | android.useAndroidX=true 14 | org.gradle.jvmargs=-Xmx1536m 15 | 16 | # When configured, Gradle will run in incubating parallel mode. 17 | # This option should only be used with decoupled projects. More details, visit 18 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 19 | # org.gradle.parallel=true 20 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AoEiuV020/PaNovel/48fc2248521cfeb52ac9fbcbb9a09e2fc6e0e95d/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Mon Apr 08 13:21:29 CST 2019 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.1-all.zip 7 | -------------------------------------------------------------------------------- /js/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /js/build.gradle: -------------------------------------------------------------------------------- 1 | dependencies { 2 | implementation 'org.mozilla:rhino:' + rhino_version 3 | } 4 | -------------------------------------------------------------------------------- /js/src/main/java/cc/aoeiuv020/js/JsContext.kt: -------------------------------------------------------------------------------- 1 | package cc.aoeiuv020.js 2 | 3 | import org.mozilla.javascript.Context 4 | import org.mozilla.javascript.ScriptableObject 5 | 6 | /** 7 | * Created by AoEiuV020 on 2019.02.11-18:08:04. 8 | */ 9 | class JsContext private constructor( 10 | private val ctx: Context 11 | ) { 12 | companion object { 13 | internal fun create(ctx: Context): JsContext = JsContext(ctx) 14 | } 15 | 16 | private val scope: ScriptableObject = ctx.initStandardObjects() 17 | 18 | fun run(js: String): String { 19 | val result = ctx.evaluateString(scope, js, "", 1, null) 20 | return Context.toString(result) 21 | } 22 | 23 | } -------------------------------------------------------------------------------- /js/src/main/java/cc/aoeiuv020/js/JsUtil.kt: -------------------------------------------------------------------------------- 1 | package cc.aoeiuv020.js 2 | 3 | import org.mozilla.javascript.Context 4 | 5 | /** 6 | * Created by AoEiuV020 on 2019.02.11-16:48:42. 7 | */ 8 | object JsUtil { 9 | private fun getContext() = Context.enter().apply { 10 | // 禁用这个优化才能在安卓使用, 11 | optimizationLevel = -1 12 | } 13 | fun run(js: String): String { 14 | val ctx = getContext() 15 | val scope = ctx.initStandardObjects() 16 | val result = ctx.evaluateString(scope, js, "", 1, null) 17 | return Context.toString(result) 18 | } 19 | 20 | fun create(): JsContext = JsContext.create(getContext()) 21 | 22 | fun release() = Context.exit() 23 | } -------------------------------------------------------------------------------- /latest-changelog.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 打印最新版本的更新日志, 3 | # 关键是以markdown的格式加粗第一行, 4 | # 可选参数一个,指定版本打印更新日志, 5 | set -e 6 | cd $(dirname $0) 7 | versionName=$1 8 | versionName=${versionName:=$(./latest-version.sh)} 9 | changeLogFile=app/src/main/assets/ChangeLog.txt 10 | cat $changeLogFile |sed -n "/$versionName:/,\$p" |sed '/^$/,$d;1d' 11 | -------------------------------------------------------------------------------- /latest-version.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | cur="$(dirname $0)" 3 | cd $cur 4 | changeLogFile=app/src/main/assets/ChangeLog.txt 5 | cat $changeLogFile |head -2 |tail -1 |sed 's/\(.*\):/\1/' 6 | -------------------------------------------------------------------------------- /local/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /local/build.gradle: -------------------------------------------------------------------------------- 1 | dependencies { 2 | implementation project(':baseJar') 3 | implementation 'net.sourceforge.jchardet:jchardet:1.0' 4 | implementation 'cc.aoeiuv020:epublib-core:' + a_epublib_version 5 | implementation 'org.jsoup:jsoup:' + jsoup_version 6 | } 7 | -------------------------------------------------------------------------------- /local/src/main/java/cc/aoeiuv020/panovel/local/ContentProvider.kt: -------------------------------------------------------------------------------- 1 | package cc.aoeiuv020.panovel.local 2 | 3 | import java.io.InputStream 4 | import java.net.URL 5 | 6 | /** 7 | * Created by AoEiuV020 on 2018.06.19-20:54:49. 8 | */ 9 | interface ContentProvider { 10 | fun getNovelContent(chapter: LocalNovelChapter): List 11 | 12 | // 封面默认存完整url, 但是epub要存包内相对路径,否则针对临时文件解析的图片不可用, 13 | fun getImage(extra: String): URL = URL(extra) 14 | 15 | // 从url中拿输入流,可以继承为后判断是否要提供这张图片,比如导出时没缓存的图片就不导出了, 16 | fun openImage(url: URL): InputStream? = url.openStream() 17 | } -------------------------------------------------------------------------------- /local/src/main/java/cc/aoeiuv020/panovel/local/LocalNovelExpoter.kt: -------------------------------------------------------------------------------- 1 | package cc.aoeiuv020.panovel.local 2 | 3 | /** 4 | * Created by AoEiuV020 on 2018.06.19-22:21:16. 5 | */ 6 | interface LocalNovelExporter { 7 | fun export(info: LocalNovelInfo, contentProvider: ContentProvider, progressCallback: (Int, Int) -> Unit) 8 | } -------------------------------------------------------------------------------- /local/src/main/java/cc/aoeiuv020/panovel/local/LocalNovelParser.kt: -------------------------------------------------------------------------------- 1 | package cc.aoeiuv020.panovel.local 2 | 3 | import java.io.File 4 | 5 | /** 6 | * 调用parse方法解析后能拿出其他数据, 7 | * 8 | * Created by AoEiuV020 on 2018.06.15-19:06:15. 9 | */ 10 | abstract class LocalNovelParser( 11 | protected val file: File 12 | ) : ContentProvider { 13 | 14 | abstract fun parse(): LocalNovelInfo 15 | } -------------------------------------------------------------------------------- /local/src/main/java/cc/aoeiuv020/panovel/local/data.kt: -------------------------------------------------------------------------------- 1 | package cc.aoeiuv020.panovel.local 2 | 3 | /** 4 | * Created by AoEiuV020 on 2018.06.15-19:33:06. 5 | */ 6 | data class LocalNovelInfo( 7 | val author: String?, 8 | val name: String?, 9 | /** 10 | * 小说封面, 11 | */ 12 | val image: String?, 13 | val introduction: String?, 14 | /** 15 | * 章节列表不能null, 解析前可以给个空LinkedList, 16 | */ 17 | val chapters: List, 18 | val requester: String? 19 | ) 20 | 21 | /** 22 | * 本地小说的章节, 23 | */ 24 | data class LocalNovelChapter( 25 | /** 26 | * 章节名不包括小说名, 27 | */ 28 | val name: String, 29 | val extra: String 30 | ) 31 | 32 | /** 33 | * 本地小说的类型, 34 | */ 35 | enum class LocalNovelType( 36 | val suffix: String 37 | ) { 38 | TEXT(".txt"), EPUB(".epub") 39 | } 40 | -------------------------------------------------------------------------------- /local/src/test/resources/zxcs.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AoEiuV020/PaNovel/48fc2248521cfeb52ac9fbcbb9a09e2fc6e0e95d/local/src/test/resources/zxcs.txt -------------------------------------------------------------------------------- /merge-version.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 自动建个release分支合并到master, 3 | # 然后删除这个分支, 4 | set -e 5 | old=$PWD 6 | cd $(dirname $0) 7 | project=$(pwd) 8 | buildGradleFile="$project/build.gradle" 9 | versionFile="$project/version.properties" 10 | 11 | versionName=$(sed -n 's/\s*version_name\s*=\s*\(\S*\)/\1/p' $versionFile) 12 | branch=release-$versionName 13 | changeLog=$(./latest-changelog.sh $versionName) 14 | mergeLog=$(echo "Merge branch '$branch'\n\n$changeLog") 15 | tagLog=$(echo "$changeLog" |sed '1s/[,,]$//;1s/^\(.*\)$/\1\n/') 16 | 17 | git checkout -b $branch 18 | git checkout master 19 | git merge --no-ff $branch -m "$mergeLog" 20 | git branch -d $branch 21 | git tag -a $versionName -m "$tagLog" 22 | git checkout dev 23 | git merge master 24 | 25 | -------------------------------------------------------------------------------- /pager/build.gradle: -------------------------------------------------------------------------------- 1 | dependencies { 2 | implementation 'androidx.appcompat:appcompat:' + androidx_version 3 | } 4 | -------------------------------------------------------------------------------- /pager/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /pager/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /pager/src/main/java/cc/aoeiuv020/pager/AnimMode.kt: -------------------------------------------------------------------------------- 1 | package cc.aoeiuv020.pager 2 | 3 | /** 4 | * 5 | * Created by AoEiuV020 on 2017.12.02-17:58:54. 6 | */ 7 | enum class AnimMode { 8 | SIMULATION, 9 | COVER, 10 | SLIDE, 11 | NONE, 12 | SCROLL, 13 | } -------------------------------------------------------------------------------- /pager/src/main/java/cc/aoeiuv020/pager/BlankPagerDrawer.kt: -------------------------------------------------------------------------------- 1 | package cc.aoeiuv020.pager 2 | 3 | import android.graphics.Canvas 4 | 5 | /** 6 | * 7 | * Created by AoEiuV020 on 2017.12.03-03:08:23. 8 | */ 9 | class BlankPagerDrawer : PagerDrawer() { 10 | override fun drawCurrentPage(background: Canvas, content: Canvas) { 11 | background.drawColor(0xffffffff.toInt()) 12 | } 13 | 14 | override fun scrollToPrev(): Boolean { 15 | return true 16 | } 17 | 18 | override fun scrollToNext(): Boolean { 19 | return true 20 | } 21 | } -------------------------------------------------------------------------------- /pager/src/main/java/cc/aoeiuv020/pager/IPagerDrawer.kt: -------------------------------------------------------------------------------- 1 | package cc.aoeiuv020.pager 2 | 3 | import android.graphics.Canvas 4 | 5 | /** 6 | * 7 | * Created by AoEiuV020 on 2017.12.02-17:58:54. 8 | */ 9 | interface IPagerDrawer { 10 | 11 | fun attach(pager: Pager, backgroundSize: Size, contentSize: Size) 12 | 13 | fun drawCurrentPage(background: Canvas, content: Canvas) 14 | 15 | fun scrollToPrev(): Boolean 16 | 17 | fun scrollToNext(): Boolean 18 | 19 | fun detach() 20 | } -------------------------------------------------------------------------------- /pager/src/main/java/cc/aoeiuv020/pager/PagerAnimation.kt: -------------------------------------------------------------------------------- 1 | package cc.aoeiuv020.pager 2 | 3 | import android.graphics.Canvas 4 | import android.view.MotionEvent 5 | 6 | /** 7 | * 8 | * Created by AoEiuV020 on 2017.12.03-03:05:20. 9 | */ 10 | interface PagerAnimation { 11 | fun draw(canvas: Canvas) 12 | fun scrollAnim() 13 | fun refresh() 14 | fun onTouchEvent(event: MotionEvent): Boolean 15 | /** 16 | * 滚动到下一页,不必支持, 17 | * @return 返回是否成功翻页, 18 | */ 19 | fun scrollNext(): Boolean 20 | 21 | /** 22 | * 滚动到下一页,不必支持, 23 | * 传入滚动动画的起始点, 24 | */ 25 | fun scrollNext(x: Float, y: Float): Boolean 26 | fun scrollPrev(): Boolean 27 | fun scrollPrev(x: Float, y: Float): Boolean 28 | fun setDurationMultiply(multiply: Float) 29 | } -------------------------------------------------------------------------------- /pager/src/main/java/cc/aoeiuv020/pager/PagerDirection.kt: -------------------------------------------------------------------------------- 1 | package cc.aoeiuv020.pager 2 | 3 | /** 4 | * 5 | * Created by AoEiuV020 on 2017.12.02-19:02:59. 6 | */ 7 | enum class PagerDirection { 8 | NONE, 9 | PREV, 10 | NEXT, 11 | } -------------------------------------------------------------------------------- /pager/src/main/java/cc/aoeiuv020/pager/PagerDrawer.kt: -------------------------------------------------------------------------------- 1 | package cc.aoeiuv020.pager 2 | 3 | /** 4 | * 5 | * Created by AoEiuV020 on 2017.12.07-23:54:07. 6 | */ 7 | abstract class PagerDrawer : IPagerDrawer { 8 | var pager: Pager? = null 9 | protected lateinit var backgroundSize: Size 10 | protected lateinit var contentSize: Size 11 | 12 | override fun attach(pager: Pager, backgroundSize: Size, contentSize: Size) { 13 | this.pager = pager 14 | this.backgroundSize = backgroundSize 15 | this.contentSize = contentSize 16 | } 17 | 18 | override fun detach() { 19 | this.pager = null 20 | } 21 | } -------------------------------------------------------------------------------- /pager/src/main/java/cc/aoeiuv020/pager/Size.kt: -------------------------------------------------------------------------------- 1 | package cc.aoeiuv020.pager 2 | 3 | /** 4 | * 5 | * Created by AoEiuV020 on 2017.12.08-00:22:05. 6 | */ 7 | data class Size(var width: Int, var height: Int) { 8 | } -------------------------------------------------------------------------------- /pager/src/main/java/cc/aoeiuv020/pager/animation/AnimationConfig.kt: -------------------------------------------------------------------------------- 1 | package cc.aoeiuv020.pager.animation 2 | 3 | import android.view.View 4 | import cc.aoeiuv020.pager.IMargins 5 | 6 | /** 7 | * 8 | * Created by AoEiuV020 on 2017.12.09-15:42:38. 9 | */ 10 | data class AnimationConfig( 11 | var width: Int, 12 | var height: Int, 13 | var margins: IMargins, 14 | var view: View, 15 | var listener: PageAnimation.OnPageChangeListener, 16 | var durationMultiply: Float 17 | ) -------------------------------------------------------------------------------- /pager/src/main/java/cc/aoeiuv020/pager/animation/NonePageAnim.java: -------------------------------------------------------------------------------- 1 | package cc.aoeiuv020.pager.animation; 2 | 3 | import android.graphics.Canvas; 4 | import android.view.View; 5 | 6 | import cc.aoeiuv020.pager.IMargins; 7 | 8 | /** 9 | * Created by newbiechen on 17-7-24. 10 | */ 11 | 12 | @SuppressWarnings("All") 13 | public class NonePageAnim extends HorizonPageAnim { 14 | 15 | public NonePageAnim(AnimationConfig config) { 16 | super(config); 17 | } 18 | 19 | public NonePageAnim(int w, int h, IMargins margins, View view, OnPageChangeListener listener) { 20 | super(w, h, margins, view, listener); 21 | } 22 | 23 | @Override 24 | public void drawMove(Canvas canvas) { 25 | if (isCancel) { 26 | canvas.drawBitmap(mCurBitmap, 0, 0, null); 27 | } else { 28 | canvas.drawBitmap(mNextBitmap, 0, 0, null); 29 | } 30 | } 31 | 32 | @Override 33 | public void startAnim() { 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /pager/src/main/java/cc/aoeiuv020/pager/margins.kt: -------------------------------------------------------------------------------- 1 | package cc.aoeiuv020.pager 2 | 3 | /** 4 | * 5 | * Created by AoEiuV020 on 2018.03.11-01:57:32. 6 | */ 7 | interface IMargins { 8 | val left: Int 9 | val top: Int 10 | val right: Int 11 | val bottom: Int 12 | } 13 | 14 | class IMarginsImpl( 15 | override var left: Int = 0, 16 | override var top: Int = 0, 17 | override var right: Int = 0, 18 | override var bottom: Int = 0 19 | ) : IMargins { 20 | @Suppress("unused") 21 | constructor(margins: IMargins) : this( 22 | left = margins.left, 23 | top = margins.top, 24 | right = margins.right, 25 | bottom = margins.bottom 26 | ) 27 | } 28 | -------------------------------------------------------------------------------- /pager/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /pager/src/test/java/cc/aoeiuv020/pager/ExampleUnitTest.java: -------------------------------------------------------------------------------- 1 | package cc.aoeiuv020.pager; 2 | 3 | import org.junit.Test; 4 | 5 | import static org.junit.Assert.assertEquals; 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * @see Testing documentation 11 | */ 12 | public class ExampleUnitTest { 13 | @Test 14 | public void addition_isCorrect() throws Exception { 15 | assertEquals(4, 2 + 2); 16 | } 17 | } -------------------------------------------------------------------------------- /publish.properties.example: -------------------------------------------------------------------------------- 1 | # suppress inspection "UnusedProperty" for whole file 2 | bintrayUser=aoeiuv020 3 | bintrayKey= 4 | userOrg=aoeiuv020 5 | groupId=cc.aoeiuv020 6 | website=https://github.com/AoEiuV020/PaNovel 7 | licences=MIT 8 | -------------------------------------------------------------------------------- /reader/build.gradle: -------------------------------------------------------------------------------- 1 | dependencies { 2 | implementation 'androidx.appcompat:appcompat:' + androidx_version 3 | implementation 'androidx.recyclerview:recyclerview:' + androidx_version 4 | implementation 'androidx.constraintlayout:constraintlayout:' + constraint_layout_version 5 | implementation project(':pager') 6 | implementation project(':baseJar') 7 | implementation 'com.github.bumptech.glide:glide:' + glide_version 8 | api 'com.github.chrisbanes:PhotoView:' + photo_view_version 9 | } 10 | -------------------------------------------------------------------------------- /reader/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /reader/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /reader/src/main/java/cc/aoeiuv020/reader/AnimationMode.kt: -------------------------------------------------------------------------------- 1 | package cc.aoeiuv020.reader 2 | 3 | import cc.aoeiuv020.pager.AnimMode 4 | 5 | /** 6 | * 所有翻页动画, 7 | * SIMPLE对应旧阅读器, 8 | * 剩下的对应新阅读器的所有动画,AnimMode, 9 | * Created by AoEiuV020 on 2017.12.09-01:01:49. 10 | */ 11 | enum class AnimationMode { 12 | SIMPLE, 13 | SIMULATION, 14 | COVER, 15 | SLIDE, 16 | NONE, 17 | SCROLL; 18 | 19 | fun toAnimMode(): AnimMode = when (this) { 20 | SIMPLE -> { 21 | throw IllegalStateException("简单翻页动画不可用在这里,") 22 | } 23 | SIMULATION -> AnimMode.SIMULATION 24 | COVER -> AnimMode.COVER 25 | SLIDE -> AnimMode.SLIDE 26 | NONE -> AnimMode.NONE 27 | SCROLL -> AnimMode.SCROLL 28 | } 29 | 30 | companion object { 31 | @Suppress("unused") 32 | fun fromAnimMode(animMode: AnimMode): AnimationMode = when (animMode) { 33 | AnimMode.SIMULATION -> SIMULATION 34 | AnimMode.COVER -> COVER 35 | AnimMode.SLIDE -> SLIDE 36 | AnimMode.NONE -> NONE 37 | AnimMode.SCROLL -> SCROLL 38 | } 39 | } 40 | } -------------------------------------------------------------------------------- /reader/src/main/java/cc/aoeiuv020/reader/ConfigChangedListener.kt: -------------------------------------------------------------------------------- 1 | package cc.aoeiuv020.reader 2 | 3 | internal interface ConfigChangedListener { 4 | fun onConfigChanged(name: ReaderConfigName) 5 | } -------------------------------------------------------------------------------- /reader/src/main/java/cc/aoeiuv020/reader/INovelReader.kt: -------------------------------------------------------------------------------- 1 | package cc.aoeiuv020.reader 2 | 3 | import android.content.Context 4 | import java.util.concurrent.ExecutorService 5 | import java.util.concurrent.Executors 6 | 7 | /** 8 | * 9 | * Created by AoEiuV020 on 2017.12.01-02:13:39. 10 | */ 11 | interface INovelReader { 12 | val ctx: Context 13 | 14 | var novel: String 15 | 16 | var readingListener: ReadingListener? 17 | var menuListener: MenuListener? 18 | 19 | var requester: TextRequester 20 | var chapterList: List 21 | 22 | var currentChapter: Int 23 | var textProgress: Int 24 | val maxTextProgress: Int 25 | 26 | val config: ReaderConfig 27 | 28 | fun refreshCurrentChapter() 29 | 30 | fun scrollNext(): Boolean 31 | fun scrollPrev(): Boolean 32 | 33 | fun destroy() 34 | } 35 | 36 | abstract class BaseNovelReader(override var novel: String, override var requester: TextRequester) : INovelReader { 37 | override var readingListener: ReadingListener? = null 38 | override var menuListener: MenuListener? = null 39 | override var chapterList: List = emptyList() 40 | // 独立的线程池用于请求小说章节,不要用anko-common自带的以免导致阻塞, 41 | val ioExecutorService: ExecutorService = Executors.newCachedThreadPool() 42 | 43 | override fun scrollNext(): Boolean = false 44 | override fun scrollPrev(): Boolean = false 45 | } 46 | 47 | -------------------------------------------------------------------------------- /reader/src/main/java/cc/aoeiuv020/reader/ReaderConfigName.kt: -------------------------------------------------------------------------------- 1 | package cc.aoeiuv020.reader 2 | 3 | /** 4 | * 5 | * Created by AoEiuV020 on 2017.12.08-03:46:01. 6 | */ 7 | enum class ReaderConfigName { 8 | TextSize, MessageSize, DateFormat, 9 | LineSpacing, ParagraphSpacing, 10 | ContentMargins, 11 | PaginationMargins, TimeMargins, BatteryMargins, BookNameMargins, ChapterNameMargins, 12 | PaginationEnabled, TimeEnabled, BatteryEnabled, BookNameEnabled, ChapterNameEnabled, 13 | TextColor, BackgroundColor, BackgroundImage, 14 | AnimationMode, AnimDurationMultiply, 15 | Font, 16 | FullScreenClickNextPage, CenterPercent, FitWidth, FitHeight 17 | } -------------------------------------------------------------------------------- /reader/src/main/java/cc/aoeiuv020/reader/Readers.kt: -------------------------------------------------------------------------------- 1 | package cc.aoeiuv020.reader 2 | 3 | import android.content.Context 4 | import android.view.ViewGroup 5 | import cc.aoeiuv020.reader.complex.ComplexReader 6 | import cc.aoeiuv020.reader.simple.SimpleReader 7 | 8 | /** 9 | * 10 | * Created by AoEiuV020 on 2017.12.01-01:16:49. 11 | */ 12 | object Readers { 13 | 14 | fun getReader(ctx: Context, novel: String, parent: ViewGroup, requester: TextRequester, config: ReaderConfig) 15 | : INovelReader = if (config.animationMode == AnimationMode.SIMPLE) { 16 | SimpleReader(ctx, novel, parent, requester, config) 17 | } else { 18 | ComplexReader(ctx, novel, parent, requester, config) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /reader/src/main/java/cc/aoeiuv020/reader/complex/Page.kt: -------------------------------------------------------------------------------- 1 | package cc.aoeiuv020.reader.complex 2 | 3 | import java.util.* 4 | 5 | 6 | /** 7 | * 8 | * Created by AoEiuV020 on 2017.12.08-00:48:07. 9 | */ 10 | data class Page( 11 | var lines: List, 12 | var fitLineSpacing: Int 13 | ) { 14 | constructor(lines: ArrayList) : this(lines, 0) 15 | } -------------------------------------------------------------------------------- /reader/src/main/java/cc/aoeiuv020/reader/complex/type.kt: -------------------------------------------------------------------------------- 1 | package cc.aoeiuv020.reader.complex 2 | 3 | /** 4 | * 5 | * Created by AoEiuV020 on 2017.12.08-02:39:43. 6 | */ 7 | data class Title(val string: String) 8 | data class ParagraphSpacing(val height: Int) -------------------------------------------------------------------------------- /reader/src/main/java/cc/aoeiuv020/reader/data.kt: -------------------------------------------------------------------------------- 1 | package cc.aoeiuv020.reader 2 | 3 | import java.net.URL 4 | 5 | /** 6 | * Created by AoEiuV020 on 2018.06.18-16:40:23. 7 | */ 8 | 9 | class Image( 10 | // 参考glide, 各种可以得到图片的model, 不局限于URL, 11 | // 仔细想想好像没什么必要,URL能带URLStreamHandler, 可以自己决定从url得到输入流的方法, 12 | val url: URL 13 | ) { 14 | // 阅读器不支持的话可以调用toString直接展示文本, 15 | override fun toString(): String { 16 | return "![img]($url)" 17 | } 18 | } 19 | 20 | -------------------------------------------------------------------------------- /reader/src/main/java/cc/aoeiuv020/reader/ext.kt: -------------------------------------------------------------------------------- 1 | package cc.aoeiuv020.reader 2 | 3 | import android.view.View 4 | 5 | /** 6 | * 7 | * Created by AoEiuV020 on 2017.12.01-15:23:42. 8 | */ 9 | fun View.setHeight(height: Int) { 10 | layoutParams = layoutParams.also { it.height = height } 11 | } 12 | 13 | fun View.hide() { 14 | visibility = View.GONE 15 | } 16 | 17 | fun View.show() { 18 | visibility = View.VISIBLE 19 | } 20 | -------------------------------------------------------------------------------- /reader/src/main/java/cc/aoeiuv020/reader/listener.kt: -------------------------------------------------------------------------------- 1 | package cc.aoeiuv020.reader 2 | 3 | import java.io.File 4 | 5 | /** 6 | * 7 | * Created by AoEiuV020 on 2017.12.01-02:17:55. 8 | */ 9 | 10 | interface TextRequester { 11 | fun requestChapter(index: Int, refresh: Boolean = false): List 12 | // 小说章节正文由一个个段落构成,一个段落可能是一行文字,也可能是一张图片, 13 | // 支持图片就判断一下,否则直接toString, 14 | fun requestParagraph(string: String): Any 15 | 16 | // 请求图片,回调图片文件, 17 | fun requestImage( 18 | image: Image, 19 | exceptionHandler: (Throwable) -> Unit = { 20 | it.printStackTrace() 21 | }, 22 | block: (File) -> Unit 23 | ) 24 | } 25 | 26 | /** 27 | * 阅读进度监听器, 28 | * 进度改变时调用, 29 | */ 30 | interface ReadingListener { 31 | fun onReading(chapter: Int, text: Int) 32 | } 33 | 34 | // 这东西有点多余,换成点中心的回调就可以了, 35 | interface MenuListener { 36 | fun hide() 37 | fun show() 38 | fun toggle() 39 | } 40 | 41 | -------------------------------------------------------------------------------- /reader/src/main/java/cc/aoeiuv020/reader/margins.kt: -------------------------------------------------------------------------------- 1 | package cc.aoeiuv020.reader 2 | 3 | import cc.aoeiuv020.pager.IMarginsImpl 4 | 5 | /** 6 | * 阅读器页面中有显示时间,电量之类的, 7 | * 每一个都可以设置留白,可以禁用, 8 | * 为了隐藏pager模块的接口IMargins而声明一个自己的接口, 9 | * 不继承IMargins主要是, 10 | * app依赖reader, reader依赖pager, 11 | * app使用reader中继承pager接口的接口也会编译出错,stub截断就过不去, 12 | * 13 | * Created by AoEiuV020 on 2018.06.17-16:07:30. 14 | */ 15 | interface ItemMargins { 16 | /** 17 | * 对应的东西是否显示, 18 | */ 19 | val enabled: Boolean 20 | val left: Int 21 | val top: Int 22 | val right: Int 23 | val bottom: Int 24 | } 25 | 26 | fun ItemMargins.toIMargins() = IMarginsImpl( 27 | left = this.left, 28 | top = this.top, 29 | right = this.right, 30 | bottom = this.bottom 31 | ) -------------------------------------------------------------------------------- /reader/src/main/java/cc/aoeiuv020/reader/simple/ResizableImageView.kt: -------------------------------------------------------------------------------- 1 | package cc.aoeiuv020.reader.simple 2 | 3 | import android.content.Context 4 | import android.util.AttributeSet 5 | import android.view.View 6 | import com.github.chrisbanes.photoview.PhotoView 7 | 8 | /** 9 | * https://stackoverflow.com/a/12283909/5615186 10 | * Created by AoEiuV020 on 2018.06.11-10:56:37. 11 | */ 12 | class ResizableImageView : PhotoView { 13 | constructor(context: Context) 14 | : super(context) 15 | 16 | constructor(context: Context, attrs: AttributeSet) 17 | : super(context, attrs) 18 | 19 | constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) 20 | : super(context, attrs, defStyleAttr) 21 | 22 | override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { 23 | val d = drawable 24 | 25 | if (d != null) { 26 | // ceil not round - avoid thin vertical gaps along the left/right edges 27 | val width = View.MeasureSpec.getSize(widthMeasureSpec) 28 | val height = Math.ceil((width.toFloat() * d.intrinsicHeight.toFloat() / d.intrinsicWidth.toFloat()).toDouble()).toInt() 29 | setMeasuredDimension(width, height) 30 | } else { 31 | super.onMeasure(widthMeasureSpec, heightMeasureSpec) 32 | } 33 | } 34 | 35 | } -------------------------------------------------------------------------------- /reader/src/main/res/layout/simple.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 14 | 15 | 19 | 20 | -------------------------------------------------------------------------------- /reader/src/main/res/layout/simple_image_item.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 15 | 16 | 17 | 23 | -------------------------------------------------------------------------------- /reader/src/main/res/layout/simple_text_item.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 14 | 15 | 19 | 20 | -------------------------------------------------------------------------------- /reader/src/main/res/layout/simple_view_pager_item.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 11 | 12 | 15 | 16 | 23 | 24 | -------------------------------------------------------------------------------- /reader/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /reader/src/test/java/cc/aoeiuv020/reader/ExampleUnitTest.java: -------------------------------------------------------------------------------- 1 | package cc.aoeiuv020.reader; 2 | 3 | import org.junit.Test; 4 | 5 | import static org.junit.Assert.assertEquals; 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * @see Testing documentation 11 | */ 12 | public class ExampleUnitTest { 13 | @Test 14 | public void addition_isCorrect() throws Exception { 15 | assertEquals(4, 2 + 2); 16 | } 17 | } -------------------------------------------------------------------------------- /refresher/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /refresher/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'application' 2 | 3 | mainClassName = "cc.aoeiuv020.panovel.refresher.ApplicationKt" 4 | 5 | dependencies { 6 | implementation 'org.slf4j:slf4j-simple:' + slf4j_version 7 | implementation project(':baseJar') 8 | implementation project(':api') 9 | implementation project(':server') 10 | } 11 | -------------------------------------------------------------------------------- /refresher/src/main/java/cc/aoeiuv020/panovel/api/context.kt: -------------------------------------------------------------------------------- 1 | package cc.aoeiuv020.panovel.api 2 | 3 | /** 4 | * Created by AoEiuV020 on 2018.06.04-19:10:18. 5 | */ 6 | @Suppress("RemoveExplicitTypeArguments") 7 | val contexts: List by lazy { 8 | NovelContext.getAllSite() 9 | } 10 | private val nameMap by lazy { 11 | contexts.associateBy { it.site.name } 12 | } 13 | 14 | fun getNovelContextByName(name: String): NovelContext { 15 | return nameMap[name] ?: throw IllegalArgumentException("网站不支持: $name") 16 | } 17 | -------------------------------------------------------------------------------- /refresher/src/main/java/cc/aoeiuv020/panovel/refresher/Application.kt: -------------------------------------------------------------------------------- 1 | package cc.aoeiuv020.panovel.refresher 2 | 3 | import cc.aoeiuv020.jsonpath.JsonPathUtils 4 | import cc.aoeiuv020.panovel.server.ServerAddress 5 | import cc.aoeiuv020.panovel.server.common.toBean 6 | import java.io.File 7 | 8 | /** 9 | * Created by AoEiuV020 on 2018.04.21-16:05:40. 10 | */ 11 | fun main(args: Array) { 12 | JsonPathUtils.initGson() 13 | val ite = args.iterator() 14 | var address: ServerAddress = ServerAddress.getDefault() 15 | var config = Config() 16 | val bookshelfList = mutableSetOf() 17 | 18 | while (ite.hasNext()) { 19 | when (ite.next()) { 20 | "-a" -> { 21 | address = File(ite.next()).readText().toBean() 22 | } 23 | "-h" -> { 24 | address = ServerAddress.new(ite.next()) 25 | } 26 | "-b" -> { 27 | bookshelfList.add(ite.next()) 28 | } 29 | "-c" -> { 30 | config = File(ite.next()).readText().toBean() 31 | } 32 | } 33 | } 34 | Refresher(config = config).start(address = address, bookshelfList = bookshelfList) 35 | } 36 | 37 | -------------------------------------------------------------------------------- /refresher/src/main/java/cc/aoeiuv020/panovel/refresher/Config.kt: -------------------------------------------------------------------------------- 1 | package cc.aoeiuv020.panovel.refresher 2 | 3 | import java.util.concurrent.TimeUnit 4 | 5 | data class Config( 6 | /** 7 | * 每轮的最小时间,太快了就休息到够, 8 | */ 9 | val minTime: Long = TimeUnit.MINUTES.toMillis(10), 10 | /** 11 | * 目标时间,希望每轮执行时间, 12 | */ 13 | val targetTime: Long = TimeUnit.MINUTES.toMillis(30), 14 | /** 15 | * 一轮最多拿这么多个, 16 | */ 17 | val maxSize: Int = 100, 18 | /** 19 | * 是否必须成功获取书架, 20 | * 否则直接让程序崩溃, 21 | */ 22 | val requireBookshelf: Boolean = true, 23 | /** 24 | * 调试模式, 25 | */ 26 | val debug: Boolean = false, 27 | /** 28 | * 并发请求线程数, 29 | */ 30 | val threads: Int = 10, 31 | /** 32 | * 禁用的网站名, 33 | */ 34 | val disableSites: Set = setOf() 35 | ) -------------------------------------------------------------------------------- /refresher/src/main/java/cc/aoeiuv020/panovel/share/PasteUbuntu.kt: -------------------------------------------------------------------------------- 1 | package cc.aoeiuv020.panovel.share 2 | 3 | import cc.aoeiuv020.base.jar.jsoupConnect 4 | 5 | /** 6 | * 网上贴文本,免费还无限制, 7 | * 8 | * Created by AoEiuV020 on 2018.03.07-19:16:41. 9 | */ 10 | internal class PasteUbuntu { 11 | companion object { 12 | const val homePage = "https://paste.ubuntu.com/" 13 | } 14 | 15 | fun check(url: String): Boolean { 16 | return url.startsWith(homePage) 17 | } 18 | 19 | fun download(url: String): String { 20 | val root = jsoupConnect(url) 21 | return root.select("#contentColumn > div > div > div > table > tbody > tr > td.code > div > pre").first().text() 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /screenshots/bookshelf.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AoEiuV020/PaNovel/48fc2248521cfeb52ac9fbcbb9a09e2fc6e0e95d/screenshots/bookshelf.jpg -------------------------------------------------------------------------------- /screenshots/detail.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AoEiuV020/PaNovel/48fc2248521cfeb52ac9fbcbb9a09e2fc6e0e95d/screenshots/detail.jpg -------------------------------------------------------------------------------- /screenshots/epub.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AoEiuV020/PaNovel/48fc2248521cfeb52ac9fbcbb9a09e2fc6e0e95d/screenshots/epub.jpg -------------------------------------------------------------------------------- /screenshots/genre.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AoEiuV020/PaNovel/48fc2248521cfeb52ac9fbcbb9a09e2fc6e0e95d/screenshots/genre.jpg -------------------------------------------------------------------------------- /screenshots/list.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AoEiuV020/PaNovel/48fc2248521cfeb52ac9fbcbb9a09e2fc6e0e95d/screenshots/list.jpg -------------------------------------------------------------------------------- /screenshots/text.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AoEiuV020/PaNovel/48fc2248521cfeb52ac9fbcbb9a09e2fc6e0e95d/screenshots/text.jpg -------------------------------------------------------------------------------- /server/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /server/build.gradle: -------------------------------------------------------------------------------- 1 | dependencies { 2 | api 'org.jsoup:jsoup:' + jsoup_version 3 | implementation project(':baseJar') 4 | } 5 | -------------------------------------------------------------------------------- /server/src/main/java/cc/aoeiuv020/panovel/server/ServerAddress.kt: -------------------------------------------------------------------------------- 1 | package cc.aoeiuv020.panovel.server 2 | 3 | /** 4 | * 5 | * Created by AoEiuV020 on 2018.04.06-13:03:41. 6 | */ 7 | class ServerAddress( 8 | val baseUrl: String 9 | ) { 10 | companion object { 11 | const val MESSAGE_HOST = "msg.panovel.aoeiuv020.com" 12 | const val CONFIG_HOST = "panovel.aoeiuv020.com" 13 | private const val PANOVEL_API_HOST = "http://panovel.aoeiuv020.com" 14 | 15 | fun getDefault(): ServerAddress = new(PANOVEL_API_HOST) 16 | 17 | fun new(host: String): ServerAddress { 18 | return ServerAddress(host) 19 | } 20 | } 21 | 22 | val updateUploadUrl: String 23 | get() = "$baseUrl/novel/update" 24 | 25 | val needRefreshNovelListUrl: String 26 | get() = "$baseUrl/novel/needRefreshNovelList" 27 | 28 | val queryListUrl: String 29 | get() = "$baseUrl/novel/queryList" 30 | 31 | val touchUrl: String 32 | get() = "$baseUrl/novel/touch" 33 | 34 | val minVersionUrl: String 35 | get() = "$baseUrl/novel/minVersion" 36 | 37 | val config: String 38 | get() = "$baseUrl/novel/config" 39 | 40 | val message: String 41 | get() = "$baseUrl/novel/message" 42 | } 43 | -------------------------------------------------------------------------------- /server/src/main/java/cc/aoeiuv020/panovel/server/common/ErrorCode.kt: -------------------------------------------------------------------------------- 1 | package cc.aoeiuv020.panovel.server.common 2 | 3 | /** 4 | * 5 | * Created by AoEiuV020 on 2018.04.02-11:56:23. 6 | */ 7 | enum class ErrorCode(val code: Int) { 8 | SUCCESS(200), 9 | UNKNOWN_ERROR(-1) 10 | } -------------------------------------------------------------------------------- /server/src/main/java/cc/aoeiuv020/panovel/server/common/json.kt: -------------------------------------------------------------------------------- 1 | package cc.aoeiuv020.panovel.server.common 2 | 3 | import cc.aoeiuv020.gson.GsonUtils 4 | import com.google.gson.Gson 5 | import com.google.gson.reflect.TypeToken 6 | import java.lang.reflect.Type 7 | 8 | /** 9 | * 10 | * Created by AoEiuV020 on 2018.04.05-10:35:13. 11 | */ 12 | val gson: Gson = GsonUtils.gsonBuilder 13 | .create() 14 | 15 | inline fun type(): Type = object : TypeToken() {}.type 16 | inline fun String.toBean(): T = toBean(type()) 17 | fun String.toBean(type: Type): T = gson.fromJson(this, type) 18 | ?: throw IllegalStateException("empty json") 19 | 20 | fun Any.toJson(): String = gson.toJson(this) 21 | -------------------------------------------------------------------------------- /server/src/main/java/cc/aoeiuv020/panovel/server/common/md5.kt: -------------------------------------------------------------------------------- 1 | package cc.aoeiuv020.panovel.server.common 2 | 3 | import cc.aoeiuv020.encrypt.hex 4 | import cc.aoeiuv020.encrypt.md5 5 | import cc.aoeiuv020.panovel.server.dal.model.autogen.Novel 6 | 7 | /** 8 | * 计算md5充当推送的tag, 9 | * 两端计算结果必需一致, 10 | * 11 | * Created by AoEiuV020 on 2018.04.17-13:01:51. 12 | */ 13 | // 算md5以确保长度小于,40, 极光推送限制tag长度40, 14 | fun Novel.md5(): String = "$site.$author.$name".md5().hex() 15 | -------------------------------------------------------------------------------- /server/src/main/java/cc/aoeiuv020/panovel/server/common/novel.kt: -------------------------------------------------------------------------------- 1 | package cc.aoeiuv020.panovel.server.common 2 | 3 | import cc.aoeiuv020.panovel.server.dal.model.autogen.Novel 4 | 5 | /** 6 | * Created by AoEiuV020 on 2018.06.19-16:54:09. 7 | */ 8 | val Novel.bookId: String get() = "$name.$author.$site" 9 | -------------------------------------------------------------------------------- /server/src/main/java/cc/aoeiuv020/panovel/server/dal/model/Config.kt: -------------------------------------------------------------------------------- 1 | package cc.aoeiuv020.panovel.server.dal.model 2 | 3 | /** 4 | * Created by AoEiuV020 on 2018.09.04-19:29:14. 5 | */ 6 | data class Config( 7 | val apiUrl: String?, 8 | val minVersion: String, 9 | val qqGroup: String? 10 | ) -------------------------------------------------------------------------------- /server/src/main/java/cc/aoeiuv020/panovel/server/dal/model/Message.kt: -------------------------------------------------------------------------------- 1 | package cc.aoeiuv020.panovel.server.dal.model 2 | 3 | /** 4 | * Created by AoEiuV020 on 2018.09.04-19:29:14. 5 | */ 6 | data class Message( 7 | val title: String?, 8 | val content: String? 9 | ) -------------------------------------------------------------------------------- /server/src/main/java/cc/aoeiuv020/panovel/server/dal/model/MobRequest.kt: -------------------------------------------------------------------------------- 1 | package cc.aoeiuv020.panovel.server.dal.model 2 | 3 | 4 | /** 5 | * 6 | * Created by AoEiuV020 on 2018.04.05-08:01:30. 7 | */ 8 | class MobRequest( 9 | val data: String = "{}" 10 | ) -------------------------------------------------------------------------------- /server/src/main/java/cc/aoeiuv020/panovel/server/dal/model/MobResponse.kt: -------------------------------------------------------------------------------- 1 | package cc.aoeiuv020.panovel.server.dal.model 2 | 3 | import cc.aoeiuv020.panovel.server.common.ErrorCode 4 | import cc.aoeiuv020.panovel.server.common.toBean 5 | import cc.aoeiuv020.panovel.server.common.toJson 6 | import java.lang.reflect.Type 7 | 8 | /** 9 | * 10 | * Created by AoEiuV020 on 2018.04.02-11:21:33. 11 | */ 12 | class MobResponse( 13 | var code: Int = ErrorCode.UNKNOWN_ERROR.code, 14 | val data: String = "{}" 15 | ) { 16 | companion object { 17 | fun success(data: Any = Any()): MobResponse { 18 | return MobResponse(ErrorCode.SUCCESS.code, data.toJson()) 19 | } 20 | 21 | fun error(error: ErrorCode = ErrorCode.UNKNOWN_ERROR): MobResponse { 22 | return MobResponse(error.code) 23 | } 24 | } 25 | 26 | inline fun getRealData(): T { 27 | return data.toBean() 28 | } 29 | 30 | fun getRealData(type: Type): T { 31 | return data.toBean(type) 32 | } 33 | 34 | fun isSuccess(): Boolean { 35 | return code == ErrorCode.SUCCESS.code 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /server/src/main/java/cc/aoeiuv020/panovel/server/dal/model/QueryResponse.kt: -------------------------------------------------------------------------------- 1 | package cc.aoeiuv020.panovel.server.dal.model 2 | 3 | import java.util.* 4 | 5 | /** 6 | * Created by AoEiuV020 on 2018.06.19-16:14:40. 7 | */ 8 | data class QueryResponse( 9 | val chaptersCount: Int, 10 | val checkUpdateTime: Date 11 | ) -------------------------------------------------------------------------------- /server/src/main/java/cc/aoeiuv020/panovel/server/service/NovelService.kt: -------------------------------------------------------------------------------- 1 | package cc.aoeiuv020.panovel.server.service 2 | 3 | import cc.aoeiuv020.panovel.server.dal.model.Config 4 | import cc.aoeiuv020.panovel.server.dal.model.Message 5 | import cc.aoeiuv020.panovel.server.dal.model.QueryResponse 6 | import cc.aoeiuv020.panovel.server.dal.model.autogen.Novel 7 | 8 | /** 9 | * 10 | * Created by AoEiuV020 on 2018.04.05-09:13:02. 11 | */ 12 | interface NovelService { 13 | val baseurl: String 14 | fun uploadUpdate(novel: Novel): Boolean 15 | fun needRefreshNovelList(count: Int): List 16 | fun queryList(novelMap: Map): Map 17 | fun touch(novel: Novel): Boolean 18 | fun minVersion(): String 19 | fun config(): Config 20 | fun message(): Message 21 | } -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'PaNovel' 2 | include ':app' 3 | include ':js' 4 | include ':api' 5 | include ':pager' 6 | include ':server' 7 | include ':baseJar' 8 | include ':refresher' 9 | include ':IronDB' 10 | include ':local' 11 | include ':reader' 12 | include ':filepicker' 13 | -------------------------------------------------------------------------------- /template-update.md: -------------------------------------------------------------------------------- 1 | *爬小说更新,${VERSION_PREFIX}${BUILD_VERSION}* 2 | [CI](https://github.com/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}) [TG](https://t.me/PaNovelGroup) [反馈](https://github.com/${GITHUB_REPOSITORY}/issues) 3 | 4 | *下载地址:* 5 | [GITHUB](https://github.com/${GITHUB_REPOSITORY}/releases/tag/${BUILD_VERSION}) 6 | 7 | *更新日志:* 8 | ${TG_CHANGELOG} 9 | -------------------------------------------------------------------------------- /version.properties: -------------------------------------------------------------------------------- 1 | # suppress inspection "UnusedProperty" for whole file 2 | compile_version=30 3 | min_version=16 4 | target_version=30 5 | version_code=90 6 | version_name=3.4.5 7 | studio_version=3.3.0 8 | jitpack_android_version=2.1 9 | dokka_version=0.9.17 10 | bintray_version=0.9 11 | androidx_version=1.0.0 12 | support_version=28.0.0 13 | constraint_layout_version=1.1.3 14 | test_runner_version=1.1.0 15 | test_espresso_version=3.1.1 16 | multidex_version=2.0.0 17 | mockito_version=2.15.0 18 | 19 | kotlin_version=1.3.0 20 | anko_version=0.10.1 21 | junit_version=4.12 22 | slf4j_version=1.7.25 23 | debug_db_version=1.0.4 24 | 25 | # https://github.com/AoEiuV020/AndroidDemo 26 | a_commons_version=1.2 27 | a_gson_version=1.1 28 | a_jsonpath_version=1.1 29 | a_okhttp_version=1.3 30 | a_epublib_version=3.1.3 31 | 32 | # Others, 33 | jsoup_version=1.11.3 34 | glide_version=4.1.1 35 | google_version=4.0.1 36 | firebase_core_version=16.0.3 37 | firebase_ads_version=15.0.1 38 | room_version=2.0.0 39 | rhino_version=1.7.10 40 | photo_view_version=2.3.0 41 | --------------------------------------------------------------------------------